AWF uses Go template syntax ({{.var}}) for variable interpolation in workflow definitions.

Syntax

Variables are enclosed in double curly braces with a dot prefix:

command: echo "Hello, {{.inputs.name}}!"

Variable Categories

Input Variables

Access workflow input values:

{{.inputs.variable_name}}

Example:

inputs:
  - name: file_path
    type: string
  - name: max_tokens
    type: integer

states:
  initial: process
  process:
    type: step
    command: |
      process-file "{{.inputs.file_path}}" --tokens={{.inputs.max_tokens}}

State Variables

Access output, exit code, and token usage from previous steps:

{{.states.step_name.Output}}            # Command output (raw text, or cleaned if output_format set)
{{.states.step_name.ExitCode}}          # Exit code (0 for success, non-zero for failure)
{{.states.step_name.TokensUsed}}        # Total tokens consumed by agent steps
{{.states.step_name.TokensInput}}       # Input tokens (prompt + context)
{{.states.step_name.TokensOutput}}      # Output tokens (assistant response)
{{.states.step_name.TokensEstimated}}   # true if token counts are estimates, false if from provider
{{.states.step_name.Response.field}}    # Parsed field from operation/agent structured output (heuristic)
{{.states.step_name.JSON.field}}        # Parsed field from output_format: json (explicit)

Output

The standard output (stdout) from the executed step:

analyze:
  type: agent
  provider: claude
  prompt: "Analyze: {{.states.read_file.Output}}"

ExitCode

The exit code from the step’s command execution. Use in transitions, expressions, and templates:

In transitions (numeric comparison):

transitions:
  - when: "states.test_run.ExitCode == 0"
    goto: success
  - when: "states.test_run.ExitCode > 0"
    goto: failure

In templates (as string):

report_failure:
  type: step
  command: |
    echo "Test failed with exit code: {{.states.test_run.ExitCode}}"
    notify-on-error --code={{.states.test_run.ExitCode}}

Numeric range expressions:

transitions:
  - when: "states.build.ExitCode >= 100 and states.build.ExitCode < 128"
    goto: handle_user_error
  - when: "states.build.ExitCode >= 128"
    goto: handle_signal

On POSIX systems, exit codes are typically 0–255. Exit code 0 indicates success; non-zero indicates failure.

Transition priority: Transitions are evaluated on both success and failure paths. When a matching transition is found, it takes priority over on_success, on_failure, and continue_on_error. If no transition matches, the legacy routing applies as fallback.

TokensUsed

Total tokens consumed by agent steps. Available for all agent step types:

run_agent:
  type: agent
  provider: claude
  prompt: "Process this"
  on_success: log_tokens

log_tokens:
  type: step
  command: |
    echo "Tokens used: {{.states.run_agent.TokensUsed}}"

Use in conditional expressions for token budgeting:

transitions:
  - when: "states.agent_step.TokensUsed > inputs.token_limit"
    goto: token_exceeded

TokensInput / TokensOutput

Separate input and output token counts. Available for all agent step types:

log_details:
  type: step
  command: |
    echo "Input: {{.states.analyze.TokensInput}}, Output: {{.states.analyze.TokensOutput}}"

In conversation mode (continue_from), TokensInput includes all prior turns. In single-turn mode, TokensInput is 0 and TokensOutput equals TokensUsed.

TokensEstimated

Boolean indicating whether token counts are exact (from the provider’s JSON output) or estimated (len/4 approximation). Use to decide whether to trust the values for billing or budgeting:

transitions:
  - when: "states.analyze.TokensEstimated == false and states.analyze.TokensUsed > inputs.budget"
    goto: over_budget
ProviderTokensEstimatedSource
Claudefalseresult event usage field
Geminifalseresult event stats field
Codexfalseturn.completed event usage field
Copilotfalse (output only)assistant.message event outputTokens
OpenCodefalsestep_finish event part.tokens field
OpenAI-CompatiblefalseAPI response usage field
Any provider (fallback)truelen(output) / 4 estimation

Note: TokensUsed replaced deprecated states.step_name.Tokens field. If migrating from earlier versions, update workflow YAML expressions from {{.states.step_name.Tokens}} to {{.states.step_name.TokensUsed}}.

Response (Operation Outputs)

Operation steps (e.g., github.get_issue, http.request) return structured data accessible via Response:

{{.states.step_name.Response.title}}       # Parsed field from operation result
{{.states.step_name.Response.number}}      # Numeric field
{{.states.step_name.Response.labels}}      # Array field

Use Output for raw JSON, Response.field for parsed fields:

