Skip to content

Build a chain

Background: Chain.

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 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.

Run the step once, then click a field in the JSON response. That creates an extraction:

extract:
- var: userId
from: body
path: data.id

from 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.

  • condition — an expression with true and false handles.
  • 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, with body and done handles.
  • split / merge — fan out and join. split says 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.

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.

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.

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: error

The 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.

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.

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 s
nodes:
- 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.

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.

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.

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.

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.

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.

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.