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
{{<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".
| Namespace | Pattern | Source | Resolution time |
|---|---|---|---|
| User variables | {{my_var}} | dev.variables | Compile |
| 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: block | Compile |
| Prompt file | {{prompt.X}} → prompts/X.md | Bundle dir | Compile (file content inlined) |
| Skill file | {{skill.X}} → skills/X.md | Bundle dir | Compile (file content inlined) |
| Behavior profile | {{behavior.X}} → behavior/X.yaml | Bundle dir | Compile (parsed dict as JSON string) |
| Asset URL | {{asset.X}} → a stable asset URL for that app | Bundle dir | Compile (URL substitution) |
| Asset (base64) | {{asset_b64.X}} | Bundle dir | Compile (file inlined as base64) |
| Include | {{include:path/to/file}} | Bundle dir | Compile (file content inlined) |
| Runtime passthrough | Any other dotpath (event.X, caller.X, tool.X, ...) | Module-specific runtime | Run 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.
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:
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 thewhenfield 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 compositeall_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.
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
- Flow route expressions (
when:): Flows - Hook conditions (registered conditions + composite operators): Tool Hooks
- Channel activation pipeline (
prepare:): Channels (Bidirectional I/O) - Bundle namespaces deep dive (
{{prompt.X}},{{include:}}, frontmatter, hot reload): Bundle namespaces