states:
  initial: get_issue

  get_issue:
    type: operation
    operation: github.get_issue
    inputs:
      number: "{{.inputs.issue_number}}"
    on_success: show_title
    on_failure: error

  show_title:
    type: step
    command: echo "Issue: {{.states.get_issue.Response.title}}"
    on_success: done
    on_failure: error

  done:
    type: terminal
  error:
    type: terminal
    status: failure

HTTP operation outputs follow the same pattern:

{{.states.step_name.Response.status_code}}       # HTTP status (200, 404, etc.)
{{.states.step_name.Response.body}}              # Response body (truncated at 1MB)
{{.states.step_name.Response.headers.Content-Type}}  # Response header value
{{.states.step_name.Response.body_truncated}}    # true if body was truncated

See Workflow Syntax - Operation State for the full list of available operations and their output fields.

JSON (Explicit Output Formatting)

When an agent step uses output_format: json, the parsed JSON is accessible via JSON:

{{.states.step_name.JSON.field}}         # Access a JSON object field
{{.states.step_name.JSON}}               # Full parsed JSON object

Key differences from Response:

  • JSON is only populated when output_format: json is explicitly set on the agent step
  • Response is populated automatically for all agent outputs if valid JSON is detected (heuristic)
  • JSON represents explicitly formatted output; Response is automatic best-effort parsing

Example with output_format: json:

states:
  initial: analyze

  analyze:
    type: agent
    provider: claude
    prompt: "Return JSON analysis with 'issues' and 'severity' fields"
    output_format: json
    on_success: process
    on_failure: error

  process:
    type: step
    command: |
      echo "Severity: {{.states.analyze.JSON.severity}}"
      echo "Issues: {{.states.analyze.JSON.issues}}"
    on_success: done

  done:
    type: terminal
  error:
    type: terminal
    status: failure

If agent returns:

{"issues": ["buffer overflow", "memory leak"], "severity": "high"}

Then:

  • {{.states.analyze.JSON.severity}} = "high"
  • {{.states.analyze.JSON.issues}} = ["buffer overflow", "memory leak"]

See Agent Steps - Output Formatting for detailed examples and best practices.

Workflow Metadata

Access workflow execution information:

{{.workflow.id}}
{{.workflow.name}}
{{.workflow.duration}}

Example:

log_result:
  type: step
  command: |
    echo "Workflow {{.workflow.name}} ({{.workflow.id}}) completed"

Environment Variables

Access system environment variables:

{{.env.VARIABLE_NAME}}

Example:

deploy:
  type: step
  command: |
    deploy.sh --env={{.env.DEPLOY_ENV}} --token={{.env.API_TOKEN}}

AWF Directory Context

Access system directories configured per XDG standards:

{{.awf.config_dir}}      # ~/.config/awf (or $XDG_CONFIG_HOME/awf)
{{.awf.data_dir}}        # ~/.local/share/awf (or $XDG_DATA_HOME/awf)
{{.awf.cache_dir}}       # ~/.cache/awf (or $XDG_CACHE_HOME/awf)
{{.awf.prompts_dir}}     # Designated prompts directory within config_dir
{{.awf.scripts_dir}}     # Designated scripts directory within config_dir
{{.awf.skills_dir}}      # Designated skills directory within config_dir
{{.awf.workflows_dir}}   # Designated workflows directory within config_dir
{{.awf.plugins_dir}}     # Plugin installation directory

Examples:

analyze:
  type: agent
  provider: claude
  prompt_file: "{{.awf.prompts_dir}}/code_review.md"
  on_success: done

deploy:
  type: step
  script_file: "{{.awf.scripts_dir}}/deploy.sh"
  on_success: done

Local-Before-Global Resolution

When using {{.awf.prompts_dir}}, {{.awf.scripts_dir}}, or {{.awf.skills_dir}}, AWF implements local-before-global resolution. This enables per-project overrides of shared global files. The resolution applies to all uses of these variables:

  • In script_file fieldsscript_file: "{{.awf.scripts_dir}}/deploy.sh"
  • In prompt_file fieldsprompt_file: "{{.awf.prompts_dir}}/code_review.md"
  • In command fieldscommand: "source {{.awf.scripts_dir}}/helpers.sh && deploy"
  • In dir fieldsdir: "{{.awf.scripts_dir}}"
  • In skills discovery — Agent skills are resolved across multiple tiers: .awf/skills/ (project override) → .agents/skills/.claude/skills/$XDG_CONFIG_HOME/awf/skills/ (global fallback)
Local Workflows (2-tier)

