Value Resolution¶
Every step input needs a value. AAT resolves values through a priority-based chain that tries each source in order, from the most specific (plan-provided) to the most general (graph defaults). Understanding this chain is key to writing effective plans and debugging unexpected results.
Resolution Priority¶
When the engine resolves an input for a step, it checks these sources in order and uses the first match:
| Priority | Source | Description |
|---|---|---|
| 1 | Named selection (fromSelection) |
Reference to a pre-resolved array selection |
| 2 | Intra-step reference (fromResolved) |
Reference to an input resolved earlier in the same step |
| 3 | Cross-step input reference (fromInput) |
Reference to a resolved input from a previous step |
| 4 | Step output reference (from) |
Value from a previous step's output, optionally with array select |
| 5 | Plan default / expression / pool | Literal value, {{...}} expression, or fallback pool |
| 6 | Graph default | Default value declared on the node's input in the graph |
| 7 | Optional skip | Input is optional and no value was found — omit from request |
| 8 | Error | Required input has no value |
Priorities 5 and 6 are merged during plan instantiation — graph defaults (including value pools) are copied into the plan's step values before execution, so the engine sees them at priority 5. Layers (when applied) override graph defaults at the same level. This means a well-designed graph with pools on configurable inputs can handle most values automatically — plans only need to specify values that the test requires to be specific.
Two kinds of override sit outside this chain:
- A recipe's overrides.values rewrite the composed plan before it runs, replacing an input's literal or its wiring, so the engine sees them at priority 5.
- Override values: from env.yaml overrides:, .aat-overrides.yaml, or an --overlay file apply after resolution and win over everything. The archive records them with the source override_value.
The explicit-absence marker {} short-circuits this chain for an optional input: it tells the engine to skip the input entirely, bypassing graph defaults, layers, and auto-wiring. On a required input, a plain literal default still applies, from the graph or from a layer, and without one the step fails.
Literal Values¶
The simplest value form is a bare scalar in the plan YAML:
values:
category: "electronics"
maxResults: 20
enabled: true
threshold: 0.95
YAML type inference applies: quoted strings remain strings, unquoted numbers become integers or floats, true/false become booleans. When the graph declares a specific type for the input, the engine coerces the value (see Type Coercion).
To explicitly mark an optional input as absent — preventing it from being filled by graph defaults or auto-wiring — use an empty map:
values:
optionalFilter: {}
References¶
Step Output References (from)¶
The from field pulls a value from a previous step's output:
values:
cartId: {from: createCart.cartId}
The syntax is stepId.outputName. The engine looks up the output value from the referenced step's execution results.
When the referenced output is a scalar, the value is used directly. When the output is an array, you typically need a select block (see Array Selection) or a named selection to pick one element.
Dependency Inference¶
Any reference implies an execution dependency. When a plan is instantiated, the step that a from, a fromInput, or a named selection's from reads joins the step's dependsOn, whether the plan, a recipe, or a graph default wrote the reference. A reference to a step that runs later reorders the steps, and one that closes a cycle is reported with the value or selection that implies the dependency.
Which Step a Default Reads¶
A graph default's from: node.output names a node, and a plan can run that node in several steps. It reads:
- On a main step: the nearest earlier step on that node that isn't expected to fail. With none earlier, the first such step that doesn't depend on it.
- On a verification step: the last step on that node that isn't expected to fail.
So a check after createRefund ran once as a refused request and once successfully reads the successful one. To read a particular step, set the value on the step: charge: {from: firstRefund.charge}.
Array Selection¶
When a step input needs a value from an array output, a selection strategy picks one element.
Inline Selection (from + select)¶
Add a select block alongside from for a self-contained selection:
values:
productId:
from: searchProducts.products
select:
strategy: min
field: productId
sortField: price
filter: "inStock == true"
The field specifies which field to extract from the selected element. If field is omitted, the entire element is used as the value.
Named Selections¶
For coordinated multi-field extraction — pulling several fields from the same array element — define a named selection in the step's selections block:
- node: addToCart
dependsOn: [searchProducts]
selections:
bestProduct:
from: searchProducts.products
strategy: min
sortField: price
filter: "inStock == true"
values:
productId: {fromSelection: bestProduct.productId}
productName: {fromSelection: bestProduct.name}
unitPrice: {fromSelection: bestProduct.price}
All three values come from the same element because they reference the same named selection. Without a named selection, three separate inline select blocks could each pick a different element.
The fromSelection syntax is selectionName.fieldName. If the field part is omitted, the entire selected element is used.
Selection Strategies¶
| Strategy | Required Fields | Description |
|---|---|---|
first |
(none) | First element (after filtering). This is the default when there is no filter; with a filter and no strategy, the selection works like match. |
last |
(none) | Last element (after filtering) |
index |
index |
Element at the specified zero-based index (after filtering) |
random |
(none) | Random element (after filtering) |
min |
sortField, or field in an inline select |
Element with the smallest value of sortField (of field when there is no sortField) |
max |
sortField, or field in an inline select |
Element with the largest value of sortField (of field when there is no sortField) |
match |
filter |
First element matching the filter predicate (no pre-filtering) |
For min and max, the sortField must resolve to a number or to a string that holds one, such as "99.10": APIs often send prices and totals as decimal strings, and they compare by value. Any other value fails the selection. The field parameter (if set) determines which field to extract from the winning element. A named selection needs sortField.
When several elements share the smallest or largest value, the first of them in array order, after the filter, wins, and the step prints a warning:
[3/5] addItem 201 45ms
warning: selection "cheapest": 3 of 8 elements from listProducts.products tie for min price at 19.99; picked index 0 (add a filter to choose, or onTie: first to accept)
Where else the tie appears:
- Warning: in the step's warnings in --json, and in aat run show --step.
- Archive: the selection record carries ties and sortValue.
If a tie would pick the wrong element, add a filter that narrows the candidates. onTie on the selection says what a tie does:
- first takes the first of the tied elements without a warning.
- fail fails the step.
onTie applies only to min and max.
Selections from the same array share one pick when they use the same strategy, filter, index, and compared field. That way, several inputs can read fields of the same chosen element. A pick by another field is made separately.
A filter does not convert strings: comparing a string field with a number fails. Filter such fields with string equality, or let min and max do the numeric comparison.
Filtering¶
A filter expression narrows the array before the strategy is applied. The predicate runs against each element, keeping only those that evaluate to true:
select:
strategy: first
filter: "status == 'available' && price < 1000"
If filtering removes all elements, the selection fails with an error.
For the match strategy, the filter serves as both the narrowing predicate and the selection criterion — it returns the first matching element without pre-filtering.
Coordinated Multi-Field Extraction¶
When you need multiple fields from the same array element, always use a named selection:
selections:
chosenItem:
from: listItems.items
strategy: first
filter: "category == 'premium'"
values:
itemId: {fromSelection: chosenItem.id}
itemName: {fromSelection: chosenItem.name}
itemPrice: {fromSelection: chosenItem.price}
This guarantees that itemId, itemName, and itemPrice all come from the same element.
Intra-Step References¶
fromResolved references a value that was resolved earlier in the same step:
values:
outboundOrigin: "JFK"
outboundDestination: "LAX"
returnOrigin: {fromResolved: outboundDestination}
returnDestination: {fromResolved: outboundOrigin}
This is useful when inputs are logically related — a return trip reverses the origin and destination. The referenced input must appear before the referencing input in the node's declared input order (inputs are resolved in declaration order).
Cross-Step Input References¶
fromInput references a resolved input value from a previous step:
steps:
- node: searchFlights
values:
origin:
pool: ["DEN", "LAX", "ORD"]
destination: "JFK"
- node: bookFlight
dependsOn: [searchFlights]
values:
origin: {fromInput: searchFlights.origin}
destination: {fromInput: searchFlights.destination}
This differs from from (which references step outputs extracted from API responses). fromInput references the inputs that were sent to a previous step. This is useful when multiple steps need consistent input values — for example, using the same origin city for both a search and a booking.
The syntax is stepId.inputName. The referenced step joins dependsOn, and the input name must exist on the source step's graph node.
fromInput is mutually exclusive with from, fromSelection, fromResolved, default, and pool.
Dynamic Expressions¶
Expressions use {{...}} delimiters and are evaluated at execution time. Where they work:
- step values, pools, graph defaults, layers, recipe overrides, and slot inject values
- a fieldEquals value and a quoted predicate string in an assertion, which can name the step's inputs (see Plans: Assertions)
aat validate checks their syntax in step values, pools, and assertions.
Date Expressions¶
values:
departureDate: "{{today}}" # today's date (YYYY-MM-DD)
returnDate: "{{today + 7 days}}" # 7 days from today
pastDate: "{{today - 30 days}}" # 30 days ago
Date expressions always produce YYYY-MM-DD format strings.
Environment Variables¶
values:
apiKey: "{{env.API_KEY}}"
region: "{{env.TEST_REGION}}"
{{env.KEY}} checks the OS environment variable KEY first, then the values: map of the selected environment in env.yaml. An error is raised if neither provides a non-empty value. This works wherever a value is resolved — plan step values, graph input defaults, layer values, and pool entries — but not in request templates: a template placeholder is filled only from step inputs, and env.yaml values: do not reach templates. To send an environment value, give the input a default such as "{{env.postalCode}}" in the graph and use {{postalCode}} in the template.
Generated Values and Timestamps¶
values:
requestKey: "{{uuid}}" # a random UUID
reference: "order-{{random 8}}" # 8 random digits and lowercase letters
since: "{{unixtime - 1 hours}}" # Unix seconds, one hour ago
cutoff: "{{now + 30 minutes}}" # a UTC timestamp, such as 2026-02-08T12:30:00Z
| Form | Result |
|---|---|
{{uuid}} |
A random version 4 UUID, in lowercase |
{{random N}} |
N random characters from 0-9 and a-z, with N from 1 to 64 |
{{now}}, {{now + N unit}}, {{now - N unit}} |
The current time in UTC, in RFC 3339 to the second |
{{unixtime}}, {{unixtime + N unit}}, {{unixtime - N unit}} |
The current time as Unix seconds, an integer |
The unit is seconds, minutes, hours, or days, or the singular; a day is 24 hours. {{today}} counts days only, so {{today + 2 hours}} is an error that suggests now or unixtime.
- Each occurrence is its own value.
"{{random 4}}-{{random 4}}"has two different halves, and two inputs set to{{uuid}}get different UUIDs. To send one generated value twice, set it on one input and read it withfromResolvedin the same step orfromInputin a later step. - A step's values are generated once. A retried step resends the values its first attempt sent, so an idempotency key stays the same across the attempts. A new run, or a plan-level
--retriesrerun, generates new ones. - Reserved words.
uuid,now, andunixtimealways mean these forms, so{{now}}can't refer to an input namednow.{{random}}without a length still refers to an input namedrandom.
Reference Arithmetic¶
values:
departureDate: "2026-03-15"
returnDate: "{{departureDate + 7 days}}"
Reference arithmetic operates on a previously resolved input's value. The referenced value must be a date string in YYYY-MM-DD format.
Mixed Expressions¶
Expressions can be embedded in literal strings:
values:
searchQuery: "flights departing {{today + 3 days}}"
correlationId: "test-{{env.BUILD_ID}}-run"
When an expression is the entire value (no surrounding text), the result retains its native type (e.g., a date string). When embedded in text, the result is always a string.
Fallback Pools¶
A pool is a list of candidate values. The engine uses it in two cases only:
- There is no
default. The engine picks from the pool. This is what the list shorthanddefault: [a, b, c]on a graph input produces: a random pick on each run. - The
defaultfails itsconstraint. The engine then tries the pool entries (evaluating any expressions) and uses the first one that passes.
A default without a constraint always wins, so a pool next to it is never used:
values:
origin: "JFK"
destination:
default: "JFK"
pool: ["LAX", "ORD", "SFO"]
constraint: "value != origin" # "JFK" fails, so a pool entry is used
Pool iteration order depends on poolStrategy:
| Strategy | Behavior |
|---|---|
random |
Shuffled order (default) |
sequential |
Declaration order |
A pool entry whose expression cannot be evaluated is skipped. When no entry passes the constraint, the step fails with an error naming the constraint. An expression in the default itself that cannot be evaluated is an error, not a reason to fall back.
Constraints¶
constraint is a predicate expression that a candidate value must satisfy. The candidate is available as value, and every input of the same step resolved before this one is available by name — inputs resolve in the order the graph node declares them, so origin must come before destination in the example above. The constraint is checked against the default and against pool entries; it is not applied to from, fromSelection, fromInput, or fromResolved values. Graph input defaults and layer entries accept the same pool, poolStrategy, and constraint fields (a graph default writes its literal as value: instead of default:).
Type Coercion¶
The engine coerces resolved values based on the graph input's declared type:
| Input Type | Coercion Rules |
|---|---|
date |
Datetime strings (2026-01-15T00:00:00Z) are truncated to date (2026-01-15); time.Time values are formatted as YYYY-MM-DD |
datetime |
time.Time values are formatted as RFC3339 |
integer |
String digits are parsed to int; floats are truncated |
boolean |
String "true"/"false" are parsed to bool |
float |
String numbers are parsed to float64 |
Coercion is best-effort — if parsing fails, the original value is passed through so the downstream API can report the type mismatch.
Predicate Expression Syntax¶
Predicate expressions are used in selection filters, value constraints, and predicate assertions. They support standard comparison and logical operators:
Operators¶
| Operator | Description | Example |
|---|---|---|
== |
Equal | status == "active" |
!= |
Not equal | type != "archived" |
< |
Less than | price < 100 |
> |
Greater than | quantity > 0 |
<= |
Less than or equal | rating <= 5.0 |
>= |
Greater than or equal | stock >= 10 |
&& |
Logical AND | inStock == true && price < 50 |
\|\| |
Logical OR | status == "active" \|\| status == "pending" |
! |
Logical NOT | !isExpired |
in |
Membership | category in ["electronics", "books"] |
Value Types¶
- Strings:
"double"or'single'quotes — single quotes avoid escaping inside a double-quoted YAML string, as infilter: "status == 'available'". There are no escape sequences. - Numbers:
42,3.14,-5(integer and float) - Booleans:
true,false - Identifiers:
fieldName,nested.path(dot notation through nested objects; no array indexes or queries) - Parentheses:
(a || b) && c
Predicates evaluate against a context map:
- In selection filters, the context is the array element.
- In a
constraint, it isvalueplus the step's inputs resolved so far. - In
predicateassertions, it is the step's extracted outputs, keyed by output name — or the raw response body when the assertion setsraw: true, or when the response status is 400 or above (nothing is extracted then). See Plans: Assertions.
A field that does not exist in the context is an error rather than false, so a filter or assertion that names a missing field fails with unknown field. Comparing values of different types (a string with a number) is an error too.
Debugging Value Resolution¶
Every input resolution is recorded in the run archive with a ValueResolution entry:
| Field | Description |
|---|---|
inputName |
Which input was resolved |
source |
How it was resolved: plan_default (a value the plan sets), graph_default (a graph default), layer (a layer's value), expression, fallback_pool, plan_from, select_edge, named_selection, from_resolved, from_input, or optional_skip when an optional input is left out (AUTOWIRE?, or from: an output the earlier step didn't return) |
layer |
The layer that set the value, whatever the source, such as an expression a layer wrote |
rawValue |
Value before expression evaluation |
finalValue |
Value after evaluation and type coercion |
expression |
The {{...}} template if evaluated |
constraint |
The constraint checked, if any |
constraintOk |
Whether the chosen value passed it |
poolIndex |
Which pool entry was used (-1 if not from pool) |
poolSize |
Total pool size |
tried |
Values that were tried and rejected before the winning value |
Array selections are recorded as SelectionDecision entries:
| Field | Description |
|---|---|
sourceNode |
Step that produced the array |
sourceField |
Output name of the array |
sourceSize |
Array size before filtering |
filteredSize |
Array size after filtering |
strategy |
Selection strategy used |
selectedIndex |
Index of the selected element |
filterExpr |
Filter predicate if applied |
selectionName |
Named selection name (if applicable) |
Use the Web UI to inspect these records in the step detail view, or read the archive JSON directly.
Common Patterns¶
Date Expressions for Testing¶
values:
departureDate: "{{today + 3 days}}"
returnDate: "{{today + 10 days}}"
Named Selection with Multi-Field Extraction¶
selections:
bestOffer:
from: searchProducts.products
strategy: min
sortField: price
filter: "inStock == true && rating >= 4.0"
values:
productId: {fromSelection: bestOffer.productId}
productName: {fromSelection: bestOffer.name}
unitPrice: {fromSelection: bestOffer.price}
Pool with Expressions¶
values:
departureDate:
pool: ["{{today + 2 days}}", "{{today + 5 days}}", "{{today + 10 days}}"]
poolStrategy: random
Intra-Step Reference Chaining¶
values:
outboundOrigin: "JFK"
outboundDestination: "LAX"
returnOrigin: {fromResolved: outboundDestination}
returnDestination: {fromResolved: outboundOrigin}
Cross-Step Input Consistency¶
steps:
- node: searchFlights
values:
origin:
pool: ["DEN", "LAX", "ORD", "SFO"]
destination: "JFK"
departureDate: "{{today + 7 days}}"
- node: bookFlight
dependsOn: [searchFlights]
values:
origin: {fromInput: searchFlights.origin}
destination: {fromInput: searchFlights.destination}
departureDate: {fromInput: searchFlights.departureDate}
Explicit Absence to Override Defaults¶
values:
# Suppress the optional discount code — don't use the graph default
discountCode: {}
Source: resolution logic in engine/resolve.go, selection strategies in engine/selection.go, expressions in plan/expr.go, predicates in plan/predicate.go, type coercion in engine/resolve.go.