LEARN / EXECUTION & OBSERVATION
Control the moving parts.
Follow state transitions, inspect the result, and keep execution inside clear application boundaries.
Runs & concurrency
engine.run(initial, options?) resolves with a RunResult. Only one run can be active on a given engine, including its cancellation-draining period. Use separate engines for independent simultaneous runs.
const engine = new Engine(workflow, {
concurrency: 4, // shared by nodes AND transformations
historyLimit: 500,
});
const result = await engine.run({ orderId: "A-001", total: 1500 });
if (result.status === "succeeded") {
console.log(result.results);
}
The default concurrency is four tasks. This is async concurrency, not multithreading: synchronous CPU work still blocks the event loop. The engine validates and captures a frozen graph during construction.
Snapshots & events
const stopState = engine.subscribe(() => {
console.log(engine.getSnapshot().status);
});
const stopEvents = engine.onEvent((event) => {
if (event.type === "node.changed") {
console.log(event.nodeId, event.state.status);
}
});
try {
await engine.run(21);
const previous = engine.getHistory().at(-2);
console.log(previous?.snapshot.status);
} finally {
stopState();
stopEvents();
}
getSnapshot() returns an immutable snapshot with a stable reference between transitions. It includes workflow/run IDs, version, overall status, and node/link state dictionaries. Business payloads are absent; read outputs from RunResult.results.
| Event | Additional fields |
|---|---|
run.started | No additional fields |
node.changed | nodeId, state |
link.changed | linkId, state |
run.finished | Final status |
Every event includes sequence number, run ID, and timestamp. State and history update before synchronous notifications. Synchronous observer exceptions are isolated from execution.
Bounded history, not replay
getHistory() returns event/snapshot pairs. The default retention is 500 entries. Set historyLimit: 0 to disable it or use clearHistory() to empty it. The bound counts entries, not bytes.
History stores statuses, timestamps, and error metadata—not input data, outputs, or link payloads. Inspecting it never restarts work or repeats an external effect. Avoid putting secrets in error messages.
Failures & terminal statuses
Rejected node handlers fail the node. Rejected transformations fail the link, leaving the source node’s successful status intact. Failed dependencies block descendants; independent branches keep running.
| State | Statuses |
|---|---|
| Run | idle · running · succeeded · failed · cancelled |
| Node | pending · running · succeeded · failed · skipped · blocked · cancelled |
| Link | pending · delivered · inactive · failed · cancelled |
Outside cancellation, any node/link failure makes the run failed. Inactive links and skipped nodes are normal routing outcomes; an empty graph succeeds.
Cancel cooperatively
const controller = new AbortController();
const pending = engine.run(21, { signal: controller.signal });
controller.abort();
const result = await pending;
console.log(result.status); // "cancelled"
Cancellation stops new tasks and signals in-flight nodes and transformations. Forward context.signal to cancellable operations such as fetch and check it inside handlers.
The engine waits for already-started tasks to settle. Work that ignores the signal can delay finalization indefinitely. Aborting is not transaction rollback and does not undo effects already performed. Await the original promise before reusing the engine; cancellation accepted before finalization produces cancelled.