Skip to main content
For the curious. Your AI agent handles template variables automatically. This explains how data flows between nodes and what’s happening when you see ${variable} syntax in workflows or traces.
Template variables let nodes pass data to each other without writing glue code. When you see ${variable} syntax in a workflow, it’s pulling data from previous nodes, workflow inputs, or nested structures.

Basic syntax

The ${variable} syntax accesses values from the shared store:
Here, ${read.content} pulls the content output from the read node.

Nested access

Template variables can traverse deeply nested structures:
This traverses:
  1. api node’s output
  2. response key
  3. items array
  4. First element ([0])
  5. name field

Type preservation

Template variables preserve the original data type when used alone. When combined with text, they become strings.
Simple templates (just ${var}) preserve type. Complex templates (any surrounding text) become strings.

Inline objects

This type preservation makes inline object construction intuitive:
If settings is {"timeout": 30} and results is {"status": "ok"}, the resolved stdin is:
Without type preservation, both would be stringified JSON requiring manual parsing.

JSON auto-parsing

When a template accesses nested fields on a JSON string, pflow automatically parses it:
Even though fetch.stdout is a string containing JSON, the nested access ${fetch.stdout.results[0].name} works because pflow:
  1. Sees you’re trying to access .results
  2. Attempts to parse stdout as JSON
  3. Traverses the parsed structure
  4. Returns the value at results[0].name
This means shell commands that output JSON work directly with template variables — no manual json.loads() needed.
A reference rooted in a workflow input declared with a type other than any is never re-parsed. ${config.field} reads a - type: object input directly, but JSON text held in a - type: string input stays text — ${text.field} on it is a validation error (declare the input object instead).

Workflow inputs

Template variables also reference workflow inputs declared in the workflow definition:
When running this workflow, inputs are provided via CLI arguments:
Each value is read by its input’s declared type — see Parameter syntax.

Stdin input

Inputs can receive piped data by adding stdin: true. See Stdin input for details.

Array notation

Array elements are accessed using bracket notation:
The index can itself be a reference, such as ${results[${__index__}].title} in a batch. The whole expression is one reference: it keeps its value’s type, and if the index does not resolve to an in-range whole number the reference is unresolved (a ?? fallback then applies).

Batch processing

In batch nodes, a special template variable (${item} by default) represents the current item:
The as: "file" creates ${file} as the item variable. See Batch processing for details.

Coalesce operator

The ?? operator returns the first operand that resolves, falling through whenever the left side isn’t there — whether the node didn’t run (a branch that wasn’t taken) or the referenced field is absent on a node that did run:
This is particularly useful with conditional branching where only one path executes. Without coalesce, referencing a node that didn’t run would be an unresolved variable error. Operands can also be JSON literals, used as a final default when nothing else resolves:
Literal operands accept numbers, double-quoted strings, true, false, null, and empty []/{} (a literal string cannot contain the ?? sequence itself). A bare reference with no ?? fallback — ${node.field} — still raises an unresolved-variable error when the field is missing, so genuine typos are caught; add a fallback only where absence is expected.

Node metadata

Beyond their primary outputs, some nodes expose metadata that downstream nodes can access through templates. This is useful for cost-aware workflows, debugging, or building on execution details.

LLM token usage

Both llm and agent nodes write llm_usage with token counts:

Shell command

A shell node’s command is the text the workflow wrote — pflow never fills in template variables there. Values reach it through env: (see Shell). When a shell node fails, the error output shows the command and its bound env: values, and pflow report shows both for every shell node.

HTTP response details

The http node exposes response_headers (dict) and response_time (seconds as float) alongside the response body. Useful for handling pagination links, rate limit headers, or performance monitoring.
Per-node cost is available internally as llm_usage.cost_usd (estimated from token pricing, null for unknown models). Access it via a code node’s inputs dict — it’s not exposed as a template variable because pricing coverage varies by model. Aggregate cost across all nodes appears in the CLI JSON output after the workflow finishes.

Escaping

Literal ${...} text (not a template variable) uses double dollar signs to escape:
This produces the literal string Price: ${PRICE} instead of trying to resolve a variable. An escape runs through its matching }, and anything inside it is literal, including braces nested one level deep: $${NAME:-${default}} produces ${NAME:-${default}} without resolving ${default}. So shell parameter expansion ($${NAME:-world}, $${#ITEMS}) works the same way inside a templated parameter. Text after the closing } is templated as usual. Only $${ is an escape: a bare $$ is passed through unchanged. A shell command and a code node’s code block are not templated, so they need no escape: ${NAME:-world} there is already the shell’s own. $${ in either is a validation error.

JavaScript passed to an MCP tool

In a code parameter of an MCP tool (such as evaluate_script’s function), ${...} is JavaScript and $${ is sent as written. A workflow value goes in as ${x|json}, which inserts it as a JavaScript value — a quoted, escaped string, true, a number, an object literal. |json is accepted only there. See MCP: code passed to a tool.

Validation

pflow validates template variables at workflow creation time:
  • Unknown variables → Error: “Unresolved variable: ${typo}”
  • Type mismatches → Warning: “Expected string, got dict”
  • Invalid syntax → Error: “Invalid template: ${foo.}”
This works because node types declare their outputs — pflow knows at creation time what fields exist and what types they have. With arbitrary code, you’d discover these mismatches at runtime.
Most validation happens when the workflow is created (compile-time). Some validations happen during execution (runtime):
  • Compile-time: Variable existence, type compatibility, syntax
  • Runtime: JSON parsing success, nested access on dynamic values
If JSON auto-parsing fails at runtime, you’ll see an “Unresolved variable” error.