API Graphs¶
A graph is the foundational data model in AAT. It declares what API operations exist, what data they accept and produce, and how they relate to each other through ordering rules. Everything else in AAT — templates, plans, workflows, validation — builds on the graph.
Overview¶
An AAT graph is a YAML file that models your API as a set of nodes. Each node represents one API operation (e.g., "list products", "create order", "cancel order"). Nodes declare typed inputs (what data the operation needs) and typed outputs (what data it produces).
An input can default to an earlier node's output (default: {from: createCart.cartId}), which covers the common case; plans and workflows wire the rest, where step values reference outputs from earlier steps (e.g., listProducts.productId).
Prerequisites between operations are declared with requires/satisfies tokens and conditions, not with explicit edges. This keeps the graph focused on what each operation is rather than how operations compose into specific test scenarios.
Nodes¶
Each node in the graph represents one API operation. The node name is the YAML map key, and it must be unique within the graph.
nodes:
listProducts:
description: "Search for available products"
adapter: listProducts
inputs:
- name: category
type: string
description: "Product category to filter by"
- name: minPrice
type: float
description: "Minimum price filter"
optional: true
- name: maxResults
type: integer
optional: true
default: 20
outputs:
- name: products
type: product[]
description: "Matching products"
elementFields:
- name: productId
type: string
- name: name
type: string
- name: price
type: money
The adapter field links the node to its template — the YAML file that defines the actual HTTP request and response extraction.
Inputs¶
An input declares one piece of data the operation needs. Inputs are resolved at execution time from plan values, upstream step outputs, or defaults.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Input name, unique within the node |
type |
string | yes | Data type (see Types) |
description |
string | no | Human-readable description |
optional |
bool | no | If true, the operation can execute without this input (default: false) |
configurable |
bool | no | Marks an optional input for aat prompt to fill: the model is offered it as optional configuration to set from the request, even when it has a default, and aat docs generate lists it as configurable. Requires optional: true |
default |
varies | no | Default value when no plan value or upstream output provides one |
constraints |
map | no | min, max, minLength, maxLength, pattern, description. Documentation only: aat docs generate and the MCP tools show them and aat validate checks that they are well formed (the pattern compiles, min ≤ max), but the engine does not enforce them. aat generate fills them from the spec |
Input Defaults¶
Defaults provide fallback values for inputs not supplied by the plan. AAT supports three forms:
Scalar literal — a bare value:
inputs:
- name: currency
type: string
default: "USD"
Pool shorthand — a YAML sequence, from which the engine picks one value:
inputs:
- name: region
type: string
default: ["us-east", "us-west", "eu-central"]
Rich default — a map with explicit control over resolution:
inputs:
- name: productId
type: string
default:
from: listProducts.products # reference an upstream output
select:
strategy: min # selection strategy
field: price # field to evaluate
filter: "category == 'electronics'" # predicate filter
Rich default fields:
| Field | Description |
|---|---|
value |
Literal value |
pool |
Array of candidate values, used when there is no value or the value fails constraint |
poolStrategy |
How to pick from the pool: random (default) or sequential |
constraint |
Predicate the value must satisfy, over value and the inputs already resolved for the step (see Fallback Pools) |
from |
Reference to an upstream step output (step.output) |
fromResolved |
Name of an input declared earlier on the same node; the input takes that input's resolved value (see Intra-Step References) |
select |
Selection config for array sources: strategy, field, filter, index, sortField |
Outputs¶
An output declares one piece of data the operation produces. Outputs are extracted from the HTTP response by the node's template.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Output name, unique within the node |
type |
string | yes | Data type (see Types); use X[] for arrays |
description |
string | no | Human-readable description |
optional |
bool | no | If true, aat validate does not require the node's template to extract this output. When the output is missing, an optional input that takes it with from:, directly or through a named selection, is left out, and a required one fails; aat validate --strict warns about a required input that takes an optional output |
display |
string | no | Label for surfacing this output to the user. When set, the extracted value is printed under the step in console output (Locator: ABC123), included as display_outputs in --json summaries, and stored in the archive |
elementFields |
list | no | Field definitions for array element structure (see below) |
Array Outputs and Element Fields¶
When an output is an array (type ending in []), you can declare elementFields to describe the structure of each element. Element fields are the semantic contract that selection strategies use to pick elements.
outputs:
- name: products
type: product[]
elementFields:
- name: productId
type: string
- name: price
type: money
- name: category
type: string
- name: rating
type: float
Each element field has:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Logical field name used in plans and selection configs |
type |
string | yes | Data type |
path |
string | no | gjson path within the element, used only when the template has no fields map for the output; defaults to name |
The actual extraction mechanics — mapping JSON paths to logical names — are defined in the template: its fields map for the output renames each element's fields (for example productId: id). The graph declares what fields exist; the template declares how to extract them.
The graph-side path is an older way to do the same renaming. It is used only when the template gives the output no fields map: then a selection that names productId reads the element's id when the element field sets name: productId and path: id.
Cleanup¶
The cleanup field on a node names another node that should run after the plan completes to tear down resources, even if the plan fails. This pairs creation operations with their corresponding deletion or cancellation. The field is the node's name, or a mapping with node, when, and releasedBy that says when the cleanup isn't needed (see When a cleanup is skipped below).
nodes:
createOrder:
adapter: createOrder
cleanup: cancelOrder # runs after the plan once createOrder succeeds
inputs: [...]
outputs: [...]
cancelOrder:
adapter: cancelOrder
inputs:
- name: orderId
type: string
The engine registers the pairing once the creating step succeeds (a status below 400 with outputs extracted and no error detection rule triggered), even if the step's assertions then fail, so a plan need not list it. After the main flow, the plan's own cleanup: steps for other nodes run first, in the order listed; then the registered pairings, last created first, once per resource. Cleanup inputs are matched by name against the outputs of the steps that ran (for a registered pairing, the creating step's outputs first), so cancelOrder.orderId takes createOrder's orderId output, and two createOrder steps each cancel their own order. Recipes and aat prompt plans list each pairing in the plan's cleanup: section; a listed pairing still runs from the registered entries, and its runOn decides whether they run.
Cleanup chains. A cleanup node can declare a cleanup of its own. When a cleanup step succeeds (a status below 400 and no error detection rule triggered), its node's cleanup runs right after it, before the next resource is released. This covers an API that releases a resource in two calls, such as requesting a refund and then confirming it:
nodes:
createOrder:
adapter: createOrder
cleanup: requestRefund
outputs:
- name: orderId
type: string
requestRefund:
adapter: requestRefund
cleanup: confirmRefund # runs after each requestRefund that succeeds
inputs:
- name: orderId
type: string
outputs:
- name: refundId
type: string
confirmRefund:
adapter: confirmRefund
inputs:
- name: refundId # named after requestRefund's output
type: string
How a chain behaves:
- Inputs. A chained step takes its inputs first from the outputs of the cleanup steps before it in its chain, nearest first, then the same way as any cleanup step.
- Isolation. Those outputs stay inside the chain, so no other cleanup step picks them up.
- Failure. When a cleanup step fails, the rest of its chain is skipped.
- Loops. A chain that loops back to a node it already passed is rejected when the graph loads.
- Step IDs and links. In archives and --json output, each cleanup step has its own step ID: requestRefund for the first order, requestRefund_2 for the second. cleanupFor (cleanup_for in --json) names the step whose resource the cleanup releases, or the cleanup step before it in its chain.
When a cleanup is skipped. A plan can release a resource itself, such as by capturing a payment whose cleanup would void it. It can also leave the resource in a state its cleanup can't handle. Either way, a cleanup that runs anyway fails. Give the pairing as a mapping to say when it isn't needed:
nodes:
createPayment:
adapter: createPayment
cleanup:
node: voidPayment
when: 'status == "authorized"' # void only a payment that is still authorized
releasedBy: [capturePayment] # capturing this payment releases it
outputs:
- name: paymentId
type: string
- name: status
type: string
capturePayment:
adapter: capturePayment
inputs:
- name: paymentId
type: string
voidPayment:
adapter: voidPayment
inputs:
- name: paymentId
type: string
Before a registered cleanup runs, AAT checks, in order:
1. runOn. If the plan's cleanup: section lists the node, that listing's runOn decides first, as described above.
2. Released. The cleanup is skipped when a main step already released the resource:
- it ran after the step that registered the cleanup
- it ran on the cleanup node itself, or on a node in releasedBy
- it succeeded: a status below 400, no error detection rule triggered, and not a negative test
- it sent the same value for every input it shares with the cleanup, and there is at least one such input. Numbers compare by value.
So an explicit voidPayment step skips the pairing for the payment it voided, with no releasedBy. A step for another payment doesn't, and neither does one that left a shared input unset. Verification steps never release a cleanup, and no main step releases a cleanup in a chain: it cleans up what the cleanup step before it created, after the main steps ran.
3. when. A predicate, in the syntax of a selection filter, over the outputs of the step that registered the cleanup, or of the cleanup step before it in a chain. The cleanup is skipped when it's false. The predicate reads no other step's outputs, so a later step that returns a status of its own doesn't change it. If it can't be evaluated, such as when the step returned no status, the cleanup runs, and its record carries whenError.
A skipped cleanup sends nothing, so its chain doesn't run either. Where skips appear:
- Console: skipped: and the reason in place of a status, as in voidPayment skipped: released by capturePayment (for createPayment).
- Archive: cleanupSkipped, and cleanup_skipped in --json.
- aat run show: under cleanup skipped:.
Loading the graph checks that releasedBy names other existing nodes, each once, and that when parses and names outputs of the node that declares it. So aat validate, aat run, and the MCP server all report a bad pairing.
List in releasedBy only nodes whose success always ends the resource. If an API reports a failed release in a successful response, give that node an error detection rule, so the failure doesn't count as a release.
Tags¶
Nodes can carry tags for filtering and categorization:
nodes:
listProducts:
tags: [search, catalog]
...
Tags are metadata — they don't affect execution but can be used by tooling and the MCP server to filter operations.
Node Ordering¶
AAT uses two mechanisms to declare the valid ordering of operations: requires/satisfies tokens and conditions. Together, these define prerequisite relationships without coupling nodes to specific data-flow scenarios.
These rules describe the API for the tools that trace it: backward chaining in the MCP server (trace_workflow, trace_dependency_chain, workflow documentation), the dependency sections of aat docs generate, and the token and cycle checks in aat validate. They do not order a hand-written plan, which runs its steps in dependsOn order; when a recipe or aat prompt composes a plan from a workflow, AAT adds dependsOn entries from them.
Requires and Satisfies¶
Nodes declare abstract tokens they require (must happen after) and satisfy (make available to others):
nodes:
listProducts:
satisfies: [searchComplete]
...
addToCart:
requires: [searchComplete]
satisfies: [cartPopulated]
...
checkout:
requires: [cartPopulated]
...
This creates an ordering: listProducts -> addToCart -> checkout. The tokens are abstract labels — they don't name specific outputs, just logical prerequisites.
When multiple nodes satisfy the same token, the preferred flag hints which one to use:
nodes:
listProducts:
satisfies: [searchComplete]
preferred: true # prefer this over alternatives
...
searchProducts:
satisfies: [searchComplete]
...
Conditions¶
Conditions are graph-level rules that pull extra nodes into a chain when a predicate holds:
conditions:
- when: "order.international == true"
require: [addCustomsInfo]
before: [submitOrder]
| Field | Description |
|---|---|
when |
Predicate expression, evaluated against a condition context supplied by the caller |
require |
Nodes the chain must include when when is true |
before |
Nodes that each require node is ordered before |
Conditions express rules that don't fit the token model (e.g., "international orders need customs information before submission"). Backward chaining evaluates them only when its caller supplies a predicate evaluator and a context, and no command does today: the MCP tracing tools treat every condition as false. aat validate checks that when is set and that require and before name existing nodes, and the MCP aat://graph resource lists conditions.
Cycle Breaker¶
If your ordering rules create a cycle, mark one node as a cycle breaker to allow traversal:
nodes:
updateCartItem:
cycleBreaker: true
...
This tells the backward chaining algorithm to stop traversing through this node, breaking the cycle. The node still executes normally — the flag only affects graph analysis.
Error Detection¶
APIs sometimes return HTTP 200 with an error payload. Error detection rules let the graph declare patterns that indicate a "successful" response is actually an error.
Rules can be defined at the graph level (apply to all nodes) or at the node level. A node's own rules replace the graph-level rules for that node; they are not merged. Rules are checked on responses with a status below 400, after outputs are extracted, and the first rule that triggers fails the step:
# Graph-level: applies to all nodes
errorDetection:
- path: "error"
rule: exists
details:
message: "error.message"
code: "error.code"
nodes:
listProducts:
# Node-level: applies only to this node
errorDetection:
- path: "errors"
rule: non-empty
details:
message: "errors.0.message"
Rule Types¶
| Rule | Behavior |
|---|---|
exists |
Error if the path exists in the response (non-nil) |
non-empty |
Error if the path holds a non-empty string, array, or object, or any number or boolean |
equals |
Error if the value at the path equals the specified value, which must be a string, a number, or a boolean. Numbers compare by value (0 matches 0 and 0.0), and a string never matches a number |
Detail Mapping¶
The optional details section extracts error information from the response for better error messages:
| Field | Description |
|---|---|
message |
gjson path to the error message |
code |
gjson path to an error code |
category |
gjson path to an error category |
Types¶
AAT supports these data types for inputs, outputs, and element fields:
| Type | Description | Example |
|---|---|---|
string |
Text value | "hello" |
integer |
Whole number | 42 |
float |
Decimal number | 3.14 |
boolean |
True or false | true |
date |
Date in YYYY-MM-DD format | "2026-03-15" |
datetime |
Date and time | "2026-03-15T10:30:00Z" |
money |
Currency amount | "149.99" |
enum[a, b, c] |
Enumeration of allowed values | "a" |
X[] |
Array of type X | — |
| customName | Domain-specific type (e.g., sku, postalCode) |
"WIDGET-100" |
Custom types integrate with the domain knowledge layer. The domain file can declare type definitions and value pools for custom types, which aat prompt, aat docs generate, and the MCP tools use to suggest realistic values. The engine does not draw from them; give an input a pool default for varied test data at run time.
OAS Integration¶
AAT can scaffold graphs from OpenAPI specs, reference OAS operations for validation, and cross-check graph definitions against the spec.
Scaffolding from OpenAPI¶
aat generate --oas api-spec.yaml writes a starting-point graph (one node per operation, with an oas reference on each) and one template per node. The scaffold has no ordering rules, cleanup pairings, or data wiring; you add those by hand. See Scaffolding from OpenAPI for the flags, what the generator writes, and how to preview it.
OAS References¶
Link graph nodes to their OAS operations for validation:
# Graph-level: default spec for all nodes
oas: api-spec.yaml
nodes:
listProducts:
oas:
operationId: ListProducts # required: the OAS operationId
spec: other-spec.yaml # optional: override the graph-level spec
Spec paths are resolved relative to the graph file's directory.
OAS Validation¶
aat validate and aat validate graph check every node that has an oas reference against its spec:
aat validate graph --graph graph.yaml --strict
Errors (always fail validation):
- A referenced spec file cannot be loaded (validation stops there)
oasis set butoperationIdis empty- The node has no spec (neither a node-level
specnor a graph-leveloas) operationIdnot found in the node's spec
Warnings (fail only with --strict):
- A graph input that is neither an OAS parameter nor a request body property, unless the node's template sends it only in request headers, such as an idempotency key
- A required OAS parameter or required request body property that is not a graph input, unless the node's template sends it itself: a query parameter written into
request.path, a header inrequest.headers, a top-level JSON body key, or a form body key, outside{{?…}}and{{#…}}blocks. A bracketed query or form key such asmetadata[source]counts asmetadata - A graph output not found in the first 2xx response schema that declares properties. Each output is looked up at its template extract path, through nested objects and array items, and outputs a Lua transform computes are skipped
The template-aware parts of these checks need the templates: aat validate always loads them, and aat validate graph loads them from --templates or the manifest. Without templates, every input must be a parameter or body property, required fields must be graph inputs, and outputs must be top-level response properties named after the output.
Warnings are informational — intentional divergence from the spec is normal (e.g., omitting optional parameters or extracting only specific response fields).
Graph YAML Reference¶
An annotated graph with every top-level field except examples (few-shot examples for aat prompt's workflow selection) and the common node fields; the tables above list the rest. version is required and must be X.Y.Z.
version: "1.0.0"
title: "E-Commerce API"
description: "Product catalog, cart, and order operations"
notes: "Covers browse-to-checkout happy path plus cancellation"
# Default OpenAPI spec for OAS validation
oas: api-spec.yaml
# Graph-level error detection (applies to all nodes)
errorDetection:
- path: "error"
rule: exists
details:
message: "error.message"
code: "error.code"
# Workflow definitions (see workflows.md for details)
workflows:
- name: Standard Checkout
description: "Browse products, add to cart, and complete checkout"
template: workflows/standard-checkout.yaml
# Conditional prerequisites (see Conditions above)
conditions:
- when: "order.international == true"
require: [addCustomsInfo]
before: [submitOrder]
# Node definitions
nodes:
listProducts:
description: "Search the product catalog"
adapter: listProducts
tags: [search, catalog]
satisfies: [searchComplete]
preferred: true
oas:
operationId: ListProducts
inputs:
- name: category
type: string
description: "Product category to filter by"
- name: minPrice
type: float
optional: true
- name: maxResults
type: integer
optional: true
default: 20
- name: region
type: string
optional: true
configurable: true
outputs:
- name: products
type: product[]
description: "Matching products"
elementFields:
- name: productId
type: string
- name: price
type: money
- name: category
type: string
path: "category"
- name: rating
type: float
addToCart:
description: "Add a product to the cart"
adapter: addToCart
requires: [searchComplete]
satisfies: [cartPopulated]
inputs:
- name: productId
type: string
default:
from: listProducts.products
select:
strategy: min
field: price
outputs:
- name: cartId
type: string
createOrder:
description: "Create a new order from cart contents"
adapter: createOrder
cleanup: cancelOrder
requires: [cartPopulated]
satisfies: [orderCreated]
oas:
operationId: CreateOrder
inputs:
- name: cartId
type: string
- name: productId
type: string
outputs:
- name: orderId
type: string
- name: status
type: string
cancelOrder:
description: "Cancel an order (cleanup)"
adapter: cancelOrder
inputs:
- name: orderId
type: string
outputs:
- name: status
type: string
addCustomsInfo:
description: "Attach customs information to an international order"
adapter: addCustomsInfo
requires: [orderCreated]
inputs:
- name: orderId
type: string
outputs: []
submitOrder:
description: "Finalize and submit an order for processing"
adapter: submitOrder
requires: [orderCreated]
inputs:
- name: orderId
type: string
- name: acceptTerms
type: boolean
optional: true
outputs:
- name: confirmationId
type: string
Validation¶
Run aat validate graph to check the graph for structural errors:
# Structural validation (from manifest or explicit path)
aat validate graph
# With template cross-validation
aat validate graph --templates templates/
# With OAS alignment
aat validate graph --strict
# Explicit paths
aat validate graph --graph graph.yaml --oas api-spec.yaml --templates templates/
Structural checks include: version present and X.Y.Z, adapter set on every node, valid type syntax, input/output name uniqueness within a node, configurable only on optional inputs, well-formed constraints and errorDetection rules, every required token satisfied by some node, no requires/satisfies cycles (unless a node in the cycle is a cycleBreaker), cleanup node existence, and condition references. Unknown keys are errors, with the file, line, and a suggestion.
When templates are available (--templates, or the manifest's templates), validation also checks that each node's declared outputs match the extract keys in its template, catching mismatches like graph outputs that the template never extracts (will be nil at runtime) or template extract keys the graph doesn't declare (dead extraction).
See Validation for the full reference covering all aat validate subcommands.
Source: graph types in graph/types.go, parsing in graph/parse.go, validation in graph/validate.go and graph/oas/validator.go, chaining in graph/chain.go.