Graphs
Primary orchestration API. Nodes, conditional edges, interrupts, cycle limits, and checkpoints. State advances in Rust. Node execute and edge predicates run in TypeScript.
Linear graph
If you omit edge(), Monorch wires nodes in declaration order to GRAPH_END.
import { graph, memorySaver } from "@monorch/ai";
const refund = graph("refund")
.node("lookup", async ({ input }) => ({
output: `order:${input.orderId}`,
state: { orderId: input.orderId },
}))
.interrupt("approve", { prompt: "Approve refund?" })
.node("pay", async ({ outputs }) => `refunded:${outputs.lookup}`)
.compile({ checkpointer: memorySaver(), maxSteps: 32 });
const run = await refund.start({ orderId: "ord_9" }, { threadId: "customer-42" });
if (run.status === "waitingInterrupt") {
await run.resume("approved");
}Branching and cycles
import { GRAPH_END, graph } from "@monorch/ai";
const supportGraph = graph("support_graph")
.node("classify", async ({ input, state }) => {
const text = String(input.text ?? state.text ?? "");
const intent = text.includes("refund") ? "refund" : "faq";
return { output: intent, state: { intent, text, hops: Number(state.hops ?? 0) } };
})
.node("refund_path", async ({ state }) => `refund:${state.text}`)
.node("faq", async ({ state }) => {
const hops = Number(state.hops ?? 0) + 1;
return {
output: `faq:${state.text}`,
state: { hops, needsRetry: hops < 2 },
};
})
.edge("classify", "refund_path", (ctx) => ctx.state.intent === "refund")
.edge("classify", "faq", (ctx) => ctx.state.intent !== "refund")
.edge("faq", "classify", (ctx) => ctx.state.needsRetry === true)
.edge("faq", GRAPH_END, (ctx) => ctx.state.needsRetry !== true)
.edge("refund_path", GRAPH_END)
.compile({ maxSteps: 16 });Node kinds
node for tasks, agentNode to call a registered agent, interrupt for human or external gates. Node functions may return a string, void, or { output, state }.
import { agent, graph } from "@monorch/ai";
agent({ name: "writer", model, instructions: "Draft a short reply." });
const draft = graph("draft")
.node("prep", async ({ input }) => ({ state: { topic: input.topic } }))
.agentNode("write", "writer", (ctx) => `Write about ${ctx.state.topic}`)
.compile();
const run = await draft.start({ topic: "refunds" });
// write output is the agent textagentNode calls agent.run(prompt) with the prompt string only — it does not forward the graph threadId, AbortSignal, or ThreadMemory. For threaded or abortable agent steps, use a plain node that calls getAgent(...).run(input, opts) yourself.
Compile options
checkpointer, maxSteps (default 64), replace to hot-reload a definition. In-flight runs with an old def hash fail after replace.
const v1 = graph("hot")
.node("prep", async () => "v1")
.interrupt("hold")
.compile({ replace: true });
const run = await v1.start({ n: 1 }); // waitingInterrupt
graph("hot")
.node("prep", async () => "v2")
.interrupt("hold", { prompt: "changed?" })
.compile({ replace: true });
await run.resume("approved"); // throws — defHash no longer matchesMismatch and bad resume surface as GRAPH_FAILED / GRAPH_RESUME_INVALID. Recovery steps: Errors & failure modes.
Run handle
drive() advances until done, interrupt, or failure. Calling drive() again while waitingInterrupt is idempotent: it re-emits wait instead of failing. Use resume(decision) to continue.
// status: pending | running | waitingInterrupt | needsRoute | completed | failed
run.drive();
run.resume(decision?);
run.stream(); // AiEvent async generator
run.checkpoint(); // blob for checkpointer.put
refund.restore(threadId);