Skip to content

Latest commit

 

History

History
765 lines (567 loc) · 20.7 KB

File metadata and controls

765 lines (567 loc) · 20.7 KB
title Variable Interpolation

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
Provider TokensEstimated Source
Claude false result event usage field
Gemini false result event stats field
Codex false turn.completed event usage field
Copilot false (output only) assistant.message event outputTokens
OpenCode false step_finish event part.tokens field
OpenAI-Compatible false API response usage field
Any provider (fallback) true len(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 (Structured Outputs)

Both agent steps and operation steps return structured data accessible via Response when the output is valid JSON.

Agent steps automatically populate Response when the agent's output contains valid JSON (regardless of output_format setting):

{{.states.step_name.Response.field}}        # Parsed field from agent JSON output
{{.states.step_name.Response}}              # Full parsed JSON object

Operation steps (e.g., github.get_issue, http.request) always return structured data 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

Example with agent step returning JSON:

states:
  initial: analyze

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

  process:
    type: step
    command: |
      echo "Severity: {{.states.analyze.Response.severity}}"
      echo "Issues: {{.states.analyze.Response.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.Response.severity}} = "high"
  • {{.states.analyze.Response.issues}} = ["buffer overflow", "memory leak"]

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

Difference from JSON:

  • Response is populated automatically for all agent outputs if valid JSON is detected (heuristic)
  • JSON is only populated when output_format: json is explicitly set on the agent step
  • Use Response.field for automatic best-effort parsing; use JSON.field for explicit, validated JSON output

See Agent Steps - Output Formatting for examples and Workflow Syntax - Operation State for available operations.

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 (and validation passes)
  • Response is populated automatically for all agent outputs if valid JSON is detected (heuristic, regardless of output_format)
  • Use JSON.field for strict, validated output; use Response.field for lenient, automatic parsing

When to use each:

  • JSON.field: You explicitly requested output_format: json and want strict validation
  • Response.field: You want to access JSON from any agent output without requiring output_format: json

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: |
      # Both work — JSON is strict, Response is lenient
      echo "Severity (JSON): {{.states.analyze.JSON.severity}}"
      echo "Severity (Response): {{.states.analyze.Response.severity}}"
    on_success: done

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

If agent returns:

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

Then both work identically:

  • {{.states.analyze.JSON.severity}} = "high"
  • {{.states.analyze.Response.severity}} = "high"

Difference becomes apparent when output_format: json is omitted:

  • Without output_format: json, JSON validation is skipped
  • JSON remains empty (never populated)
  • Response still populates if valid JSON is detected

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