Skip to main content

Version Actions

Version actions generate multiple action instances from a single configuration, enabling parallel processing with different parameters.

Configuration​

yaml
YAML
actions:
- name: extract_raw_qa
versions:
range: [1, 3] # Inclusive range - creates _1, _2, _3
prompt: |
Extract questions using strategy {{ i }}

Parameters​

ParameterTypeDefaultDescription
paramstringiName of the version variable used in templates
rangearray[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:

VariableTypeDescriptionExample
{{ i }}intCurrent iteration value1, 2, 3
{{ idx }}intZero-based index0, 1, 2
{{ version.length }}intTotal iterations3
{{ version.first }}boolTrue on first iterationtrue/false
{{ version.last }}boolTrue on last iterationtrue/false
{{ custom_param }}intCustom 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:

yaml
YAML
versions:
param: classifier_id
range: [1, 3]
jinja2
JINJA2
{# Both work: #}
Classifier {{ classifier_id }}
Classifier {{ i }}

Using with Prompt Store​

Version variables work with prompt store references, enabling reusable versioned prompts:

yaml
YAML
# In workflow config
- name: classify_severity
versions:
range: [1, 3]
prompt: $incident_triage.Classify_Severity
markdown
MD
{# 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.

FieldTypeDescription
sourcestringName of the upstream versioned action to consume
patternstringmerge — combine all version outputs into one record (fan-in); match — pair each version output 1:1 with the consumer
note

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:

yaml
YAML
- 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:

json
JSON
{
"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):

yaml
YAML
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.

python
PY
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.

python
PY
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​

yaml
YAML
- 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.

warning

${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​

yaml
YAML
- 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​

yaml
YAML
- 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:

yaml
YAML
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:

bash
BASH
agac inspect action -a workflow_name foo_1 # full config + rendered prompt for variant 1

Enable prompt debug to see rendered prompts per iteration:

yaml
YAML
- name: extract_variants
versions:
range: [1, 3]
prompt_debug: true

See Also​