Skip to main content

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 stepsIn the CompiledGraph, in memoryReturned to you as plain JSON
Resumeapp.resume(runId), same instance, same processresumeCatalogGraph(definition, state), anywhere
Nodes that runAllAgents, components, human gates, subgraphs, mapAgents. Your own .node() functions and conditional-edge functions do not run.
AlsoStreaming, timers and signalsCancellation, 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​