For local workflows (no pack context), resolution is 2-tier:

  1. Local override<workflow_dir>/prompts/ or <workflow_dir>/scripts/
  2. Global fallback~/.config/awf/prompts/ or ~/.config/awf/scripts/

Example: {{.awf.scripts_dir}}/deploy.sh checks:

  • <workflow_dir>/scripts/deploy.sh (local override)
  • Then ~/.config/awf/scripts/deploy.sh (global fallback)
Pack Workflows (3-tier)

When executing a workflow from an installed pack (e.g., awf run speckit/specify), resolution extends to 3 tiers:

  1. User override (highest priority) — .awf/prompts/<pack>/... or .awf/scripts/<pack>/...
  2. Pack embedded<pack_root>/prompts/... or <pack_root>/scripts/...
  3. Global XDG (lowest priority) — ~/.config/awf/prompts/... or ~/.config/awf/scripts/...

Example: Pack speckit references {{.awf.prompts_dir}}/specify/system-prompt.md:

1. .awf/prompts/speckit/specify/system-prompt.md        ← user override (checked first)
2. .awf/workflow-packs/speckit/prompts/specify/...       ← pack embedded
3. ~/.config/awf/prompts/specify/...                     ← global fallback

No new template variables are introduced. {{.awf.prompts_dir}} and {{.awf.scripts_dir}} are context-aware — they automatically resolve based on whether the workflow is local or from an installed pack.

Override Example
# Project structure with pack override
my-project/
├── .awf/
│   ├── workflows/
│   │   └── deploy.yaml
│   ├── prompts/
│   │   └── speckit/              # User overrides for speckit pack
│   │       └── specify/
│   │           └── system-prompt.md
│   ├── scripts/
│   │   └── deploy.sh            # Local override for local workflows
│   └── workflow-packs/
│       └── speckit/              # Installed pack
│           ├── manifest.yaml
│           ├── workflows/
│           ├── prompts/          # Pack-embedded prompts (overridden above)
│           └── scripts/
└── ...

# ~/.config/awf/scripts/deploy.sh exists globally but is superseded

# Local workflow: uses 2-tier resolution
states:
  deploy:
    type: step
    script_file: "{{.awf.scripts_dir}}/deploy.sh"    # Uses local scripts/deploy.sh if present
    on_success: done

The same behavior applies to prompt_file with {{.awf.prompts_dir}}.

Template Helper Functions

When interpolating template expressions, the following helper functions are available:

split

Split a string into an array by delimiter:

{{split "apple,banana,orange" ","}}

Returns: ["apple" "banana" "orange"]

Use in templates with range to iterate:

{{range split .states.select.Output ","}}
- {{trimSpace .}}
{{end}}

join

Join an array into a string with separator:

{{join (split .states.agents.Output ",") " | "}}

Returns: apple | banana | orange

readFile

Read and inline file contents (with 1MB size limit):

## Specification

{{readFile .states.spec_path.Output}}

The file path is relative to the workflow directory. Fails if:

  • File doesn’t exist
  • File exceeds 1MB (prevents accidental large file loading)
  • Path is not readable

trimSpace

Remove leading and trailing whitespace:

Result: {{trimSpace .states.process.Output}}

Useful for cleaning multiline outputs or removing shell command trailing newlines.

Example: String Manipulation

# Analysis Report

## Available Agents
{{range split .states.list_agents.Output ","}}
- {{trimSpace .}}
{{end}}

## Combined Skills
Skills: {{join .states.available_skills.Output ", "}}

## Research Summary
{{readFile .states.research_summary_path.Output}}

## Status
{{trimSpace .states.final_status.Output}}

Loop Context Variables

Available inside for_each and while loops:

{{.loop.Item}}      # Current item value (for_each only)
{{.loop.Index}}     # 0-based iteration index
{{.loop.Index1}}    # 1-based iteration index
{{.loop.First}}     # True on first iteration
{{.loop.Last}}      # True on last iteration (for_each only)
{{.loop.Length}}    # Total items (-1 for while loops)
{{.loop.Parent}}    # Parent loop context (nested loops)

Example:

process_files:
  type: for_each
  items: '["a.txt", "b.txt", "c.txt"]'
  body:
    - process_single
  on_complete: done

process_single:
  type: step
  command: |
    echo "Processing {{.loop.Item}} ({{.loop.Index1}}/{{.loop.Length}})"
    echo "First: {{.loop.First}}, Last: {{.loop.Last}}"

Loop Item JSON Serialization

