Long-running runs
A run can stop for a person, a date or an external event, and continue later. This page shows where the state lives while it waits, and how to pick it up again.
Two ways to run a graph
app.run() | runCatalogGraph(app.definition) | |
|---|---|---|
| State between steps | In the CompiledGraph, in memory | Returned to you as plain JSON |
| Resume | app.resume(runId), same instance, same process | resumeCatalogGraph(definition, state), anywhere |
| Nodes that run | All | Agents, components, human gates, subgraphs, mapAgents. Your own .node() functions and conditional-edge functions do not run. |
| Also | Streaming, timers and signals | Cancellation, record and replay |
Use app.run() while a run finishes within one process. Use the catalog runner when a run must
survive a restart or move between machines.
Resume in another process
runCatalogGraph returns the state when the run stops. Store it anywhere that holds JSON, and
hand it to resumeCatalogGraph later:
import { createGraph, model, resumeCatalogGraph, runCatalogGraph, type GraphState } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "contract-review" })
.channel("contract", { type: "string", default: "" })
.agentNode("review", { model: model.balanced, prompt: { system: "List the risky clauses." } })
.humanGate("legal-sign-off")
.agentNode("summarize", {
model: model.fast,
prompt: { system: "Summarize the review for the customer." },
outputChannel: "summary"
})
.edge("review", "legal-sign-off")
.edge("legal-sign-off", "summarize")
.compile();
// Process A: run until the gate, then save the state (a database row, a file, a queue message).
const paused = await runCatalogGraph(app.definition, { initialData: { contract: "..." } });
console.log(paused.status); // "suspended"
const saved = JSON.stringify(paused.state);
// Process B, hours later, maybe on another machine: load the state and resume.
const done = await resumeCatalogGraph(app.definition, JSON.parse(saved) as GraphState);
console.log(done.status); // "completed"
To resume a tool approval this way, pass the approved tools in the resume options:
resumeCatalogGraph(definition, state, { approvedTools: [{ name: "refund", requestedBy: "assistant", resolvedBy: "alice@example.com" }], tools, approvalEngine }).
Pass your tool handlers again (tools) on every call: they are code, not state. With
approvalEngine, the resume first checks that the engine approved what the run waits on (see
Governance).
Wait for an external event
A node can suspend the run until your system delivers a named signal: a payment webhook, a message from another service, a manual action.
import { createGraph, readSignal, waitForSignal } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "order" })
.channel("paid", { type: "boolean", default: false })
// The run suspends here until your system delivers the "payment" signal.
.node("await-payment", async () => waitForSignal("payment"))
.node("ship", async (_input, state) => ({ paid: readSignal(state, "payment") !== undefined }))
.edge("await-payment", "ship")
.compile();
const waiting = await app.run();
console.log(waiting.status); // "suspended"
// Later, a webhook arrives: deliver the signal and the run continues.
const done = await app.signal(waiting.runId, "payment", { amount: 42 });
console.log(done.status, done.channels.paid); // "completed" true
waitForSignal(name, { wakeAt }) also wakes the run at wakeAt if the signal never comes.
Wait until a date
sleepUntil(date) suspends the run and records when to wake it. Ailu does not keep a clock
running: your scheduler (a cron job, a delayed queue message) calls resume at that time.
import { createGraph, readSuspendMeta, sleepUntil } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "reminder" })
.channel("sent", { type: "boolean", default: false })
// Suspend until a date. The engine records when to wake up; your scheduler resumes the run.
.node("wait", async () => sleepUntil("2030-01-01T09:00:00Z"))
.node("remind", async () => ({ sent: true }))
.edge("wait", "remind")
.compile();
const sleeping = await app.run();
console.log(readSuspendMeta(sleeping)); // { reason: "timer", wakeAt: "2030-01-01T09:00:00Z" }
// At wakeAt, your scheduler (cron, queue, ...) resumes the run.
const done = await app.resume(sleeping.runId);
console.log(done.channels.sent); // true
readSuspendMeta(state) tells you why a run is suspended: human-gate, interrupt (a tool
approval), timer or signal, with wakeAt or awaitingSignal.
Cancel a run
Pass an AbortSignal to the catalog runner. Aborting it stops the run at the next step: the
node in flight finishes, its checkpoint is saved, and the run ends with status "cancelled".
A cancelled run can still be resumed or replayed.
import { createGraph, model, runCatalogGraph } from "@ailu-ai/graph-sdk";
const app = createGraph({ name: "long-job" })
.agentNode("step1", { model: model.fast, prompt: { system: "Plan the job." }, outputChannel: "plan" })
.agentNode("step2", { model: model.fast, prompt: { system: "Do the job." }, outputChannel: "work" })
.edge("step1", "step2")
.compile();
// Abort the controller to stop the run. The engine finishes the node in flight,
// saves its checkpoint, and returns with status "cancelled".
const controller = new AbortController();
const outcome = await runCatalogGraph(app.definition, {
signal: controller.signal,
onEvent: (event) => {
if (event.type === "node_completed") controller.abort(); // e.g. the user clicked Stop
}
});
console.log(outcome.status); // "cancelled"
Next
- A complete example: Resume across processes.
- Record a run to replay it later: Governance.