Skip to main content

Expressions

Digitorn's template syntax for dynamic values in YAML. Every {{...}} placeholder is resolved by the compiler before the app runs. Template syntax is intentionally minimal - just namespaces, a fallback operator, and quoted literals. No filters, no comparison operators, no logic - anything shaped like that passes through unresolved to the consuming module instead (see below).

For complex routing logic (in flow: route conditions, hook conditions, channel activation rules), each subsystem evaluates its own expressions at runtime - see the cross-references at the end.

Syntax​

text
{{<expression>}}

Whitespace inside the braces is trimmed. The expression itself is one of:

  • A plain name (matches a key in dev.variables).
  • A namespaced reference (env.X, secret.X, sys.X, app.X, prompt.X, skill.X, behavior.X, asset.X, asset_b64.X, include:path).
  • A quoted literal ('foo' or "foo").
  • A fallback expression (<expr> ?? <fallback>).
  • Anything else - passed through unchanged for runtime resolution by the consuming module (channels, hooks, flow expressions).

Namespaces​

Resolution time = compile time unless noted "runtime passthrough".

NamespacePatternSourceResolution time
User variables{{my_var}}dev.variablesCompile
Environment{{env.VAR_NAME}}os.environ. Raises a compile error when unset. Use ?? for optional.Compile
Secret{{secret.VAR}}Encrypted DB first, os.environ fallback.Compile
System{{sys.X}}The machine that compiles the app (digitorn lint / install, or CI) - not the deployed session. timestamp, date, time, hostname, platform/os, arch, cwd, user, pid, home, tmpdir/temp_dir, shell, is_windows, is_linux, is_macos, path_sep, locale, shell_family. An unrecognized key passes through unresolved ({{sys.whatever}}) instead of erroring.Compile
App{{app.id}}, {{app.name}}, {{app.version}}, {{app.author}}, {{app.description}}app: blockCompile
Prompt file{{prompt.X}} → prompts/X.mdBundle dirCompile (file content inlined)
Skill file{{skill.X}} → skills/X.mdBundle dirCompile (file content inlined)
Behavior profile{{behavior.X}} → behavior/X.yamlBundle dirCompile (parsed dict as JSON string)
Asset URL{{asset.X}} → a stable asset URL for that appBundle dirCompile (URL substitution)
Asset (base64){{asset_b64.X}}Bundle dirCompile (file inlined as base64)
Include{{include:path/to/file}}Bundle dirCompile (file content inlined)
Runtime passthroughAny other dotpath (event.X, caller.X, tool.X, ...)Module-specific runtimeRun time

Fallback operator ??​

Returns the right side if the left side fails to resolve. Strict semantics - env.X returning a passthrough template (because the variable isn't set) is treated as failure and falls through.

yaml
dev:
variables:
timeout: "{{env.TIMEOUT ?? '30'}}" # fallback to '30'
region: "{{env.AWS_REGION ?? 'eu-west-1'}}" # default region
api_token: "{{secret.API_TOKEN ?? env.API_TOKEN ?? 'dev-token'}}"
# 3-level fallback chain

The right side is a full expression - it can chain to another namespace, another fallback, or a quoted literal.

Quoted string literals​

A single- or double-quoted string is returned verbatim:

yaml
dev:
variables:
greeting: "{{ 'Hello, world' }}" # literal string
fallback: "{{ env.X ?? 'unset' }}" # quoted fallback value

Useful as the right side of ?? or as a guarded literal in a context where the templating layer would otherwise interpret the value.

Comparisons and boolean logic (equality, &&/||, and similar) live at a different layer than {{...}} templates: flow: route conditions and hook conditions use their own runtime expression engine - see the cross-references below. Format dates at the source instead of formatting inside the template ({{sys.date}} is already pre-formatted).

Templating contexts​

Where Digitorn applies resolve_variables:

  • Anywhere in the YAML - every string value across the declared top-level blocks is rendered through the resolver before validation. Includes agents[].system_prompt, tools.modules.*.config.*, tools.modules.channels.config.providers.*.activation.message, and so on.

The resolver does NOT walk into binary or non-string fields (a numeric runtime.max_turns won't accept "{{env.X}}" unless the variable resolves to a valid integer literal at compile time).

Runtime expression engines (separate from templates)​

A few subsystems evaluate expressions (not just template substitution) at runtime, using their own engines:

  • Flow routes (flow: block, routes[].when) - boolean expressions over the flow context (category == 'refund', approvals.gate == 'approve', default). Documented in Flows. The schema only checks the when field is a non-empty string - the syntax is validated when the route fires.
  • Hook conditions (runtime.hooks[].condition) - declarative condition tree (context_pressure, tool_calls, tool_failed, content_contains, error_type, expression, plus composite all_of/any_of/not). Documented in Tool Hooks.
  • Channel activation prepare: steps - module action results are bound to a name (as: caller) and become available as {{caller.X}} in subsequent activations of the same channel. Documented in Channels.

These engines accept richer syntax than the template resolver - boolean operators, dotted paths, comparisons. But they are separate from {{...}} template substitution. Don't expect filters or comparisons to work inside a {{...}} placeholder.

Compile-time resolution order​

recursively resolves until no {{...}} remains or the maximum depth (10) is reached. A self-referencing variable produces a cycle error.

yaml
dev:
variables:
base: "{{env.BASE_URL}}"
api: "{{base}}/v1/api" # resolves to <BASE>/v1/api
full: "{{api}}/users" # resolves to <BASE>/v1/api/users
# works via recursion

Cross-references​