Tutorial: Build a Test Project by Hand¶
This tutorial builds an AAT project file by file for a small e-commerce API that runs on your machine. By the end you will have a graph, request templates, two environments, plans with assertions, a reusable workflow, a recipe, and a layer matrix, and you will have seen what each piece is for by watching it run. Everything runs offline against aat-sandbox; allow about 45 minutes.
The API is the one the shop example tests. The example is the finished, larger version of what you build here, so you can compare your files with it at any point.
What You'll Build¶
A test that buys a product end to end, using seven of the shop's operations:
| Operation | Method and path | Host |
|---|---|---|
listProducts |
GET /products |
shop API |
createCart |
POST /carts |
shop API |
addItem |
POST /carts/{cartId}/items |
shop API |
checkoutCart |
POST /carts/{cartId}/checkout |
shop API |
paymentCharge |
POST /payments/charges |
payments API |
deleteOrder |
DELETE /orders/{orderId} |
shop API |
deleteCart |
DELETE /carts/{cartId} |
shop API |
The shop API authenticates with OAuth2 bearer tokens and the payments API with an API key, on a different port — the same split a real service and its payment provider usually have.
Prerequisites¶
aatandaat-sandboxinstalled — see Install- Two terminals
Step 1: Start the Sandbox¶
In the first terminal, start the sandbox and leave it running:
aat-sandbox serve
aat-sandbox: shop API http://127.0.0.1:8765/{us,eu}/v1
aat-sandbox: payments API http://127.0.0.1:8766/{us,eu}/v1 (header X-API-Key: pay-demo-key)
token: POST http://127.0.0.1:8765/oauth/token grant_type=password username=demo password=demo client_id=aat-shop client_secret=aat-shop-secret
regions: us (USD, sales tax added at checkout) · eu (EUR, VAT included, no overnight tier)
chaos: GET /inventory/SKU-1004 -> STALE_READ once per token; GET /shipments/{id} -> 503 for the first 2 calls
latency: x1 (shipOrder 600 ms, paymentCharge 350 ms)
reset: POST http://127.0.0.1:8765/admin/reset
The banner lists everything this tutorial needs: the two base URLs (the region, us or eu, is part of the path), the OAuth2 token endpoint and its demo credentials, and the payments API key.
Step 2: Create the Project¶
In the second terminal, extract the finished shop example next to your project — you need its OpenAPI spec now and may want its files for comparison later — and create the project directory:
aat-sandbox init shop-reference
mkdir shop-tutorial && cd shop-tutorial
cp ../shop-reference/openapi.yaml .
mkdir templates plans
Every AAT project starts with a manifest that tells aat where its files are. Create aat-project.yaml:
name: shop-tutorial
description: Tutorial project for the aat-sandbox shop API
graph: graph.yaml
templates: templates/
environment: env.yaml
defaultEnvironment: us
plans: plans/
archives: runs/
Any aat command run in this directory (or below it) finds the manifest by itself. See Project Setup.
Step 3: Describe the Environment¶
An environment says where the API is and how to authenticate. The shop has two regions, so use the multi-environment format: an abstract _base environment (the underscore means it cannot be selected on its own) holds everything the regions share, and each region extends it. Create env.yaml:
environments:
_base:
vars:
apiHost: "localhost:8765"
apiBaseUrl: http://${apiHost}/${region}/v1
headers:
Accept: application/json
auth:
type: oauth2
tokenUrl: http://${apiHost}/oauth/token
credentials:
username: {source: literal, value: demo}
password: {source: literal, value: demo}
clientId: {source: literal, value: aat-shop}
clientSecret: {source: literal, value: aat-shop-secret}
us:
extends: _base
vars:
region: us
${apiHost}and${region}are filled fromvarsafter inheritance, sousonly has to set its region.--var apiHost=localhost:9000would point every environment at a sandbox on another port without editing the file.oauth2fetches a token fromtokenUrlbefore the first request and sends it as a bearer token. The sandbox's credentials are demo values, so they are written asliteralsecrets; for a real API use{source: env, var: SHOP_PASSWORD}so no secret lives in the file.
Check that the file loads:
aat env list
us http://localhost:8765/us/v1
See Environments.
Step 4: Model the First Operations¶
The graph describes the API as operations (nodes) with named inputs and outputs, plus the facts a spec cannot express: which operations must run before which, which operation undoes which, and where an input's value normally comes from. Start with browsing, a cart, and adding an item. Create graph.yaml:
version: "1.0.0"
title: Shop tutorial
oas: openapi.yaml
nodes:
listProducts:
description: List the catalog, optionally filtered by category
adapter: listProducts
oas:
operationId: listProducts
inputs:
- name: category
type: enum[gear, apparel, footwear]
optional: true
outputs:
- name: currency
type: string
- name: products
type: product[]
elementFields:
- name: sku
type: string
- name: name
type: string
- name: price
type: integer
- name: inStock
type: boolean
satisfies: [catalogBrowsed]
createCart:
description: Open a guest cart
adapter: createCart
oas:
operationId: createCart
outputs:
- name: cartId
type: string
cleanup: deleteCart
satisfies: [cartOpen]
addItem:
description: Add a product to a cart
adapter: addItem
oas:
operationId: addItem
inputs:
- name: cartId
type: string
default:
from: createCart.cartId
- name: sku
type: string
default:
from: listProducts.products
select:
strategy: match
field: sku
filter: inStock == true
- name: quantity
type: integer
default: 1
outputs:
- name: lineCount
type: integer
- name: subtotal
type: integer
requires: [catalogBrowsed, cartOpen]
satisfies: [cartPopulated]
deleteCart:
description: Delete a cart (the cleanup for createCart)
adapter: deleteCart
oas:
operationId: deleteCart
inputs:
- name: cartId
type: string
oas:at the top points at the spec, and each node'soas.operationIdties it to an operation, soaat validatecan check the graph and templates against the contract and runs can check every request and response.satisfiesandrequiresrecord ordering:addItemneeds a node that satisfiescatalogBrowsedandcartOpento have run first. Plans still order their steps withdependsOn; AAT turns the tokens intodependsOnwhen it composes a plan from a workflow (Step 9), and the MCP tools use them to trace what a test needs.cleanup: deleteCartpairs creation with teardown: whenever a plan creates a cart, AAT deletes it afterwards, even when a later step fails.default: {from: createCart.cartId}means a plan does not have to wirecartIdby hand; it comes from thecreateCartstep.selectpicks one element of an array output.strategy: matchwithfilter: inStock == truetakes the first product that is in stock, andfield: skutakes its SKU. The sandbox'sSKU-1005is always out of stock, so a naive "first product" would eventually pick it.
See API Graphs and Value Resolution.
Step 5: Write Templates¶
A template turns a node's inputs into an HTTP request and its response into the node's outputs. Create one file per node in templates/:
templates/listProducts.yaml:
adapter: listProducts
protocol: http
request:
method: GET
path: /products{{?category}}?category={{category}}{{/category}}
response:
extract:
currency: currency
products:
path: products
fields:
sku: sku
name: name
price: price
inStock: inStock
templates/createCart.yaml:
adapter: createCart
protocol: http
request:
method: POST
path: /carts
headers:
Content-Type: application/json
body: "{}"
response:
extract:
cartId: cartId
templates/addItem.yaml:
adapter: addItem
protocol: http
request:
method: POST
path: /carts/{{cartId}}/items
headers:
Content-Type: application/json
body: |
{
"sku": "{{sku}}",
"quantity": {{quantity}}
}
response:
extract:
lineCount: lineCount
subtotal: subtotal
templates/deleteCart.yaml:
adapter: deleteCart
protocol: http
request:
method: DELETE
path: /carts/{{cartId}}
{{name}}placeholders take input values.{{?category}}…{{/category}}is a conditional block: the query string is sent only whencategoryhas a value.extractmaps each output to a path in the JSON response. For an array,fieldsnames the element fields the graph declared, soselectand assertions can use them.quantityhas no quotes in the body because it is a number; the base URL, auth, andAcceptheader come from the environment.
Now validate. --strict also fails on warnings, which is how you want to run it while building:
aat validate --strict
Manifest: OK (project: shop-tutorial)
Environment: OK (1 environment: us)
Graph structure: OK (4 nodes)
OAS validation: OK
Adapter outputs: OK (4 templates)
Template inputs: OK
Project validation: PASSED
See Templates and Validation.
Step 6: Write and Run a Plan¶
A plan is a test: steps, the values they use, and what must be true afterwards. Create plans/first-cart.yaml:
intent:
goal: addItem
description: Browse the catalog and put two of the first in-stock product in a cart
execution:
steps:
- node: listProducts
- node: createCart
- node: addItem
dependsOn: [listProducts, createCart]
isGoal: true
values:
quantity: 2
assertions:
mechanical:
- type: status
expect: 200
- type: fieldEquals
path: lineCount
value: 1
- type: predicate
expr: subtotal > 0
Steps without an id are named after their node. addItem gets cartId and sku from the graph defaults and only overrides quantity. Run it:
aat run plan first-cart
aat: loading environment...
aat: loaded environment "us"
aat: loaded graph (4 nodes)
aat: loaded 4 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: authenticated via oauth2
aat: executing plan (3 steps)...
[1/3] listProducts 200 0ms
[2/3] createCart 201 0ms
[3/3] addItem 201 0ms ASSERTIONS FAILED
status: expected status 200, got 201
cleanup:
deleteCart 204 0ms
FAILED: step "addItem" failed mechanical validation
Archive: /home/you/shop-tutorial/runs/run-20260910-235242-31bacd01/archive.json
The request worked — the API answered 201 Created — but the plan expected 200. The line under the step says exactly which assertion failed, and the cart was still deleted. Two of the same SKU make one cart line, so lineCount is 1. Fix the expectation in plans/first-cart.yaml:
- type: status
expect: 201
aat run plan first-cart
aat: loading environment...
aat: loaded environment "us"
aat: loaded graph (4 nodes)
aat: loaded 4 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: authenticated via oauth2
aat: executing plan (3 steps)...
[1/3] listProducts 200 0ms
[2/3] createCart 201 0ms
[3/3] addItem 201 0ms
cleanup:
deleteCart 204 0ms
PASSED (3/3 steps, 0ms)
Archive: /home/you/shop-tutorial/runs/run-20260910-235242-6e5b95fa/archive.json
Every run writes an archive under runs/ with each request and response, how every input got its value, and each assertion's result. See Plans and Recipes and Running Tests.
Step 7: Check Out and Pay¶
Add checkout, payment, and the order's cleanup to the end of graph.yaml, under nodes::
checkoutCart:
description: Price the cart and create an unpaid order
adapter: checkoutCart
oas:
operationId: checkoutCart
inputs:
- name: cartId
type: string
default:
from: createCart.cartId
- name: shippingTier
type: enum[standard, express, overnight]
default: standard
- name: postalCode
type: string
default: "78701"
outputs:
- name: orderId
type: string
display: Order
- name: total
type: integer
- name: currency
type: string
- name: totalDisplay
type: string
display: Total
cleanup: deleteOrder
requires: [cartPopulated]
satisfies: [orderCreated]
paymentCharge:
description: Charge the order on the payments API
adapter: paymentCharge
oas:
operationId: paymentCharge
inputs:
- name: orderId
type: string
default:
from: checkoutCart.orderId
- name: amount
type: integer
default:
from: checkoutCart.total
- name: currency
type: string
default:
from: checkoutCart.currency
- name: cardNumber
type: string
default: "4242424242424242"
outputs:
- name: paymentId
type: string
- name: paymentStatus
type: string
- name: orderStatus
type: string
requires: [orderCreated]
satisfies: [orderPaid]
deleteOrder:
description: Delete an order (the cleanup for checkoutCart)
adapter: deleteOrder
oas:
operationId: deleteOrder
inputs:
- name: orderId
type: string
display: labels an output for run output, so each checkout will print its order ID and total. The payment's amount and currency come straight from the order, which is what the payments API requires. Add the three templates:
templates/checkoutCart.yaml:
adapter: checkoutCart
protocol: http
request:
method: POST
path: /carts/{{cartId}}/checkout
headers:
Content-Type: application/json
body: |
{
"shippingTier": "{{shippingTier}}",
"postalCode": "{{postalCode}}"
}
response:
extract:
orderId: orderId
total: total
currency: currency
totalDisplay: totalDisplay
templates/paymentCharge.yaml:
adapter: paymentCharge
protocol: http
request:
method: POST
path: /payments/charges
headers:
Content-Type: application/json
body: |
{
"orderId": "{{orderId}}",
"amount": {{amount}},
"currency": "{{currency}}",
"method": "card",
"cardNumber": "{{cardNumber}}"
}
response:
extract:
paymentId: paymentId
paymentStatus: paymentStatus
orderStatus: orderStatus
templates/deleteOrder.yaml:
adapter: deleteOrder
protocol: http
request:
method: DELETE
path: /orders/{{orderId}}
aat validate
Manifest: OK (project: shop-tutorial)
Environment: OK (1 environment: us)
Graph structure: OK (7 nodes)
OAS validation: WARN
Warnings:
- node "paymentCharge": output "paymentStatus" not found in OAS 2xx response schema for "paymentCharge"
Adapter outputs: OK (7 templates)
Template inputs: OK
Plans: OK (1 file)
Project validation: PASSED with warnings in 1 section (--strict fails on them)
The payments API calls the charge's state status, not paymentStatus, and validation caught the mismatch before any request was sent. Without --strict a warning does not fail validation, but it is shown. The fix is the point of extract: the graph keeps the clearer name, and only the template knows what the API calls it. In templates/paymentCharge.yaml:
response:
extract:
paymentId: paymentId
paymentStatus: status
orderStatus: orderStatus
Now a plan for the whole purchase. Create plans/purchase.yaml:
intent:
goal: pay
description: Buy the first in-stock product and pay by card
execution:
steps:
- node: listProducts
- node: createCart
- node: addItem
dependsOn: [listProducts, createCart]
- node: checkoutCart
dependsOn: [addItem]
assertions:
mechanical:
- type: status
expect: 201
- type: predicate
expr: total > 0
- id: pay
node: paymentCharge
dependsOn: [checkoutCart]
isGoal: true
assertions:
mechanical:
- type: status
expect: 201
- type: fieldEquals
path: paymentStatus
value: captured
aat run plan purchase
aat: loading environment...
aat: loaded environment "us"
aat: loaded graph (7 nodes)
aat: loaded 7 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: authenticated via oauth2
aat: executing plan (5 steps)...
[1/5] listProducts 200 0ms
[2/5] createCart 201 0ms
[3/5] addItem 201 0ms
[4/5] checkoutCart 201 0ms
Order: ord_0001
Total: $103.40
[5/5] pay (paymentCharge) 404 0ms ASSERTIONS FAILED OAS: 1 warning(s)
status: expected status 201, got 404
fieldEquals: field "paymentStatus" does not exist
cleanup:
deleteOrder 204 0ms
deleteCart 204 0ms
FAILED: step "pay" (paymentCharge) returned status 404
OAS: 1 warning(s)
Archive: /home/you/shop-tutorial/runs/run-20260910-235242-8e0a1311/archive.json
The payment went to the shop API, which does not serve /payments/charges (its error body, in the archive, says so), and the OAS: 1 warning(s) marker is runtime OpenAPI validation noticing that the request lacked the X-API-Key header the spec requires. Both cleanups ran anyway, in reverse order of creation. The payments API lives on its own host with its own credential, so route it there. Add payHost to the _base vars and an overrides entry to _base in env.yaml:
environments:
_base:
vars:
apiHost: "localhost:8765"
payHost: "localhost:8766"
apiBaseUrl: http://${apiHost}/${region}/v1
headers:
Accept: application/json
auth:
type: oauth2
tokenUrl: http://${apiHost}/oauth/token
credentials:
username: {source: literal, value: demo}
password: {source: literal, value: demo}
clientId: {source: literal, value: aat-shop}
clientSecret: {source: literal, value: aat-shop-secret}
overrides:
- match: "payment*"
baseUrl: http://${payHost}/${region}/v1
auth:
type: apikey
headerName: X-API-Key
credentials:
key: {source: literal, value: pay-demo-key}
us:
extends: _base
vars:
region: us
An override matches node names — here every payment* operation — and changes where those requests go and how they authenticate. Headers the environment sets still apply; the shop's bearer token is not sent to the payments host, because the override declares its own auth.
aat run plan purchase
aat: loading environment...
aat: loaded environment "us"
aat: loaded graph (7 nodes)
aat: loaded 7 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: authenticated via oauth2
aat: override: payment*
aat: executing plan (5 steps)...
[1/5] listProducts 200 0ms
[2/5] createCart 201 0ms
[3/5] addItem 201 0ms
[4/5] checkoutCart 201 0ms
Order: ord_0002
Total: $103.40
[5/5] pay (paymentCharge) 201 351ms
cleanup:
deleteOrder 204 0ms
deleteCart 204 0ms
PASSED (5/5 steps, 352ms)
Archive: /home/you/shop-tutorial/runs/run-20260910-235242-60968cd7/archive.json
Both hosts, both credentials, one plan. The order and cart were deleted afterwards, so the run left nothing behind.
Step 8: Capture the Pattern as a Workflow¶
Most tests of this API start the same way: browse, fill a cart, check out, pay. A workflow names that sequence once so tests can reuse it. Register it at the top of graph.yaml, after the oas: line:
workflows:
- name: Purchase
description: Buy the first in-stock product as a guest and pay by card
template: workflows/purchase.yaml
The template is an ordinary plan without the test-specific parts. Create workflows/purchase.yaml:
mkdir workflows
intent:
goal: pay
description: Buy the first in-stock product as a guest and pay by card
execution:
steps:
- node: listProducts
- node: createCart
- node: addItem
dependsOn: [listProducts, createCart]
- node: checkoutCart
dependsOn: [addItem]
- id: pay
node: paymentCharge
dependsOn: [checkoutCart]
isGoal: true
Tell the manifest where workflows live by adding a line to aat-project.yaml:
workflows: workflows/
See Workflows for slots (choice points such as the payment method) and addons (optional steps spliced in), which the shop example uses heavily.
Step 9: Write a Recipe¶
A recipe is a test written against a workflow: it names the workflow and states only what differs. Create plans/two-items.yaml:
kind: recipe
selection:
workflow: Purchase
description: Buy two of a product; the payment is captured
overrides:
values:
addItem.quantity: 2
assertions:
pay:
- type: fieldEquals
path: paymentStatus
value: captured
Override keys are stepId.input for values and step IDs for assertions. Steps composed from a workflow also get a default status: 2xx assertion. Run it like any plan:
aat run plan two-items
aat: loading environment...
aat: loaded environment "us"
aat: loaded graph (7 nodes)
aat: loaded 7 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: reconstituting recipe "Purchase"...
aat: authenticated via oauth2
aat: override: payment*
aat: executing plan (5 steps)...
[1/5] listProducts 200 0ms
[2/5] createCart 201 0ms
[3/5] addItem 201 0ms
[4/5] checkoutCart 201 0ms
Order: ord_0003
Total: $200.82
[5/5] pay (paymentCharge) 201 351ms
cleanup:
deleteOrder 204 0ms
deleteCart 204 0ms
PASSED (5/5 steps, 353ms)
Archive: /home/you/shop-tutorial/runs/run-20260910-235242-ee8e9b6f/archive.json
AAT composed the workflow into a full plan, applied the overrides, and ran it. When the workflow changes — a new step, a different cleanup — every recipe built on it follows. Composition also lists the workflow's cleanup pairings in the plan; they still run newest first, so the order is deleted before the cart, as in purchase (see Running Tests: Cleanup).
Step 10: Multiply with Layers¶
A layer is a named set of input values applied on top of a plan. Layers turn one test into a matrix without copying it. Add a layers/ directory, one file per shipping tier, and register it in aat-project.yaml:
mkdir layers
layers/standard.yaml:
name: standard
description: Standard shipping
inputs:
shippingTier: standard
layers/express.yaml:
name: express
description: Express shipping
inputs:
shippingTier: express
layers/overnight.yaml:
name: overnight
description: Overnight shipping
inputs:
shippingTier: overnight
And in aat-project.yaml:
layers: layers/
aat run batch runs every plan in plans/. Each --layer-group adds a dimension: a group of three layers runs every plan as-is and once per layer. --quiet prints one line per run:
aat run batch --layer-group standard,express,overnight --quiet
first-cart [(base)]: PASSED
purchase [(base)]: PASSED
purchase [express]: PASSED
purchase [overnight]: PASSED
two-items [(base)]: PASSED
two-items [express]: PASSED
two-items [overnight]: PASSED
first-cart [express]: SKIPPED (duplicate of first-cart [(base)])
first-cart [overnight]: SKIPPED (duplicate of first-cart [(base)])
first-cart [standard]: SKIPPED (duplicate of first-cart [(base)])
purchase [standard]: SKIPPED (duplicate of purchase [(base)])
two-items [standard]: SKIPPED (duplicate of two-items [(base)])
Batch: 7/12 PASSED, 5 SKIPPED
Archive: /home/you/shop-tutorial/runs/batch-20260910-235243-e2267d6a
Twelve permutations, seven runs. standard is already the graph default, so those permutations would send exactly the same requests as the plans themselves, and first-cart never checks out, so no shipping tier changes it. AAT detects such duplicates before running anything and skips them. Add a second --layer-group (card types, customers, basket contents) and the runs multiply; see Matrix Testing.
Step 11: Add a Second Region¶
The EU region prices in euros with VAT included, and its checkout needs an EU postal code. Environment values are available to inputs as {{env.NAME}}, so each region can supply its own. Add a values block to us and a new eu environment at the end of env.yaml:
us:
extends: _base
vars:
region: us
values:
postalCode: "78701"
eu:
extends: _base
vars:
region: eu
values:
postalCode: "10115"
Then make the graph default read it — in graph.yaml, change checkoutCart's postalCode input:
- name: postalCode
type: string
default: "{{env.postalCode}}"
aat validate --strict
Manifest: OK (project: shop-tutorial)
Environment: OK (2 environments: eu, us)
Graph structure: OK (7 nodes)
OAS validation: OK
Adapter outputs: OK (7 templates)
Template inputs: OK
Workflow compatibility: OK (1 workflow)
Workflows: OK (1 file, 1 template)
Layers: OK (3 layers)
Plans: OK (3 files, 1 recipe)
Project validation: PASSED
aat run plan purchase --env eu
aat: loading environment...
aat: loaded environment "eu"
aat: loaded graph (7 nodes)
aat: loaded 7 templates
aat: loaded 1 OAS spec(s) for runtime validation
aat: authenticated via oauth2
aat: override: payment*
aat: executing plan (5 steps)...
[1/5] listProducts 200 0ms
[2/5] createCart 201 0ms
[3/5] addItem 201 0ms
[4/5] checkoutCart 201 0ms
Order: ord_0001
Total: €87.78
[5/5] pay (paymentCharge) 201 351ms
cleanup:
deleteOrder 204 0ms
deleteCart 204 0ms
PASSED (5/5 steps, 352ms)
Archive: /home/you/shop-tutorial/runs/run-20260910-235245-a3cb7e02/archive.json
Same plan, same files, a different region: the base URL, prices, currency, and postal code all changed through the environment alone. aat run batch --env eu --layer-group standard,express,overnight would run the matrix there too — and show that the EU has no overnight tier.
Step 12: Look at the Results¶
Open the newest run in the web UI (it needs a release or make build binary):
aat web view latest
The run view shows each step on a timeline with its request, response, resolved inputs, and assertion results; batches show as a permutation matrix. See Web UI and Archives.
Recap¶
| File | What it does |
|---|---|
aat-project.yaml |
Tells every aat command where the project's files are |
env.yaml |
Base URLs, auth, and per-host overrides for each region |
graph.yaml |
Operations, their inputs and outputs, ordering, cleanup, defaults, and workflows |
templates/*.yaml |
HTTP requests and response extraction, one per operation |
plans/first-cart.yaml, plans/purchase.yaml |
Full plans: steps, values, assertions |
workflows/purchase.yaml |
The reusable purchase sequence |
plans/two-items.yaml |
A recipe: a test stated as the difference from a workflow |
layers/*.yaml |
Input values that multiply plans into a matrix |
Where to Go Next¶
The shop example (your shop-reference directory) covers what this tutorial left out, on the same API:
- Slots and addons — the Checkout workflow with a customer slot, a payment slot, and coupon, tracking, and return addons
- Negative tests —
expectFailuresteps,mutationsthat send broken payloads, and an overlay that turns a payment into a declined card - Retries — steps that retry the sandbox's stale inventory read and its warming-up carrier
- Checkpoints — stopping after a step and handing the live session to another tool; see Checkpoints
- Visualizers — a receipt rendered in the web UI; see Visualizers
- AI assistants —
aat mcp servegives Claude Code and other MCP clients the graph and tools to write and run plans like these; see MCP Server