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

Summarize the content from the read node.

- type: llm
- prompt: ${read.content}
Here, ${read.content} pulls the content output from the read node.

Nested access

Template variables can traverse deeply nested structures:
### extract

Extract the name of the first item from the API response.

- type: llm
- prompt: ${api.response.items[0].name}
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.
TemplateOriginal valueResultType
"${count}"42 (int)42int
"Count: ${count}"42 (int)"Count: 42"string
"${config}"{"key": "val"}{"key": "val"}dict
"Prefix ${config}"{"key": "val"}"Prefix {\"key\": \"val\"}"string
Simple templates (just ${var}) preserve type. Complex templates (any surrounding text) become strings.

Inline objects

This type preservation makes inline object construction intuitive:
### process

Process settings and results together.

- type: shell
```yaml stdin
config: ${settings}
data: ${results}
```
If settings is {"timeout": 30} and results is {"status": "ok"}, the resolved stdin is:
{
  "config": {"timeout": 30},
  "data": {"status": "ok"}
}
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:
## Steps

### fetch

Fetch data from the API.

- type: shell

```shell command
curl https://api.example.com/data
```

### extract

Analyze the first result name from the fetched data.

- type: llm
- prompt: Analyze: ${fetch.stdout.results[0].name}
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.

Workflow inputs

Template variables also reference workflow inputs declared in the workflow definition:
## Inputs

### api_key

API key for authenticating with the service.

- type: string

### endpoint

URL of the API endpoint to call.

- type: string

## Steps

### call_api

Call the API endpoint with authentication.

- type: http
- url: ${endpoint}
```yaml headers
Authorization: Bearer ${api_key}
```
When running this workflow, inputs are provided via CLI arguments:
pflow my-workflow api_key="sk-..." endpoint="https://api.example.com"

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

Extract specific items from the results.

- type: llm
- prompt: First: ${results[0]}, Tag: ${data.items[2].tags[1]}

Batch processing

In batch nodes, a special template variable (${item} by default) represents the current item:
### process

Summarize each file.

- type: llm
- prompt: Summarize: ${file}
- batch:
    items: ${files}
    as: file
The as: "file" creates ${file} as the item variable. See Batch processing for details.

Escaping

Literal ${...} text (not a template variable) uses double dollar signs to escape:
### print_price

Print the literal price variable.

- type: shell

```shell command
echo 'Price: $${PRICE}'
```
This produces the literal string Price: ${PRICE} instead of trying to resolve a variable.

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.