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.

refund-graph.ts
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

support-graph.ts
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 }.

agent-node.ts
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 text

agentNode 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.

hot-reload.ts
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 matches

Mismatch 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.

handle.ts
// 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);

FAQ