> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sirenspec.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Template Interpolation

> Embed dynamic values in workflow YAML with {{ expr }} syntax.

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

```yaml theme={null}
prompt: "Summarize this: {{ inputs.message }}"
```

### 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** (`""`):

```yaml theme={null}
prompt: "Context: {{ working.context | default('none provided') }}"
```

### 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:

````yaml theme={null}
# upstream agent may emit "", prose, or ```json ... ``` — fall back to an empty list
for_each: "{{ plan.output | json_or_default('[]') }}"
````

### Multiple placeholders per string

```yaml theme={null}
prompt: "Analyze {{ node_a.output }} against {{ node_b.output }}"
```

***

## Namespaces

### `inputs.*` — Workflow input fields

Resolves against the workflow's top-level input.

```yaml theme={null}
prompt: "Analyze: {{ inputs.message }}"
```

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.

```yaml theme={null}
system: "You are operating in the {{ env.APP_ENVIRONMENT }} environment."
```

> **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:

```yaml theme={null}
system: "Mode: {{ env.DEBUG_MODE | default('production') }}"
```

***

### `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:

```yaml theme={null}
prompt: "Summarize: {{ classifier.output }}"
```

For workflow nodes, the output is the sub-workflow's output dict. Access sub-node outputs via:

```yaml theme={null}
prompt: "Sub-workflow sentiment: {{ run_child.output.sentiment }}"
```

<Warning>
  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:`.
</Warning>

***

### `node_id.*` — Arbitrary node context access

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

```yaml theme={null}
# If the 'classify' node writes: working.classify.output.sentiment
prompt: "Sentiment was {{ classify.output.sentiment }}"
```

***

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

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

```yaml theme={null}
synthesis:
  prompt: |
    Sentiment: {{ analyze.agents.sentiment.output }}
    Risk:      {{ analyze.agents.risk.output }}
    Synthesize an investment recommendation.
```

***

### `item` — Loop iteration value (factory nodes)

Inside a `factory` node's `inputs:` values, `{{ item }}` refers to the current list element:

```yaml theme={null}
nodes:
  execute:
    type: factory
    for_each: "{{ plan.output }}"
    inputs:
      task: "{{ item }}"
```

Using `{{ item }}` outside a factory loop raises `InterpolationError`.

***

### `index` — Loop iteration index (factory nodes)

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

```yaml theme={null}
inputs:
  task: "{{ item }}"
  position: "{{ index }}"
```

***

### `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:

```yaml theme={null}
inputs:
  position: "{{ index }} of {{ total }}"

swrm:
  agents:
    - id: grader
      prompt: "Grade paper {{ index }} of {{ total }}: {{ item }}"
  synthesis:
    prompt: "Final report for item {{ index }}/{{ total }}"
```

***

## Error Behaviour

Any unresolvable expression raises `InterpolationError` with three attributes:

| Attribute    | Description                                                  |
| ------------ | ------------------------------------------------------------ |
| `expression` | The full `{{ expr }}` string (without braces)                |
| `namespace`  | The first segment of the path (e.g. `inputs`, `env`, `plan`) |
| `reason`     | Human-readable description of what went wrong                |

Example:

```
InterpolationError in '{{ plan.output }}' [plan]: Key 'output' not found
```

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

```yaml theme={null}
nodes:
  classify:
    agent: classifier_agent
    writes: working.classify.output

  reply:
    agent: reply_agent   # system: "The intent was: {{ classify.output }}"
    writes: output.reply
```

### SWRM synthesis referencing sub-agent outputs

```yaml theme={null}
nodes:
  analyze:
    type: swrm
    agents:
      - id: sentiment
        provider: openai
        model: gpt-4o-mini
        prompt: "Analyze sentiment: {{ inputs.message }}"
      - id: risk
        provider: anthropic
        model: claude-haiku-4-5-20251001
        prompt: "Identify risks: {{ inputs.message }}"
    synthesis:
      provider: anthropic
      model: claude-haiku-4-5-20251001
      prompt: |
        Sentiment: {{ analyze.agents.sentiment.output }}
        Risk:      {{ analyze.agents.risk.output }}
        Write a recommendation.
```

### Factory node with loop variables

```yaml theme={null}
nodes:
  plan:
    agent: planner
    writes: working.plan.output     # outputs JSON list: ["task A", "task B"]

  execute:
    type: factory
    agent: worker
    for_each: "{{ plan.output }}"
    inputs:
      task: "{{ item }}"
      position: "{{ index }} of {{ total }}"
    concurrency: 4
    writes: working.execute.outputs

  aggregate:
    agent: summarizer               # system: "Summarize: {{ execute.output }}"
    writes: output.report
```

### 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:

```yaml theme={null}
agents:
  analyzer:
    model: "openai:gpt-4o-mini"
    system: |
      You are operating in {{ env.APP_ENVIRONMENT | default('production') }} mode.
      Be concise and focus on actionable findings.
```

> **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:

```yaml theme={null}
nodes:
  fetch_data:
    type: tool
    tool: http
    config:
      url: "https://api.example.com/data"
      headers:
        Authorization: "Bearer {{ env.SERVICE_TOKEN }}"
```
