Variables, templates, file loading, executables, and built-in methods form mlld's core syntax. exe defines reusable functions (js, node, python, shell, prose). run invokes them. => returns values from scripts and blocks.

Variables

var vs exe: var creates values (no parameters). exe creates functions (takes parameters).

var @name = "Alice"            >> value - no params
exe @greet(who) = `Hi @who!`   >> function - takes params

>> Use var for computed values, exe for reusable functions
var @result = @greet(@name)    >> "Hi Alice!"

Primitives, arrays, objects:

var @n = 42
var @price = 19.99
var @ok = true
var @arr = [1, 2, 3]
var @obj = {"key": "value"}
var @merged = { ...@obj, "extra": 1 }    >> object spread

exe @add(a, b) = js { return a + b }
var @sum = @add(@n, 8)         >> 50 (number preserved)

var @date = cmd {date}         >> command result
var @readme = <README.md>      >> file contents

See Also

Conditional Inclusion

Conditional inclusion (@var?): omit content when a variable is falsy.
Nullish fallback in templates (@var??"default"): use tight ?? binding in template interpolation.
Nullish fallback in expressions (@a ?? @b): use spaced ?? in var/let expressions and chains.

var @tools = "json"
var @empty = ""

>> In commands: @var?`...`
run cmd { echo @tools?`--tools "@tools"` @empty?`--empty` }
>> Output: --tools json (--empty omitted)

>> In templates: omit the variable itself
var @title = "MyTitle"
var @msg = `DEBUG:@title?`

>> In templates: tight null coalescing
var @missing = null
var @hello = `Hello,@missing??"friend"`

>> In expressions: spaced null coalescing (chaining works)
var @primary = null
var @secondary = "backup"
var @chosen = @primary ?? @secondary ?? "fallback"

>> In arrays
var @list = [@a, @b?, @c]   >> @b omitted if falsy

>> In objects
var @obj = {"name": @n, "title"?: @t}   >> title omitted if @t falsy

Truthiness Rules

Truthiness: falsy = null, undefined, "", "false", "0", 0, NaN, [], {}

Templates

var @message = `Hello @name, welcome!`
var @doc = ::Use `mlld` to orchestrate::

>> Multi-line
var @report = `
Status: @status
Config: <@root/config.json>
Data: @data|@parse
`

External Template Files (.att)

For templates longer than a simple paragraph, use .att files with the exe ... = template pattern:

exe @welcome(name, role) = template "./prompts/welcome.att"
exe @systemPrompt(context) = template "./prompts/system.att"

>> Call like any other function
show @welcome("Alice", "admin")

The .att file uses @var for interpolation and supports full mlld syntax inside:

>> prompts/welcome.att
Hello @name!

Your role: @role

/for @perm in @role.permissions
- @perm
/end

Key points:

  • Parameters from exe @func(params) are automatically available as @param in the template
  • .att supports <file.md> references and /for ... /end template loops
  • In template content, use condensed pipe syntax (@value|@pipe) to avoid ambiguity
  • Relative <file> paths inside a template file resolve from that template file's directory
  • Use @@ or \@ to output a literal @ symbol (e.g., user@@example.com)
  • Never load .att files with angle brackets - use exe ... = template instead

When to use external templates:

  • Prompts longer than 2-3 lines
  • Templates with complex structure or loops
  • Reusable prompt components
  • Agent system prompts

Alternative syntax - .mtt files:

If .att syntax conflicts with your content, use .mtt files instead. Common case: prompts that include @path/to/file references for LLMs to interpret:

exe @codeReview(files, instructions) = template "./prompts/review.mtt"
>> prompts/review.mtt - uses {{var}} syntax
Review these files: {{files}}

{{instructions}}

Reference @src/utils.ts and @tests/utils.test.ts for context.

The @src/utils.ts stays literal - mlld won't try to interpolate it.

Extension Variable syntax File refs Use when
.att @var <file.md> Default, mlld-style
.mtt {{var}} None Content has @path or <tag> meant for LLMs
Feature .att .mtt
Pipes in template body Condensed only (`@var @pipe`)
Loops /for ... /end Not supported

Loops in Templates

Template-embedded vs top-level: Control flow inside templates uses /for and /end with slash prefix. Top-level directives are separate statements.

>> TOP-LEVEL: for as a directive (produces output directly)
for @item in @items => show `- @item.name`

>> TEMPLATE-EMBEDDED: /for inside template body
var @items = ["alpha", "beta", "gamma"]
var @toc = ::
/for @item in @items
- @item
/end
::
show @toc

