Execution Engine records what your code did at runtime — inputs, outputs, errors and timing — without asking you to rewrite the functions themselves. It runs in your own process, adds no dependencies, and returns plain JSON you can visualize, assert on in tests, or store and resume later.
Two independent parts. Use either one without adopting the other.
No engine and no graph: wrap a single call to see what it did, or to stop it from happening twice.
- Trace: Capture inputs, outputs, errors and timing of a single call.
- Cache: Reuse a result for a configurable TTL.
- Memoize: Collapse duplicate concurrent calls into one.
- Timer: Measure a code block, in ms and in words.
Route related calls through an engine, and the graph assembles itself while they run.
- Nodes and edges: Each call becomes a node, and the edges are inferred from order, nesting and parallelism — you never draw them yourself.
- Timing: Every node carries its own start, end, duration and elapsed time.
- Portable JSON: A Cytoscape-compatible shape you can assert on in tests, store and
resume, or open in the
json-to-graph viewer.
Every feature ships twice — a decorator for classes, a plain function for everything else. See Which API to use.
Use npm package manager:
npm install execution-engineRequires Node.js 24+. Decorators need "experimentalDecorators": true in your tsconfig.json; the plain functions
work without it — see Getting Started.
import { trace } from "execution-engine";
class MathOperations {
@trace(console.log) // logs inputs, outputs and duration on every call
add(a: number, b: number): number {
return a + b;
}
}
new MathOperations().add(2, 3); // still returns 5import { ExecutionEngine } from "execution-engine";
const engine = new ExecutionEngine();
const res1 = engine.run((param) => `result1 for ${param}`, ['param1']);
await engine.run(async (param) => `result2 for ${param}`, [res1.outputs]);
const trace = engine.getTrace(); // a flat array of nodes and edgesEach call becomes a node holding what it received, what it returned and how long it took. You never create the edges: the engine works them out from how the calls actually ran.
[
{
"data": {
"id": "fetchUser_…",
"label": "fetchUser",
"inputs": ["u_42"],
"outputs": { "id": "u_42", "plan": "pro" },
"duration": 58.37,
"elapsedTime": "58.370 ms"
},
"group": "nodes"
},
{
"data": { "id": "fetchUser_…->chargeCard_…", "source": "fetchUser_…", "target": "chargeCard_…" },
"group": "edges"
}
]Caching, memoization, decorators, engine context and the full trace format are covered in the documentation.
Runnable examples live in the /examples directory, each with the trace it produced — and the main ones are shown beside their graphs on the Examples page.
- execution-trace.ts — one call, recorded as a plain record. No engine, no graph.
- engine-sequential.ts — the smallest graph there is: four calls, four nodes, three inferred edges.
- engine-checkout.ts — one checkout that exercises everything at once: a chain,
nesting, recursion, a cache hit, a memoized call, and a fork and join.
See its graph →
Explore the comprehensive documentation for this project:
- Getting Started — install, requirements, first trace.
- Which API to use — decorators or functions, and which engine.
- Engine trace format — nodes, edges, nesting and parallelism.
- Migration — upgrading to v4.
For a detailed list of changes, enhancements, and bug fixes, please refer to our Changelog.
If you find any issues or have suggestions for improvement, feel free to open an issue or submit a pull request. Contributions are welcome!
Before getting started, please read our Contribution Guidelines.
Love execution-engine? Give our repo a star ⭐ ⬆️.
This project is licensed under the MIT License - see the LICENSE file for details.