Examples
Every example on this page is a file in the repository that was actually run. The code is on the left, what it produced is on the right, and the numbers are the ones that run recorded — not a sketch of them.
Three small ones first, each wrapping a single function. Then one checkout, routed through an engine, that puts all of it into a single graph.
One function at a time
No engine, nothing to wire up, and no graph — this half of the library wraps a single function and reports what it did. Start here if you want timings, a cache or deduplication and nothing else.
Trace one call
executionTrace runs the function and hands back the record: what went in, what came out, when, and how long it took. The function is not modified and still returns its own value.
import { executionTrace } from 'execution-engine';
async function fetchInvoice(id: string) {
await sleep(45);
return { id, total: 128.4, currency: 'EUR' };
}
const trace = await executionTrace(
fetchInvoice,
['inv_204']
);
console.log(trace.outputs.currency); // 'EUR'
console.log(trace.elapsedTime); // '46.608 ms'the record it returned
{
"metadata": {
"name": "fetchInvoice",
"parameters": ["id"],
"isAsync": true
},
"inputs": ["inv_204"],
"outputs": {
"id": "inv_204",
"total": 128.4,
"currency": "EUR"
},
"startTime": "2026-08-10T13:42:50.815Z",
"endTime": "2026-08-10T13:42:50.861Z",
"duration": 46.608,
"elapsedTime": "46.608 ms"
}Keep a result — @cache
Keyed on the arguments, kept for a TTL. A repeat of the same call never reaches the API; a different argument is a different key and runs.
import { cache } from 'execution-engine';
class ExchangeRates {
@cache({ ttl: 60_000 })
async get(base: string) {
apiCalls++;
return fetchRates(base);
}
}
const rates = new ExchangeRates();
await rates.get('EUR');
await rates.get('EUR'); // never reaches fetchRates
await rates.get('GBP'); // different keywhat it printed
EUR 71.78 ms ← miss, calls the API
EUR 0.11 ms ← hit, served from the store
GBP 71.25 ms ← different key, so it runs
3 calls, 2 reached the APIShare one call — @memoize
cache stores a result once the call succeeds, so callers that arrive while it is still running all miss and all run. memoize stores the in-flight promise instead, so they join the call already going.
import { memoize } from 'execution-engine';
class FeatureFlags {
@memoize()
async load(env: string) {
return fetchFlags(env); // 80 ms
}
}
const flags = new FeatureFlags();
// Twelve components render together, all
// needing the same flags.
const asked = Array.from({ length: 12 }, () =>
flags.load('prod')
);
await Promise.all(asked);A run as a graph
Route the same calls through an engine and you get the second half of the library. Every call becomes a node, the engine infers the edges from what actually happened, and getTrace() returns the whole run.
@engine attaches an engine to the class and @run sends a method through it, so the call sites stay ordinary method calls. One checkout exercises all of it:
- a chain — each step attached to the one before it;
- nesting, where a step runs steps of its own, twice in parallel and once as a graph inside a node;
- recursion —
explodeBundlecalls itself, and nests once per level; - a
@cachehit and a@memoized call, both visible as nodes that cost nothing; - a fork and a join at the top level.
@engine()
class Checkout extends EngineTask {
@run()
async receiveOrder(id: string) { /* … */ }
// Two traced steps of its own, at the same time.
@run()
async enrichOrder(order: Order) {
const [customer, subtotal] = await Promise.all([
this.fetchCustomer(order.customerId),
this.fetchCatalog(order.items)
]);
return { tier: customer.tier, subtotal };
}
// A bundle may contain bundles, so this calls
// itself — as deep as the order happens to go.
@run()
async explodeBundle(sku: string): Promise<Sku[]> {
const parts = await readBundle(sku);
if (!parts) return [sku];
const nested = await Promise.all(
parts.map((part) => this.explodeBundle(part))
);
return nested.flat();
}
// Outside @run, so the branch that arrives second
// joins the call in flight and adds no node.
@memoize()
@run()
async fetchRates(country: string) { /* … */ }
// A graph inside a node: two steps, in order.
@run({ config: { parallel: 'fulfil' } })
async chargeCard(total: number) {
const auth = await this.authorize(total);
return this.capture(auth);
}
}
const task = new Checkout();
const order = await task.receiveOrder('ord-7431');
const enriched = await task.enrichOrder(order);
await task.explodeBundle(order.items[0]);
const priced = await task.priceOrder(order, enriched);
// Independent of each other, so both attach to
// priceOrder instead of chaining.
const [, payment] = await Promise.all([
task.reserveStock(order.items),
task.chargeCard(priced.total)
]);
task.confirmOrder(order, payment);
task.engine.getTrace(); // every node, edge and timingThe fork at the top
reserveStock and chargeCard are two ordinary calls in a Promise.all. Sequence is the only order the engine can infer from that, so by default it would chain the second after the first. config.parallel: 'fulfil' on both says they came off the same source — which is why the graph forks into them and confirmOrder joins them back.
A graph inside a node
Nesting is not a special case of it. chargeCard calls authorize and then capture, and both are traced, so the engine records them inside chargeCard and draws the edge between them. A node that ran a workflow contains that workflow.
Nodes that cost nothing
fetchCatalog at 0.20 ms and fetchRates at 60.5 ms for two callers are the two halves of part one, seen from inside a graph: @cache returned a stored result, and @memoize let the second pricing branch await the first one's call. Both still appear as nodes — the trace records what was asked for as well as what ran.
More
Each example above links to its own source, and to the trace it wrote where there is one.
Larger traces, drawn
Two runs in the repository are better opened than read: they are past the size a page of code explains, and the shape is the point. Both links below load the recorded trace straight into the viewer — nothing to install.
Parallel, nested, and one that fails
One recommendation opens two decisions at once. One of them runs four traced calls of its own; the other throws, and errors: 'catch' keeps it in the graph instead of losing the run. Custom ids and narratives throughout.
A workflow three levels deep
A car built end to end: six stages in sequence, three of which hold steps of their own, and an assembly stage whose sub-stage runs three installations at once. The shape long work actually takes.
Open the graphcar.tscar.jsonThe rest of the repository
A plain sequential run, a recursive dependency resolution, custom trace options, error handling and narratives. The complete set lives in examples/, and every file runs on its own:
npm run examples