Key/value form in templates:

/for @k, @v in @items
- @k: @v
/end

Key differences:

Context Syntax Notes
Top-level for @x in @y => action Directive on own line
In template /for @x in @y ... /end Slash prefix required at line start

Template-embedded rules:

  • /for and /end must start at column 1 of their line inside the template
  • No => arrow - the lines between /for and /end are the body
  • Only /for and /end are valid inside templates - no other directives

File Loading

>> Basic loading
var @content = <README.md>
var @config = <config.json>          >> auto-parsed as object
var @events = <events.jsonl>         >> auto-parsed as array of JSON objects
var @author = <package.json>.author  >> field access
show @events[0].event                >> first JSONL record

>> Globs (returns array)
var @docs = <docs/**/*.md>
show @docs.length
for @doc in @docs => show @doc.mx.filename

>> JSON globs - each item is auto-parsed
var @configs = <configs/*.json>
var @first = @configs[0]
show @first.name                     >> access parsed JSON
show @first.mx.data.name             >> explicit parsed-data view
show @first.mx.text                  >> raw file text
show @first.mx.filename              >> file metadata still available

>> With "as" template
var @toc = <docs/*.md> as "- [<>.mx.fm.title](<>.mx.relative)"

Structured values loaded from files are data-first:

  • @config.apiUrl and @config.mx.data.apiUrl are equivalent.
  • @config.mx.text is the raw text representation.
  • .mx stays reserved for wrapper metadata; if payload data has an mx key, use @config.mx.data.mx.

Section selection - extract markdown heading blocks:

>> Single section (includes child headings)
var @overview = <guide.md # Overview>

>> Multi-section include list (comma-separated)
var @selected = <guide.md # "Quick Start", Other>

>> Include/exclude sets
var @filtered = <guide.md # "TL;DR", "Titled section", Other; !# tldr, "Another section title">

>> Optional include (no error if missing)
var @maybe = <guide.md # "Migration Notes"?, "Quick Start">

>> Fuzzy heading matching (case/punctuation-insensitive, prefix-based)
var @tldr = <guide.md # tldr>        >> matches heading "TL;DR"

Rules:

  • Include items are comma-separated after #.
  • Exclude items are comma-separated after ; !# (or ; #).
  • Use quotes for multi-word titles and punctuation-heavy titles.
  • Add trailing ? to an include selector to avoid errors when missing.
  • Missing non-optional include selectors throw an error.
  • Heading matching is fuzzy prefix-based and picks the first heading match.
  • Renaming with as is only supported when one section is selected.
  • Use ; !# to start excludes. <file.md # tl;dr !# later> is invalid.

Optional load - returns null for missing files, empty array for empty globs:

>> Missing file returns null
var @optional = <config.json>?
when @optional => show "Config loaded"

>> Empty glob returns []
var @matches = <*.nonexistent>?
show @matches.length                 >> 0

>> Existing file works normally
var @readme = <README.md>?
show @readme                         >> file content

See Also

AST Selection

AST Selection (extract code from files):

>> Exact names
var @handler = <src/api.ts { createUser }>

>> Wildcards
var @handlers = <api.ts { handle* }>         >> prefix match
var @validators = <api.ts { *Validator }>    >> suffix match

>> Type filters
var @funcs = <service.ts { *fn }>            >> all functions
var @classes = <service.ts { *class }>       >> all classes

>> Name listing (returns string arrays)
var @names = <api.ts { ?? }>                 >> all definition names
var @funcNames = <api.ts { fn?? }>           >> function names only

Supported: .js, .ts, .jsx, .tsx, .py, .go, .rs, .java, .rb
Type keywords: fn, var, class, interface, type, enum, struct

File Metadata

Metadata fields (via .mx):

var @file = <README.md>
show @file.mx.filename      >> "README.md"
show @file.mx.relative      >> relative path from cwd
show @file.mx.absolute      >> absolute path
show @file.mx.path          >> alias for absolute path
show @file.mx.dirname       >> parent directory name
show @file.mx.relativeDir   >> relative path to directory
show @file.mx.absoluteDir   >> absolute path to directory
show @file.mx.tokens        >> token count estimate
show @file.mx.fm.title      >> frontmatter field
show @file.mx.text          >> raw text representation
show @file.mx.fm            >> frontmatter object

For frontmatter metadata, use .mx.fm:

  • @file.mx.fm.title accesses the title frontmatter field.
  • Use .mx.text and .mx.fm when you need explicit control.
  • If frontmatter has an mx key, access it with @file.mx.fm.mx.

In loops - metadata works directly:

var @files = <docs/*.md>
for @file in @files => show @file.mx.filename
for @file in @files => show @file.mx.fm.status

JSON Parsing

Parse JSON explicitly with transforms:

>> When you have a JSON string and need to parse it
var @jsonStr = '[{"name":"Alice"},{"name":"Bob"}]'
var @parsed = @jsonStr | @parse
show @parsed.0.name                  >> "Alice"

>> When you have an object and need the JSON string
var @obj = {"name": "Alice"}
var @str = @obj | @parse
show @str                            >> '{"name":"Alice"}'

>> Common in pipelines with LLM responses
var @response = @llm("return JSON") | @parse.llm
var @items = @response

Frontmatter

YAML metadata block at the top of any mlld file:

---
name: my-module
author: alice
version: 1.0.0
about: Brief description
license: CC0
---

Access via @fm:

var @id = @fm.id
var @version = @fm.version

>> Build a metadata object
var @meta = {
  id: @fm.id,
  name: @fm.name,
  version: @fm.version
}

exe

exe vs var: exe defines functions (takes parameters). var creates values (no parameters). Use exe when you need to pass arguments; use var for computed values.

mlld validate warns when exe parameters use generic names such as result, output, or data because they can shadow caller variables. Prefer specific names such as status, finalOutput, or inputData.

Simple forms:

>> Command
exe @list(dir) = cmd {ls -la @dir | head -5}

>> JavaScript (in-process)
exe @add(a, b) = js { return a + b }

>> Node.js (VM-isolated)
exe @hash(text) = node {
  const crypto = require('crypto');
  return crypto.createHash('sha256').update(text).digest('hex');
}

>> Python
exe @add(a, b) = py { print(a + b) }
exe @calculate(x, y) = py {
result = x ** 2 + y ** 2
print(result)
}

>> Template
exe @greet(name) = `Hello @name!`

>> External template file
exe @welcome(name, role) = template "./prompts/welcome.att"

>> Prose (requires config)
exe @analyze(data) = prose:@config { session "Analyze @data" }

Python notes:

  • Use print() to return values (captured as string output)
  • Parameters preserve their original mlld types (int, float, bool, str, list, dict, None)
  • Standard library available: import json, import math, etc.
  • Cast only when you intentionally need conversion

Labels: Add labels before the name for runtime behavior. The llm label enables automatic checkpointing:

exe llm @review(file) = template "review.att"

Cached results persist across runs. Use --resume @review to selectively re-call. See mlld howto checkpoint.

See Also

  • Exe Blocks - More exe body styles including multi-step blocks.
  • Run Basics - run directive patterns and when to prefer run over exe.
  • Run Params - Parameter passing semantics for command and shell execution.

Script Return

=> @value terminates script execution immediately. When => appears inside a top-level if or when block, it terminates the script, not just the block.

Basic script return:

>> Strict mode
=> "final result"
show "unreachable"

Script return from conditionals:

if @condition [
  => "early exit"
]
=> "fallback"
var @mode = "fast"
when @mode == "fast" => [
  if @ready [
    => "immediate"
  ]
]
=> "deferred"

Module default export:

Imported .mld modules expose script return values through default:

>> module.mld
var @status = "active"
=> { code: 200, status: @status }
var @unreachable = "never runs"
>> main.mld
import { default as @result, status as @s } from "./module.mld"
show @result.code     >> 200
show @s               >> "active"

Scripts without => do not emit implicit final return output. Use => explicitly when module consumers need a default export value.

In exe blocks:

=> @value returns from the function body. See exe-blocks for function return syntax.

exe @validate(input) = [
  if !@input [
    => { error: "missing" }
  ]
  => { ok: @input }
]

Exe Metadata

Executables can include metadata for tooling and type safety.

Typed parameters:

exe @greet(name: string, times: number) = js { return "Hello " + name; }
exe @process(data: object, format: string) = js { return data; }
exe @count(items: array) = js { return items.length; }

Type annotations use : type syntax after the parameter name. Supported types: string, number, boolean, object, array.

Description metadata:

exe @greet(name: string) = js { return "Hello " + name; } with { description: "Greet a user by name" }

The with { description: "..." } clause adds a description that appears in MCP tool listings.

Combined example:

exe @searchIssues(repo: string, query: string, limit: number) = cmd {
  gh issue list -R @repo --search "@query" -L @limit --json number,title
} with { description: "Search GitHub issues by query" }

MCP integration:

When exported as tools, type annotations and descriptions generate JSON Schema for the MCP tool listing. See mcp-export for serving and mcp-tool-gateway for tool collections.

Parameter types vs runtime:

Type annotations are metadata for tooling. At runtime, parameters arrive as their actual types from the caller.

Exe Block Syntax

Block syntax (multi-statement bodies):

exe @process(data) = [
  let @validated = @validate(@data)
  let @transformed = @transform(@validated)
  => @transformed
]

>> With accumulation
exe @countItems(items) = [
  let @count = 0
  for @item in @items [
    let @count += 1
  ]
  => @count
]

Block rules:

  • Use [...] for multi-statement bodies
  • let @var = value for block-scoped variables
  • let @var += value for accumulation (arrays/strings/objects)
  • => value required as last statement for return

Exe When

When in exe (first match wins):

exe @classify(score) = when [
  @score >= 90 => "A"
  @score >= 80 => "B"
  * => "F"
]

Use if for imperative flow where multiple branches can run.

When when appears as a statement inside an exe block, matched value actions return from the enclosing exe.
Use block-form return to make that intent explicit:

exe @guard(x) = [
  when !@x => [
    => "missing"
  ]
  => "ok"
]

Exe Shadow Environments

Shadow environments expose helpers to all code blocks of that language:

>> JavaScript shadow environment
exe @double(n) = js { return n * 2 }
exe @cap(s) = js { return s[0].toUpperCase() + s.slice(1) }
exe js = { double, cap }  >> expose to all js blocks

var @out = js { cap("hello") + ": " + double(5) }  >> "Hello: 10"
>> Python shadow environment
exe @square(x) = py {
result = int(x) ** 2
print(result)
}
exe @greet(name) = py {
print(f"Hello, {name}!")
}
exe py = { square, greet }  >> expose to all py blocks

run py {
square(4)      >> prints 16
greet("World") >> prints Hello, World!
}

Cross-function calls: Shadow functions can call each other:

exe @add(a, b) = py { print(int(a) + int(b)) }
exe @multiply(x, y) = py { print(int(x) * int(y)) }
exe py = { add, multiply }

exe @calculate(n) = py {
sum_result = add(n, 10)   >> calls shadow function
product = multiply(n, 2)  >> calls shadow function
print(f"Sum: {sum_result}, Product: {product}")
}
exe py = { add, multiply, calculate }  >> update to include calculate

Supported languages: js, node, py/python

Exe Prose Syntax

Prose execution invokes LLM-interpreted DSL skills (OpenProse or custom):

import { @opus } from @mlld/prose

>> Inline (interpolates like templates)
exe @summarize(text) = prose:@opus { summarize @text }

>> File reference (.prose files do NOT interpolate)
exe @review(code) = prose:@opus "./review.prose"

>> Template files (.prose.att or .prose.mtt interpolate)
exe @greet(name) = prose:@opus "./greet.prose.att"

Interpolation rules:

  • prose:@config { inline } - interpolates @var like templates
  • "file.prose" - no interpolation, raw content
  • "file.prose.att" - ATT interpolation (@var)
  • "file.prose.mtt" - MTT interpolation ({{var}})

See mlld howto prose for setup, OpenProse syntax, and custom interpreters.

run

Decision tree:

  • Single line + pipes only → cmd { ... } (safe, recommended)
  • Needs &&, ||, control flow → sh { ... } (full shell)
  • JavaScript code → js { ... } (in-process) or node { ... } (VM-isolated)
  • Python code → py { ... } or python { ... } (subprocess)

Executable call statement sugar:

  • Strict mode (.mld): @task() executes with the same behavior as run @task()
  • Markdown mode (.md): /@task() executes with the same behavior as /run @task()
  • show @task() still displays output explicitly
>> cmd (pipes only, safe)
run cmd {echo Hello | tr '[:lower:]' '[:upper:]'}
var @date = cmd {date}

>> sh (full shell scripts)
run sh {
  npm test && npm run build || echo "Build failed"
}

>> JavaScript (in-process, fast)
run js {console.log("hello")}
var @result = js {return 42}

>> Node.js (VM-isolated, full API)
var @hash = node {
  const crypto = require('crypto');
  return crypto.createHash('md5').update('hello').digest('hex');
}

>> Python (subprocess)
run py {print("hello from python")}
var @sum = py {
result = 2 + 2
print(result)
}

Language comparison:

Language Isolation Use case
js None (in-process) Fast calculations, simple transforms
node VM context Full Node.js API, require()
py/python Subprocess Python libraries, data science

Run Command Working Directory

Working directory override:

run cmd:/ {pwd}                    >> runs in /
var @myPath = "/tmp"
run cmd:@myPath {pwd}              >> runs in /tmp
run cmd:~ {pwd}                    >> runs in home directory
run sh:/tmp {pwd}                  >> runs in /tmp
run js:/tmp {console.log(process.cwd())}

run cwd supports absolute paths, @var references, and ~ home expansion.

Run Command Stdin

Stdin support:

var @data = '[{"name":"Alice"}]'
run cmd { cat | jq '.[]' } with { stdin: @data }

>> Pipe sugar (equivalent)
run @data | { cat | jq '.[]' }

Run Parameter Syntax

Parameter syntax by language:

  • cmd: interpolate with @param
  • sh: use shell variables as $param
  • js/node: parameters as JavaScript variables
  • py/python: parameters as Python variables (typed values)
>> JavaScript - parameters are typed values
exe @process(items) = js { return items.map(x => x * 2) }
var @result = @process([1, 2, 3])  >> [2, 4, 6]

>> Python - parameters are typed values
exe @add(a, b) = py { print(a + b) }
var @sum = @add(5, 3)  >> 8

>> Python with complex types (metadata preserved)
exe @analyze(data) = py {
import json
print(json.dumps(data))
}

Python variable helpers (available as mlld global, no import needed):

  • mlld.is_variable(x) - check if value is a wrapped mlld Variable
  • Variables have __mlld_type__ and __mlld_metadata__ attributes
  • Arrays/lists and dicts pass through with metadata preserved

Builtins

Reserved Variables

  • @root / @base - project root path
  • @now - current ISO timestamp
  • @input - stdin/env (must be allowed in config)
  • @state - mutable state for SDK integrations
  • @debug - environment info
  • @fm - current file's frontmatter (in modules)
  • @mx - metadata accessor (labels, taint, guard context)

@root/@base resolution algorithm:

  1. Start at the current file directory and walk upward.
  2. Check mlld markers first (in order):
    • mlld-config.json
    • mlld-lock.json
    • mlld.lock.json
  3. If none are found, check fallback project markers:
    • package.json
    • .git
    • pyproject.toml
    • Cargo.toml
  4. The first directory containing any marker becomes @root (@base is an alias).

Built-in transformer/helper names are available for var/let declarations and are shadowable per scope:

  • @exists, @fileExists, @typeof
  • @parse, @json (deprecated alias), @xml, @csv, @md
  • @upper, @lower, @trim, @pretty, @sort
  • @keep, @keepStructured

mlld validate reports builtin shadowing as informational output.

Transformers

Used with | in pipelines. Each receives the previous value.

  • @parse - parse JSON (default mode)
  • @parse.strict - strict JSON only
  • @parse.loose - single quotes, trailing commas
  • @parse.llm - extract JSON from LLM response text
  • @xml - parse XML
  • @csv - parse CSV
  • @md - parse markdown
  • @upper - uppercase string
  • @lower - lowercase string
  • @trim - strip whitespace
  • @pretty - pretty-print JSON
  • @sort - sort array
var @users = cmd { cat users.json } | @parse
var @extracted = @llmResponse | @parse.llm
var @clean = @raw | @trim | @lower

Checks

  • @exists(target) - returns true/false. Works with file paths, variables, object fields, array indices, and globs. Note: @exists(@var) checks if the variable is defined, not if a file at that path exists.
  • @fileExists(path) - returns true/false. Always resolves argument to a path string, then checks filesystem. Unlike @exists(@var), @fileExists(@var) resolves the variable and checks the file.
  • @typeof(value) - returns type as string
  • @typeInfo(value) - returns rich type/provenance details for debugging
>> File existence (literal paths)
if @exists("config.json") [ show "config found" ]
if @exists(<src/**/*.ts>) [ show "has ts files" ]

