Skip to main content
Plan steps, solver templates, and user-tool arguments all share one small, strict, logic-less template language. It exists to move data between steps — not to compute.

Variables

Roots are step ids (E0-style or descriptive names like fetch_project) and input; item, index, and accumulator join them inside iteration bodies, and length is a path operator — step ids may not shadow any of those five. Root-anchored paths are strict — a path that doesn’t resolve is a typed error, never a silent empty string (see below). Two scoping rules come from branching: a decide step’s result is referenced through its own id ({{E1.result.…}}, {{E1.branch}}) — branch-internal step ids are visible only to later steps inside the same branch — and the non-taken branch’s templates are never rendered at all.

Typed splice

A string that is exactly one variable tag resolves to the raw JSON value — numbers stay numbers, arrays stay arrays:
In mixed text, objects and arrays serialize as JSON.

Sections

Sections iterate arrays and gate on truthiness — used mostly in solver templates:
  • Inside a section, bare keys ({{title}}) read from the current item, falling back to enclosing scopes and roots.
  • Loop variables: {{@index}} (0-based), {{@first}}, {{@last}} — booleans usable as sections, e.g. the comma-separation idiom above.
  • Inverted sections render when the value is missing, false, or empty:
  • A bare key missing on a list item renders empty (items legitimately omit optional fields). A root-anchored section path that goes nowhere is falsy rather than an error — sections are conditionals.
  • Static validation applies the same scoping rule: a bare key inside a section body is an item read, never a root reference, so it is exempt from the “input or an earlier step” check. Dotted paths are root-anchored everywhere — a typo’d or forward root inside a section ({{E9.summary}} before E9 runs) is still caught at load time.

Error taxonomy

Variable resolution failures are typed, because different failures deserve different handling: How each is handled depends on who authored the plan — see Errors & replanning. The short version: EmptyData means “nothing found”, not “broken plan”.

Not supported, on purpose

No conditionals, functions, partials, comments, or inheritance. The language is deliberately logic-less: anything requiring computation belongs in a step (a user-defined tool is a fine place for it) or in the solver’s instructions. Unsupported syntax is a parse error at load time, not a silent no-op.