Skip to content

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 with fromResolved in the same step or fromInput in 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 --retries rerun, generates new ones.
  • Reserved words. uuid, now, and unixtime always mean these forms, so {{now}} can't refer to an input named now. {{random}} without a length still refers to an input named random.

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 shorthand default: [a, b, c] on a graph input produces: a random pick on each run.
  • The default fails its constraint. 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 in filter: "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 is value plus the step's inputs resolved so far.
  • In predicate assertions, it is the step's extracted outputs, keyed by output name — or the raw response body when the assertion sets raw: 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.