>> Variable/field existence
if @exists(@obj.field) [ show "field defined" ]
if @exists(@arr[5]) [ show "index 5 exists" ]

>> File existence with dynamic paths
var @configPath = "config.json"
if @fileExists(@configPath) [ show "config found" ]
if @fileExists(@settings.configPath) [ show "found" ]

Type Checks

  • .isArray() / .isObject() / .isString()
  • .isNumber() / .isBoolean() / .isNull() / .isDefined()

Methods

String methods:

  • @str.length - string length
  • @str.includes(sub) - true if contains substring
  • @str.indexOf(sub) - index or -1
  • @str.startsWith(prefix) / endsWith(suffix)
  • @str.toLowerCase() / toUpperCase()
  • @str.trim() - remove whitespace
  • @str.split(separator) - split to array
  • @str.slice(start, end?) - extract substring by position
  • @str.substring(start, end?) - extract substring (no negative indices)
  • @str.replace(search, replacement) - replace first match (accepts string or regex)
  • @str.replaceAll(search, replacement) - replace all matches (accepts string or regex)
  • @str.replaceAll({"old": "new", ...}) - bulk replacement via object map (applied sequentially)
  • @str.match(pattern) - match against string or regex, returns match array or null
  • @str.padStart(length, char?) / padEnd(length, char?) - pad to target length
  • @str.repeat(count) - repeat string N times

