Version Actions
Version actions generate multiple action instances from a single configuration, enabling parallel processing with different parameters.
Configuration
actions:
- name: extract_raw_qa
versions:
range: [1, 3] # Inclusive range - creates _1, _2, _3
prompt: |
Extract questions using strategy {{ i }}
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
param | string | i | Name of the version variable used in templates |
range | array | [1, 1] | Two integers for an inclusive [start, end], or the values to expand over |
Values that are not integers become the suffixes directly — range: ["literal", "idiomatic"] gives translate_literal and translate_idiomatic. Version consumption cannot collect those: fan-in groups versions by a numeric suffix, so a consumer of a non-integer range finds no versions and logs a warning. Use an integer range where a downstream action consumes the versions.
Template Variables
Version variables are available in both inline prompts and prompt store references, where they render per record. In a versioned action's other values (model_vendor, model_name, intent, …) they render once per version, when the workflow loads:
| Variable | Type | Description | Example |
|---|---|---|---|
{{ i }} | int | Current iteration value | 1, 2, 3 |
{{ idx }} | int | Zero-based index | 0, 1, 2 |
{{ version.length }} | int | Total iterations | 3 |
{{ version.first }} | bool | True on first iteration | true/false |
{{ version.last }} | bool | True on last iteration | true/false |
{{ custom_param }} | int | Custom param value (when param is set) | 1, 2, 3 |
Custom Parameter Names
When using a custom param name, it's available as a top-level variable:
versions:
param: classifier_id
range: [1, 3]
{# Both work: #}
Classifier {{ classifier_id }}
Classifier {{ i }}
Using with Prompt Store
Version variables work with prompt store references, enabling reusable versioned prompts:
# In workflow config
- name: classify_severity
versions:
range: [1, 3]
prompt: $incident_triage.Classify_Severity
{# In prompt_store/incident_triage.md #}
{prompt Classify_Severity}
You are classifier {{ i }} of {{ version.length }}.
{% if version.first %}
Be conservative in your assessment.
{% elif version.last %}
Be comprehensive and thorough.
{% else %}
Balance precision and recall.
{% endif %}
Analyze the incident and provide your classification.
{end_prompt}
This renders as:
- Classifier 1: "You are classifier 1 of 3. Be conservative..."
- Classifier 2: "You are classifier 2 of 3. Balance precision..."
- Classifier 3: "You are classifier 3 of 3. Be comprehensive..."
Version Consumption
Downstream actions consume outputs from all version iterations. The version_consumption block controls how versioned outputs are collected.
| Field | Type | Description |
|---|---|---|
source | string | Name of the upstream versioned action to consume |
pattern | string | merge — combine all version outputs into one record (fan-in); match — pair each version output 1:1 with the consumer |
Consumption requires the source's versions to carry numeric suffixes, which means an integer range.
merge (Fan-In)
The most common pattern. All version outputs are collected into a single record, keyed by version name (e.g., score_quality_1, score_quality_2). Use this for aggregation, voting, and consensus:
- name: score_quality
versions:
range: [1, 3]
- name: aggregate_scores
dependencies: [score_quality]
version_consumption:
source: score_quality
pattern: merge
context_scope:
observe:
- score_quality.* # Wildcard reference
Outputs are merged as nested namespaces:
{
"score_quality_1": {"overall_score": 8, "confidence": 0.9},
"score_quality_2": {"overall_score": 7, "confidence": 0.85},
"score_quality_3": {"overall_score": 9, "confidence": 0.95}
}
Access in prompts (LLM actions):
prompt: |
Scorer 1: {{ score_quality_1.overall_score }}
Scorer 2: {{ score_quality_2.overall_score }}
Access in a record tool: each version is a dict under its own name.
scorer_1 = data["score_quality_1"]
scorer_2 = data["score_quality_2"]
# Iterate all versions dynamically
scores = []
for key, version in data.items():
if key.startswith("score_quality_") and isinstance(version, dict):
scores.append(version["overall_score"])
Access in a FILE tool: each item holds the observed fields as flat keys, qualified by version (score_quality_1.overall_score, score_quality_2.overall_score). There are no nested dicts.
for item in records:
scores = [
value
for key, value in item.items()
if key.startswith("score_quality_") and key.endswith(".overall_score")
]
Common Patterns
Multi-Strategy Extraction
- name: extract_with_strategies
versions:
range: [1, 3]
prompt: |
{% if i == 1 %}Focus on explicit statements
{% elif i == 2 %}Focus on implicit meanings
{% else %}Focus on contextual clues{% endif %}
Extract from: {{ source.text }}
- name: combine_extractions
dependencies: [extract_with_strategies]
version_consumption:
source: extract_with_strategies
pattern: merge
Dollar-brace substitution
Every value of a versioned action supports dollar-brace substitution (${i}, ${i-1}, etc.), dependency strings included. Jinja2 version variables such as {{ i }} render in the prompt per record and in the action's other values per version.
${i-1} cannot be used to chain one version to the previous one. On the first version
it resolves to nothing, leaving a dependency on a bare <action>_ that does not exist,
and the workflow is refused. Referencing the previous version through context_scope
instead reports a cyclic dependency, because every version of an action is one node to
the dependency graph. There is no supported way to make versions of one action run in
order; see Execution Order below.
Parallel Model Comparison
- name: model_comparison
versions:
range: [1, 3]
model_vendor: "{% if i == 1 %}openai{% elif i == 2 %}anthropic{% else %}gemini{% endif %}"
Each version gets the branch for its own i; set model_name and api_key the same way. Keep such a value on one quoted line: a block scalar (|) adds a trailing newline to what it renders, and model_vendor: "openai\n" names no vendor.
Execution Order
Each version becomes its own action in the dependency graph, so the graph decides the order and there is no per-action setting for it.
Independent versions run together. With no dependency between them, all iterations sit in one execution level and run simultaneously.
Versions of one action cannot be ordered against each other. They share their dependencies by construction, so they land in the same level, and nothing in the block changes that. Where later work must see earlier work, use pooling — separate actions, each able to observe the ones before it — rather than versions.
To serialise a whole run, use --execution-mode sequential; to cap how many actions
run at once, use --concurrency-limit.
Context Scope with Versions
- name: extract_variants
versions:
range: [1, 3]
- name: analyze
dependencies: [extract_variants]
context_scope:
observe:
- extract_variants.* # Expands to all version namespaces
Reference specific iterations:
context_scope:
observe:
- extract_variants_1.specific_field
- extract_variants_2.specific_field
Debugging
agac inspect -a workflow_name shows the dependency graph with version
groups collapsed (foo (×3)). To see the per-variant configuration of a
specific expansion, drill into one:
agac inspect action -a workflow_name foo_1 # full config + rendered prompt for variant 1
Enable prompt debug to see rendered prompts per iteration:
- name: extract_variants
versions:
range: [1, 3]
prompt_debug: true
See Also
- Context Scope - Field flow control
- Field References - Accessing version outputs