Build a chain
Background: Chain.
Start one
Section titled “Start one”Create a chain from the navigator’s Chains section. It is written to
.tinspec/chains/<name>.yaml and committed like everything else.
Two views of the same chain:
- Steps — an ordered list of cards. Use this for the common linear case.
- Graph — the full canvas. Use this when the flow genuinely branches.
Add steps
Section titled “Add steps”Add step opens the request picker: any saved request, any spec endpoint, or an inline copy you can edit inside the chain without touching the original.
Use a reference when the request is shared and should stay in sync. Use inline when the step needs chain-specific tweaks.
A chain step expects exactly one response, so HTTP, GraphQL, unary gRPC and MCP tool calls
can be steps. WebSocket and LangGraph are duplex/streaming sessions and cannot; nor can a gRPC
method the .proto says streams. Those appear in the picker as disabled rows with the reason
rather than being hidden, so a missing request is never a mystery — and the same check runs again
at run time, because chain YAML is hand-editable and a .proto can change under a saved chain.
Extract values
Section titled “Extract values”Run the step once, then click a field in the JSON response. That creates an extraction:
extract: - var: userId from: body path: data.idfrom is body (dotted path), header (header name as path), or status.
Downstream, use {{userId}} anywhere — the next step’s URL, body, or headers. In both the
Steps cards and the Graph inspector, the variable chips insert the placeholder at your cursor
rather than making you type it.
Branch and loop
Section titled “Branch and loop”condition— an expression withtrueandfalsehandles.router— ordered rules, first match wins, one handle each. A rule with an empty expression always matches, so put it last as the default.loop— iterate a count or a list variable, withbodyanddonehandles.split/merge— fan out and join.splitsays the branches are independent, not that they overlap; see Everything runs one node at a time.delay— wait.
Data nodes (input, environment, math, transform, generator, set, aggregate)
shape values between requests without needing a script. generator produces a uuid, random
int, random string, timestamp, sequence, or list value.
For anything genuinely custom, a script node runs JavaScript in the sandboxed runtime;
the keys of the object it returns become output variables. It gets the same ts.* standard
library a request hook does, and anything it prints with ts.log travels with the node into
the event log’s Console section — a script that did nothing silently is undebuggable.
Assert inside the chain
Section titled “Assert inside the chain”An assert node validates the response of the request above it and routes to a pass or
fail branch — so a chain can check its own intermediate state and take a different path
rather than merely failing.
It uses the same assertion vocabulary as request tests — seven sources, eighteen operators; see
Test a response — with one deliberate exception. op: schema is not offered
in a chain: it validates a body against the endpoint’s declared OpenAPI response, and the host
resolves a chain’s request nodes by id without carrying their spec bindings, so there would be
nothing to check against. Put a schema test on the request itself instead.
When a step fails
Section titled “When a step fails”By default a failing step stops the run, and for a lot of chains that is right: nothing after a failed login means anything. But a suite of twelve independent checks should not lose eleven of them to one flake, and a chain that created an order should delete it even when it blew up halfway. v0.1.0-preview.10 adds the vocabulary for saying so. Every part of it is optional, and a chain written before it round-trips unchanged — the chain format version did not move.
A per-step policy, with a chain-wide default
Section titled “A per-step policy, with a chain-wide default”The toolbar’s Policy popover (both views) sets the default every step inherits, plus the two
clocks below. A step overrides it from its own ⋯ menu in Steps, or the inspector in Graph:
settings: onError: continue # the default every step inherits; absent means `abort`nodes: - id: login type: request position: { x: 0, y: 0 } onError: abort # nothing after this means anything without a token - id: checkInventory type: request position: { x: 280, y: 0 } # no onError — one of twelve independent checks, inheriting `continue`Every field is written only when it differs from the default, so a chain that changes none of
this gains no settings: block and its diff stays empty. Setting a step back to inherit deletes
the key rather than writing onError: abort on every node — which is the noise the chain-wide
default exists to remove.
continue does not mean “pretend it passed”. The step’s outgoing branches are pruned — the rest
of that branch is skipped — and the rest of the chain carries on. The run is still reported as
failed. The node’s card on the Graph canvas carries a small continue badge so a glance at the
graph does not hide it.
There is deliberately no stopOnFailure twin for onError: abort: one behaviour with two
on-disk spellings is exactly what makes a committed format diff badly.
Catch a failure with an error edge
Section titled “Catch a failure with an error edge”Drag from a node’s red error handle (bottom edge, visible on hover) to the step that should
handle the failure. That edge is the try/catch — no new node kind, the same named-branch
mechanism condition, assert and router already use:
edges: - id: e3 source: createOrder target: reportFailure sourceHandle: errorThe catch wins over any policy, in both directions: wiring one is the statement that the failure
is handled, even on a step that says onError: abort. The failing step’s message is published as
{{error}}, readable by the catch target and everything downstream of it — the chain
validator knows about that scope, so it does not flag the one variable a catch exists to consume.
Only the last failure’s message is kept; map it somewhere durable with an output port if a run
can produce two you both need.
Bound the whole run
Section titled “Bound the whole run”settings.timeoutMs is a wall-clock bound on the run. It is applied per node, so the failure
names the node that was in flight when the clock ran out rather than pointing at the chain, and it
is never catchable — a deadline a step’s onError could swallow would leave the run walking
its graph past the deadline. The Policy popover takes seconds; the file stores milliseconds. Leave
it empty for no bound, which is what every chain did before this existed.
A teardown region that always runs
Section titled “A teardown region that always runs”Mark a step Teardown step in the Graph inspector. That marker plus everything forward-reachable from it is the cleanup region:
settings: teardownTimeoutMs: 30000 # absent = 30 snodes: - id: deleteOrder type: request position: { x: 0, y: 260 } region: teardown - id: logout # no marker needed — reached from deleteOrder type: request position: { x: 140, y: 260 }What it guarantees, and what it does not:
- It runs after a pass, after a failure, and after a timeout.
- It runs against the main region’s final variable scope — which is where
{{orderId}}, the id of the thing to delete, lives. - It runs on its own clock (
teardownTimeoutMs, 30 s when absent), not the run’s. A chain that gave up after ten minutes must not wait another ten to clean up. - A teardown failure is real — it makes the run failed — but it is stamped as teardown and never replaces the failure it was cleaning up after. The reported error stays the one that broke the run.
- It does not survive the process being killed. A cancelled CI job leaves fixtures behind, and that is worth saying plainly rather than implying otherwise.
An edge drawn from the main flow into a teardown step is presentational: the region runs whether or not the flow ever got there, which is also why the validator does not flag a teardown step as unreachable.
Run it
Section titled “Run it”Three ways:
- Run all.
- Run to here — everything up to and including one node.
- Test this node — just one, using the variables currently in scope.
Events stream live while it runs. Nodes carry a status ring (running / done / skipped / error), the activated branch animates, pruned branches fade, and the bottom dock tabs between the event log and the current variables. The final snapshot records which nodes were skipped or pruned and which assertion failed.
A run that finished is not a run that passed. A step that said continue, a failure a catch
edge handled, a failed assert, or a broken teardown all end the run normally with a failed
verdict, so the toolbar reads the verdict rather than whether anything threw: Stopped at … when
the run ended where it broke, Failed at … when it carried on past the failure, Timed out at …
when the clock ran out.
Everything runs one node at a time
Section titled “Everything runs one node at a time”Validation catches the usual mistakes
Section titled “Validation catches the usual mistakes”As you author, Tinspec checks for missing request references, variables consumed but never produced, extraction paths that do not exist in the spec’s documented 2xx response, non-chainable protocols, and dangling edges. Problems show on the step; errors disable Run.
Cookies and auth
Section titled “Cookies and auth”The shared cookie jar is inherited, so a login that sets a session cookie is followed by calls that carry it with zero configuration. A step that references a saved request carries that request’s auth provider and sender.
One gap to know about: a chain send applies only the request’s own settings block. The
collection and API-provider layers (a proxy, a client certificate, a CA bundle configured once for
a whole provider) are supplied by the app per send, and the chain runner does not pass them — so a
request behind a provider-configured proxy works in the editor and fails in a chain. The same is
true of a collection-wide script hook. Put what the chain needs on the request itself.
Attach it as a pre-run
Section titled “Attach it as a pre-run”The payoff: set a chain as a request’s pre-run chain (the Chain tab in the request
editor). The chain runs first, its outputs merge into the environment, then the request goes.
Authenticate-then-call becomes one ⌘↵.
A pre-run chain that did not pass blocks the send and says which node broke — sending anyway buys a confusing 401 instead of a clear “your login step failed”. The one exception is a failure that happened only in teardown: the prerequisite still produced its token, so the send proceeds and the cleanup failure is on the run in History.
Let Tinspec plan it
Section titled “Let Tinspec plan it”For an endpoint whose body requires ids you do not have, use Build prerequisites… — from the Docs send menu, the Form toolbar, the navigator’s context menu, or the command palette.
It reads the spec for required *Id body fields and path parameters, finds the POST that
produces each by matching resource stems, recurses, and groups independent steps. It prefers
to reference saved requests you already have and generates a step only where nothing matches.
Bodies are filled from your successful sends where available, otherwise from the spec’s
examples.
You see the planned steps, the reasoning, and anything unresolved before it creates the chain and sets it as the endpoint’s pre-run.
Troubleshooting
Section titled “Troubleshooting”unresolved variable {{x}} in body (chain node 'n') — a step consumes a variable nothing
produced. The validator flags this before you run; check the producing step’s extraction ran
and spelled the name the same way.
A step is disabled in the picker. It is WebSocket, LangGraph, or a streaming gRPC method. Not chainable — see above.
Extraction path flagged as invalid. The dotted path does not appear in the spec’s documented 2xx response for that endpoint. Either the path is wrong or the spec is behind; check Spec Changes.
The run went green in the toolbar but a step clearly failed. It did not: check the verdict wording. A run that carried on past a handled failure ends failed, and the failure list names every one in order, including the ones it continued past.
A cleanup step never ran. Teardown runs after a pass, a failure and a timeout — but not after
the app or the process was killed mid-run, and not past its own 30-second budget. Raise
teardownTimeoutMs in the Policy popover if the cleanup is genuinely slow.