Array methods:

  • @arr.length - array length
  • @arr.includes(value) - true if contains value
  • @arr.indexOf(value) - index or -1
  • @arr.join(separator) - join to string
  • @arr.slice(start, end?) - extract sub-array by position
  • @arr.concat(other) - combine arrays
  • @arr.reverse() - reverse order (returns new array)
  • @arr.sort() - sort alphabetically (returns new array)
  • @arr.flat(depth?) - flatten nested arrays (default depth 1)
  • @arr.at(index) - element at index (supports negative indices)

Helpers:

  • @keep(obj, [...keys]) - keep only specified keys from object
  • @keepStructured(obj, schema) - keep keys matching a schema structure

Method chains can continue across lines when continuation lines start with .

var @fruits = ["apple", "banana", "cherry"]
var @message = "Hello World"

show @fruits.includes("banana")    >> true
show @fruits.join(" and ")         >> "apple and banana and cherry"
show @message.toLowerCase()        >> "hello world"
show @message.split(" ")           >> ["Hello", "World"]

>> Method chaining
show @message.trim().toLowerCase().startsWith("hello")  >> true

>> Multiline method chaining
exe @normalize(text) = @text
  .trim()
  .toLowerCase()
  .replace("hello", "hi")

Comments

Use >> at start of line or << at end.