When {{.loop.Item}} contains complex types (objects, arrays), it is automatically serialized to JSON:

  • Objects → JSON object: {"name":"value","nested":{"key":"data"}}
  • Arrays → JSON array: [1,2,3] or ["a","b","c"]
  • Strings → Pass through unchanged: "main.go" stays as main.go (not quoted)
  • Numbers → Converted to string: 42 becomes "42", 3.14 becomes "3.14"
  • Booleans → Converted to string: true becomes "true", false becomes "false"

This is especially useful when passing loop items to call_workflow:

# Parent workflow with objects
process_reviews:
  type: step
  command: |
    echo '[{"file":"main.go","type":"fix"},{"file":"test.go","type":"chore"}]'
  capture:
    stdout: reviews_json
  on_success: loop_reviews

loop_reviews:
  type: for_each
  items: "{{.states.process_reviews.Output}}"
  body:
    - call_child_workflow

call_child_workflow:
  type: call_workflow
  workflow: review-file
  inputs:
    review: "{{.loop.Item}}"  # Passed as valid JSON object to child

# Child workflow receives properly formatted JSON
# {{.inputs.review}} = {"file":"main.go","type":"fix"}

Note: String items pass through unchanged without JSON quoting. Numbers and booleans are converted to their string representations.

Nested Loop Parent Access

For nested loops, access outer loop context via {{.loop.Parent}}:

process:
  type: step
  command: |
    echo "outer={{.loop.Parent.Item}} inner={{.loop.Item}}"
    echo "outer_index={{.loop.Parent.Index}} inner_index={{.loop.Index}}"

Chain for deeper nesting:

command: echo "level1={{.loop.Parent.Parent.Item}} level2={{.loop.Parent.Item}} level3={{.loop.Item}}"

Error Variables (in hooks)

Available in error hooks:

{{.error.type}}
{{.error.message}}

Security Considerations

Shell Injection

User-provided values in commands can be dangerous. AWF provides ShellEscape() in pkg/interpolation for escaping:

import "github.com/awf-project/cli/pkg/interpolation"

escaped := interpolation.ShellEscape(userInput)

Secret Masking

Variables with these prefixes are masked in logs:

  • SECRET_
  • API_KEY
  • PASSWORD
  • TOKEN

Example:

# In logs: API_KEY=****
command: curl -H "Authorization: Bearer {{.env.API_KEY}}" https://api.example.com

Interpolation in Different Contexts

Agent Provider

provider: "{{.inputs.agent}}"

Resolves the provider name before registry lookup. Works in both type: agent (single-shot) and mode: conversation steps.

Commands

command: echo "{{.inputs.message}}"

Working Directory

dir: "{{.inputs.project_path}}"

Timeout

timeout: "{{.inputs.timeout}}"

Conditional Expressions

transitions:
  - when: "inputs.mode == 'full'"
    goto: full_process

Note: In when expressions, use variable names without {{}} and without the dot prefix.

Loop Items

items: "{{.inputs.files}}"

Or literal JSON:

items: '["a.txt", "b.txt", "c.txt"]'

Loop Bounds (max_iterations)

Loop bounds support interpolation and arithmetic:

# From input
max_iterations: "{{.inputs.retry_limit}}"

# From environment
max_iterations: "{{.env.MAX_RETRIES}}"

# Arithmetic expression
max_iterations: "{{.inputs.pages * .inputs.retries_per_page}}"

Supported operators: +, -, *, /, %

Dynamic values are resolved at loop initialization (before first iteration). Static validation warns about undefined variables during awf validate.

Loop Conditions

The while and until conditions support interpolation:

while: "{{.states.check.Output}} != 'done'"
until: "{{.states.counter.Output}} >= {{.inputs.threshold}}"

Template Parameters

Template parameters use a different syntax: {{parameters.name}}

# In template definition
command: "{{parameters.model}} -c '{{parameters.prompt}}'"

# Resolved at load time, not runtime

See Templates for details.

Common Patterns

Multi-line Commands

command: |
  echo "Step 1: Process {{.inputs.file}}"
  process-file "{{.inputs.file}}"
  echo "Step 2: Analyze output"
  analyze "{{.states.process.Output}}"

Conditional Values

Use shell conditionals:

command: |
  if [ "{{.inputs.verbose}}" = "true" ]; then
    echo "Verbose mode enabled"
  fi
  run-command --verbose={{.inputs.verbose}}

JSON in Commands

Escape quotes properly:

command: |
  curl -X POST -d '{"file": "{{.inputs.file}}"}' https://api.example.com

Debugging

Use --dry-run to see resolved values:

awf run my-workflow --dry-run --input file=test.txt

Output shows interpolated commands without executing them.

See Also