Release candidate — 1.0.0-rc.7
← Back to blog

Compose Dependent API Calls Into One Traced Flow

Oleksandr Zhuravlov

You fetch an order, then the shipment booked against it, then the carrier's live tracking — and the tracking call needs the shipment's code and the order's region, two hops back. Three calls, in order, each needing a result from the one before. When one fails at 2am, you want a trace that shows what ran first, not three unrelated calls.

Written as plain awaits, most of that is already fine. Each call is a stitch — a typed, validated, resilient function over one endpoint — so the intermediate values are real types and an ancestor two steps back is just a variable still in scope:

const  = await ({ : { : 1043 } });
const  = await ({ : { : . } });
const  = await ({
    : { : ., : . },
    : { : . }, // two hops back — just a variable
});

order is Order, shipment is Shipment, no cast. Each stitch carries its own retry, timeout, and output validation. The sharp edge isn't the code — it's the trace. Each bare await mints its own root run, so those three calls are three unrelated operations in your logs; nothing ties them into one. linked() fixes exactly that, and nothing else: it opens a run scope and lets you keep writing the same awaits.

The scope

const  = await (async () => {
    const  = await (, { : { : 1043 } });
    const  = await (, {
        : { : . },
    });
    return (, {
        : { : ., : . },
        : { : . }, // still just a variable
    });
});

linked(body) hands your body a run. Call your stitches through it instead of directly — that's the whole API.

  • Same types, no casts. run(stitch, input) is typed straight from the stitch: input from its input, result from its output. The two-hops-back ancestor isn't a feature you invoke; it's a variable you already have.
  • One trace. Each run(...) chains as a child of the call before it, so the trace draws fetchOrder → fetchShipment → fetchTracking as one connected sequence — order the root run, shipment a child of order, tracking a child of shipment.
  • Fails fast. A rejected call rejects the whole linked(...) and the body stops, carrying the failing call's .status, .attempts, .body, and .url. You handle one failure at the boundary — the await you already wrote.

Concurrent steps stay in the trace

When two calls inside the scope don't depend on each other, don't await them in series — hand run an all. run accepts any combinator as a node, so the parallel fan nests inside the same trace:

const  = await (async () => {
    const  = await (, { : { : 1043 } });
    // shipment and invoice are independent — fan them out, still inside the trace
    const { ,  } = await (
        ({ : , :  }),
        { : { : . } },
    );
    return { : ., : . };
});

The chain shows up in the trace

This is the detail that pays off in production. A hand-written ladder of awaits and a linked scope read almost identically — the difference is entirely in the trace:

Plain awaitslinked
Intermediate types✅ typed variables✅ typed variables
Fail-fast✅ (await throws)
Per-call resilience✅ each stitch's own✅ each stitch's own
One linked trace❌ three root runs✅ one connected chain
Concurrent sub-stepsPromise.all (loose)run(all(...)) (in the trace)

So when tracking comes back pointing at the wrong region, the trace (and the playground's call-graph DAG) shows the order fetch that ran first, the value it produced, and the input the tracking call received. You read the sequence instead of reassembling it — and each stitch in the scope stays a standalone function you can call, test, or expose through any front door on its own.

Try it

npm install stitchapi@rc

linked lives at the stitchapi/pipe subpath, so you only pull it into the bundle when a flow needs it. Each call is a stitch with its own validation, and the whole scope is visible in the event stream.

Related reading: run independent calls in parallel, type-safe pagination, one definition, four front doors, and runtime stitching vs. workflow platforms.