>> This is a comment
var @x = 5    << end-of-line comment
show @x       >> also works here

Escaping

Special characters:

Character In templates Solution
@ Variable interpolation \@ or helper function
` Template delimiter Use ::...:: syntax
:: Alternate delimiter Use triple-colon :::...:::
{{}} Object expression Use triple-colon escape

Escape hatch templates:

>> Triple-colon disables all @ interpolation — treats @ as literal text
var @xml = :::<USER id="@id">@name</USER>:::

>> Useful for XML/HTML with @ attributes
var @elem = :::<input type="email" placeholder="user@example.com">:::

When to use each:

  • Backticks `...` - Default template, simple interpolation
  • Double-colon ::...:: - When content has backticks
  • Triple-colon :::...::: - When content triggers file detection (has @ with dots/slashes)

See Also

Escaping @ Symbol

Use \@ or @@ to output a literal @. Works in all string and template contexts.

var @email = `user\@example.com`  >> "user@example.com"
var @email = `user@@example.com`  >> "user@example.com"

In .att template files:

Contact: user@@example.com
Follow @@username on Twitter

The exception is sh {} / bash {} blocks — content is passed to the shell verbatim with no @ processing.

Default Values

Problem: Need default values when data might be missing.

Solutions:

>> 1. Ternary expressions
var @name = @user.name ? @user.name : "Anonymous"

>> 2. JavaScript helper for safe access
exe @safe(obj, field, fallback) = js {
  return (obj && obj[field] !== undefined) ? obj[field] : (fallback || null)
}
var @title = @safe(@item, "title", "Untitled")

>> 3. Safe array access
exe @safeArr(obj, field) = js {
  return (obj && Array.isArray(obj[field])) ? obj[field] : []
}
var @tags = @safeArr(@post, "tags")

>> 4. Conditional inclusion with @var?
var @subtitle? = @item.subtitle      >> omit if falsy
show `Title: @item.title @subtitle?` >> subtitle only if present

Common patterns:

>> Optional chaining equivalent
exe @get(obj, path, fallback) = js {
  const keys = path.split('.');
  let val = obj;
  for (const k of keys) {
    if (val == null) return fallback;
    val = val[k];
  }
  return val ?? fallback;
}

var @city = @get(@user, "address.city", "Unknown")