Skip to content

Chain

A chain is a graph of steps where later steps use values produced by earlier ones. Call the login endpoint, extract the token, call the real endpoint with it. Create a customer, extract the id, create an order for it.

This is the headline feature, so extraction and variable-passing are first-class rather than bolted on.

Chains are workspace-level, not part of a collection: one YAML file per chain under .tinspec/chains/, with their own formatVersion: 3. They are committed like everything else.

A chain can reference requests from any collection in the project, or carry its own inline copy of a request.

Sixteen node kinds in four groups:

  • Request — request. Either a reference to a saved request (collection + name) or an editable inline copy with its own auth and body.
  • Data — input, environment, math, transform, generator, set, aggregate.
  • Control flow — condition, router, loop, merge, split, delay.
  • Logic — script (JavaScript in the engine’s sandboxed runtime) and assert.

Edges connect nodes. A condition has true/false handles, a router one handle per rule (first match wins), a loop a body and a done handle, an assert a pass and a fail handle.

Click a field in a request node’s JSON response and Tinspec creates an extraction — a named variable downstream nodes can use as {{name}}.

extract:
- var: userId
from: body
path: data.id
- var: requestId
from: header
path: x-request-id

from is body (with a JSON path), header (with the header name as path), or status.

An assert node evaluates the same assertion vocabulary a request’s own tests use — one evaluator, one on-disk shape, so a check reads the same in both places. All of its assertions must pass for the pass branch; any failure takes fail.

Seven sources — status, body, header, var, duration, size, cookie — and eighteen operators.

One operator is deliberately not offered here: op: schema, which validates a body against the endpoint’s own declared OpenAPI response schema. It needs a spec binding to mean anything, and a chain node does not necessarily have one, so the request editor’s Tests tab offers it and the chain assert inspector does not.

By default a failing node aborts the run — which is what every chain did before there was any vocabulary for this. Three things can change that, and they are checked in this order:

  1. An outgoing edge on the error branch is a catch. Declaring where a failure goes is the statement that it is handled, so the edge wins over any policy, in both directions. This needed no new vocabulary: it reuses the same named-branch mechanism condition, router and assert already use. A handled failure publishes its message as {{error}} for the catch branch to read.
  2. The node’s own onError — abort or continue.
  3. The chain’s settings.onError, the default for every node that declares none.

The granularity is per node with a chain-wide default because a CI suite’s needs differ per step: the login must abort, and the twelve independent checks after it must not.

Note that Ok has never meant “everything passed” — a failed assert has always taken its fail branch and finished normally. The run’s status and its list of failures are what make that legible.

settings.timeoutMs bounds the run. It is applied per node, so the failure names the node that was running when the clock ran out, and it is deliberately not catchable — a timeout a node’s onError could swallow would leave the run walking its graph past the deadline.

A node marked region: teardown, plus everything forward-reachable from it, is the teardown region. It runs after a pass, after a failure and after a timeout, on its own clock (settings.teardownTimeoutMs, 30 seconds by default) — deliberately not the chain’s own, because a chain that gave up after ten minutes must not wait another ten to delete the order it created. It sees the main region’s final variable scope, which is where the id of the thing to delete lives.

A teardown failure is recorded and stamped as such, and never replaces the failure it was cleaning up after.

It does not survive a process signal. A killed CI job leaves fixtures behind — worth saying rather than implying.

All of this is additive: formatVersion stays 3, and a chain written before any of it existed loads and behaves identically.

  • Steps renders a linear chain as an ordered list of editable cards — URL, headers and body in place for inline requests, outputs, reorder, run to here, test this step. A fan-out is bracketed and labelled independent: the branches do not depend on each other, so their order does not matter — but they still run one after another.
  • Graph is the full canvas, built on Svelte Flow, for anything genuinely branching.

It is one chain in both views; the toggle is a rendering choice.

Nothing in a chain runs concurrently. split fans out in the graph and the frontier interleaves the branches layer by layer, but the engine executes sequentially — there is no parallel execution anywhere in Tinspec at v0.1.0-preview.10. Loops are bounded and not nested, and a global step budget guards against a runaway graph.

Run the whole chain, run to here, or test a single node. Events stream live: each node shows a status ring (running / done / skipped / error), the activated branch animates, pruned branches fade, and a dock at the bottom tabs between an event log and the current variables.

Final results come back as a snapshot too, including which nodes were skipped or pruned and which assertion failed.

Chains are validated as you author them — missing request references, variables consumed but never produced, extraction paths that do not exist in the spec’s documented 2xx response, non-chainable protocols, dangling edges. Problems show on the step, and errors disable Run.

One limit to know before you trust a chain in CI

Section titled “One limit to know before you trust a chain in CI”

The collection and API-provider layers of send settings do not apply inside a chain. A chain run takes no settings environment, so each node uses its own settings and nothing else. A suite behind a provider-configured corporate proxy therefore succeeds in the editor and fails in a chain.

A chain step expects exactly one response per request, so only single-shot protocols qualify: HTTP, GraphQL, unary gRPC, and MCP (one tool call is one request and one response).

WebSocket sessions, streaming gRPC methods, and LangGraph runs are multi-message and are not chainable. The request picker shows them disabled with a tooltip, and the engine refuses them at run time rather than failing halfway.

A request can name a chain as its pre-run chain. The chain runs first, its output variables merge into the environment, and then the request is sent — so “authenticate, then call” is one press of ⌘↵ with no manual step.

Chains inherit the shared cookie jar automatically, so a login that sets a session cookie is followed by calls that carry it, with nothing to configure.

For an endpoint whose request body requires ids you do not have, Build prerequisites… plans the chain for you: it reads the spec for required *Id body fields and path parameters, finds the POST that produces each one by matching resource stems, recurses, and groups independent steps together. It prefers to reference your existing saved requests and generates a step only where none matches.

You get a preview of the steps and the reasoning before anything is created.

An imported Postman collection can also become one: where a variable set by pm.environment.set crosses to a later request, the importer builds a linear chain instead of leaving you two disconnected scripts. See Importing for what it converts and what it refuses.

See Build a chain for the walkthrough.