Skip to content

Pause & resume

Pausable stages stop execution and create a JSON-serializable checkpoint. Store it in Redis, Postgres, or a file — then resume hours later on any server.

A PausableHandler has two methods: execute runs first and optionally pauses (by returning data), resume continues after the human responds.

import { flowChart, FlowChartExecutor } from 'footprintjs';
import type { PausableHandler } from 'footprintjs';
interface RefundState {
orderId: string;
amount: number;
riskLevel?: string;
approved?: boolean;
approver?: string;
refundId?: string;
}
const approvalGate: PausableHandler<any> = {
execute: async (scope) => {
scope.riskLevel = scope.amount > 500 ? 'high' : scope.amount > 100 ? 'medium' : 'low';
// Return data = pause. Return nothing = continue.
return {
question: `Approve $${scope.amount} refund for ${scope.orderId}?`,
riskLevel: scope.riskLevel,
};
},
resume: async (scope, input) => {
const decision = input as { approved: boolean; approver: string };
scope.approved = decision.approved;
scope.approver = decision.approver;
},
};
const chart = flowChart<RefundState>('ReceiveRequest', async (scope) => {
scope.orderId = 'ORD-2024-7891';
scope.amount = 299;
}, 'receive-request')
.addPausableFunction('ManagerApproval', approvalGate, 'manager-approval')
.addFunction('ProcessRefund', async (scope) => {
if (scope.approved) {
scope.refundId = 'REF-' + Date.now();
}
}, 'process-refund')
.build();
const executor = new FlowChartExecutor(chart);
executor.enableNarrative();
await executor.run();
if (executor.isPaused()) {
const checkpoint = executor.getCheckpoint(); // JSON-safe, store anywhere
// Later (hours, different server):
await executor.resume(checkpoint, { approved: true, approver: 'Sarah' });
}
// Narrative spans both run + resume
executor.getNarrativeEntries().map((e) => e.text).forEach((line) => console.log(line));
  • execute returns data → pauses. Returns void → continues normally (conditional pause).
  • Checkpoint is JSON-serializable — no functions, no class instances. Store in any persistence layer.
  • resume() reuses the execution runtime — narrative, metrics, and execution tree all accumulate across the pause boundary.
  • A resume re-enters once, then the run is the chart as built. A loopTo back to the paused stage runs that stage again — it pauses again, so you can re-ask; a loop reaches its head even from inside a subflow; the next pass through a subflow is a fresh entry (its inputMapper runs); a pause several subflows deep does not re-run the outer subflows’ earlier stages; a subflow mounted as a decider or selector branch, or as a fork child, hands back to its parent’s continuation. Example: examples/runtime-features/pause-resume/09-ask-again-in-a-loop.ts (since 9.28.0).
  • Parallel siblings that both pause are asked in turn. The checkpoint asks the first child and carries the others in checkpoint.pendingPauses; each resume answers one, nothing re-runs, and the fork’s join runs after the last answer. Example: examples/runtime-features/pause-resume/10-two-questions-at-once.ts (since 9.28.0).
  • Not resumable yet: a pause inside a lazy subflow or an addParallelForEach branch (resume() refuses it), and a pause inside a subflow whose id is mounted twice in one chart (give each mount its own id).
  • FlowRecorder/Recorder both fire onPause/onResume events.
  • Pause/Resume — manager approval gate with live resume panel