Template Expressions
This document defines where template expressions are evaluated, which context variables are allowed, and how determinism is enforced across phases.
Who this is for
- Workflow authors writing
{{ ... }}expressions. - Component authors consuming evaluated inputs.
What you'll get
- The supported expression syntax and contexts.
- Determinism rules for batch vs streaming.
- Common patterns and pitfalls.
Quick start
- Always wrap expressions in
{{ ... }}. Plain strings are treated as literals. - Use
inputs.*,steps.*, andpacket.*(streaming only) as your root contexts. - For step names with dashes, use
steps['my-step']orindex steps "my-step".
Expression Forms
There are two supported forms:
- Template strings (Go templates + Sprig):
{
"path": "{{ inputs.filePath }}",
"flag": "{{ eq steps.validate.output.ok true }}"
}
- Raw template expressions (for explicit execution by an Engram / SDK):
{
"$bubuTemplate": "steps.fetch.output.status | int | gt 200",
"$bubuTemplateVars": {
"threshold": 200
}
}
Notes:
- Pipes are supported (
|) via Go templates + Sprig. - Both
{{ inputs.foo }}and{{ .inputs.foo }}are accepted. $bubuTemplateVars(optional) merges into the evaluation context for that one expression.
Template Engine
All templating uses Go text/template with the Sprig function library. This means:
- Pipes work as in Sprig/Go templates.
- Missing keys are errors (
missingkey=error). - Root names are normalized, so
inputs.fooand.inputs.fooare accepted. - Plain strings are not evaluated; use
{{ ... }}to execute expressions. - For step names with dashes, use
steps['name']in simple path templates orindex steps "name"in complex expressions.
Sprig functions are available with a few restrictions:
- Disabled for safety:
env,expandenv,getHostByName. - Random helpers and
noware allowed only in runtime evaluation, not in deterministic inputs-only evaluation (see Determinism).
Custom Helper Functions
Custom helpers are always available in addition to Sprig:
len→ Length of strings, arrays, slices, or maps. If the value is an offloaded storage ref, evaluation is blocked for materialization.hash_of→ SHA256 of a string or byte array. Offloaded refs are blocked for materialization.type_of→ Returnsstring|number|bool|object|array|null, oroffloaded(<ref>)for storage refs.exists→trueif a value is non-nil.sample→ Returns the value (useful for probing); offloaded refs are blocked for materialization.
Context Variables
inputs.*: Story input payload (from StoryRun spec.inputs).steps.*: Step outputs (batch) or upstream packet outputs (streaming hub). In streaming, the hub flattens outputs sosteps.my-stepis the output object (no.outputswrapper).packet.*: Per-packet metadata (streaming only).now: Current timestamp (runtime only; disallowed in deterministic inputs-only evaluation).rand*,uuidv4: Random helpers (runtime only; disallowed in deterministic inputs-only evaluation).
Unsupported aliases (rejected by validation):
trigger.*-> useinputs.*upstream.*-> usesteps.*
Evaluation Matrix
Batch stories (spec.pattern: batch)
| Field | When evaluated | Allowed contexts |
|---|---|---|
steps[*].if | runtime (DAG controller) | inputs, steps, now, random helpers |
steps[*].with | runtime (StepRun creation) | inputs, steps, now, random helpers |
spec.output | runtime (StoryRun finalize) | inputs, steps, now, random helpers |
steps[*].runtime | not supported | - |
Streaming stories (spec.pattern: streaming)
| Field | When evaluated | Allowed contexts |
|---|---|---|
steps[*].with | runtime (hub per-packet; deterministic inputs only) | inputs |
steps[*].runtime | runtime (hub per-packet) | inputs, steps, packet, now, random helpers |
steps[*].if | runtime (hub per-packet) | inputs, steps, packet, now, random helpers |
spec.output | runtime (StoryRun finalize, if applicable) | inputs, steps, now, random helpers |
Common patterns
Step if
if: "{{ gt (len steps['fetch'].output.items) 0 }}"
Wait until
with:
until: "{{ inputs.ready }}"
Streaming runtime config
runtime:
threshold: "{{ steps.transcribe.confidence }}"
Determinism Rules
Deterministic evaluation forbids now and random helpers to keep results replayable.
This applies to inputs-only contexts (for example streaming steps[*].with). Runtime
evaluation can use now/random helpers as needed.
Template values are resolved at evaluation time and stored in the resulting StepRun/packet; they remain stable across reconciliation once computed.
Offloaded data and materialization
Templates may reference step outputs that are offloaded to storage (objects that contain
$bubuStorageRef). Direct evaluation of those values is blocked by default.
Behavior is controlled by templating.offloaded-data-policy:
error(default): reject evaluation with a clear error.inject: create a "materialize" StepRun/engram to hydrate data and re‑evaluate the template, then continue execution when the result is ready.
When inject is enabled, the operator uses templating.materialize-engram (commonly set to
materialize) to resolve the template in the same StoryRun namespace.
Templating Configuration
Operator-level limits and behavior are controlled by:
templating.evaluation-timeout(example:30s)templating.max-expression-length(example:1000)templating.max-output-bytes(example:65536)templating.deterministic(true|false)templating.offloaded-data-policy(error|inject)templating.materialize-engram(engram name)
Validation
The Story webhook enforces the matrix above at admission time. Invalid templates are rejected with clear field-level errors.