Skip to main content
SirenSpec uses {{ expr }} syntax to embed dynamic values in workflow YAML. Expressions are resolved at node execution time — values from upstream nodes are available once those nodes complete.

Syntax

Basic placeholder

Default filter

Use | default('value') to supply a fallback. It fires both when the key is missing and when the resolved value is an empty string (""):

JSON-or-default filter

Use | json_or_default('value') when the value should parse as JSON but might not. It returns the fallback when the key is missing, when the value is an empty string, or when the value cannot be parsed as JSON. This covers LLM outputs that return "" or fenced markdown instead of a clean JSON array:

Multiple placeholders per string


Namespaces

inputs.* — Workflow input fields

Resolves against the workflow’s top-level input.
The message field is always present. Additional fields defined in input: are also available.

env.* — Environment variables

Reads os.environ at node execution time, not at parse time.
Security note: env.* values are redacted (***) in execution traces. Use env vars for non-sensitive runtime configuration — feature flags, environment names, region identifiers. Never inject secrets or API keys into system: or prompt: fields; use HTTP headers: in tool nodes instead.
Unset variables raise InterpolationError. Use a default if the variable is optional:

node_id.output — Canonical node output

Every node (agent, swrm, factory, workflow) writes its primary output to working.{node_id}.output automatically. Reference it in downstream nodes:
For workflow nodes, the output is the sub-workflow’s output dict. Access sub-node outputs via:
Reference upstream nodes by their ID ({{ classifier.output }}), never via the internal working namespace. {{ working.<node_id>.* }} does not resolve at runtime and the load-time linter rejects it with the working_dot_node_id error, pointing you to the canonical {{ <node_id>.output }} form. Reserve working.* for custom paths you write to or seed in state:.

node_id.* — Arbitrary node context access

Any value written to the working dict is accessible by its path:

node_id.agents.agent_id.output — SWRM sub-agent output

Inside a swrm synthesis prompt, each sub-agent’s output is available:

item — Loop iteration value (factory nodes)

Inside a factory node’s inputs: values, {{ item }} refers to the current list element:
Using {{ item }} outside a factory loop raises InterpolationError.

index — Loop iteration index (factory nodes)

Zero-based position of the current item in the list:

total — Total item count (factory nodes)

The total number of items in the current factory loop. Available in agent prompts, swrm agent prompts, synthesis prompts, and factory inputs: templates:

Error Behaviour

Any unresolvable expression raises InterpolationError with three attributes: Example:
Use | default('fallback') to suppress the error and return a static value instead.

Circular Reference Detection

At workflow load time, SirenSpec scans all prompt templates for {{ node_id.* }} references and builds a dependency graph. If node A’s prompt references node B and node B’s prompt references node A, loading raises InterpolationError with namespace="circular_ref". This check runs automatically in load_workflow() — no configuration needed.

Security

  • env.* values are never written to execution traces. The trace shows *** instead.
  • when: edge conditions use a separate, restricted eval() namespace — not the interpolation engine.
  • YAML is loaded with SafeLoader, so no code can be injected via YAML tags.

Examples

Sequential pipeline with node output chaining

SWRM synthesis referencing sub-agent outputs

Factory node with loop variables

Environment variables for runtime configuration

Use env.* to inject non-sensitive configuration into prompts — environment names, feature flags, locale identifiers, and similar non-secret values:
Credentials and API keys must never appear in system: or prompt: fields. Prompt content can surface in logs, traces, and model context windows. Pass secrets via HTTP headers: in tool nodes instead: