Skip to main content

Getting started

Build a graph that pauses for a human and resumes without redoing its work — in about five minutes. Pick your language once with the tabs below; the choice follows you across every page.

Install​

npm install @ilmek/core

@ilmek/core has zero runtime dependencies. Durable threads that outlive the process are opt-in via a checkpointer package:

npm install @ilmek/checkpoint-sqlite # single-process, one file
# or
npm install @ilmek/checkpoint-postgres # threads shared across processes

Requires Node ≥ 22.5 for the SQLite checkpointer (built-in node:sqlite).

Your first graph​

A graph is a set of channels (the state), nodes (the work), and edges (the flow). The state is typed: it lives in one named place, so a node body and the update it returns are both checked against it.

import { graph, channel, START, END, run } from "@ilmek/core";
import type { ChannelMap, StateOf } from "@ilmek/core";

/** Support state — one append-only channel of messages. */
const SupportState = {
messages: channel.append<string>(),
} satisfies ChannelMap;

type SupportState = StateOf<typeof SupportState>; // { messages: string[] }

const g = graph("support", SupportState)
.node("agent", async (state, ctx) => ({ messages: ["hi"] })) // state.messages: string[]
.edge(START, "agent")
.edge("agent", END)
.compile();

const { status, state } = await run(g, { messages: ["hi"] });
console.log(status, state.messages); // "done" [ "hi", "hi" ]

Prefer events? The same run is one canonical stream:

import { stream } from "@ilmek/core";

for await (const ev of stream(g, { messages: ["hi"] })) console.log(ev);

The move that matters​

Wrap side effects in a step. On the resume pass, a completed step returns its journaled value instead of running again — and a node can interrupt to pause for a human, given a durable thread (a threadId + a checkpointer).

import { graph, channel, START, END, run, resume, InMemoryCheckpointer } from "@ilmek/core";
import type { ChannelMap, StateOf } from "@ilmek/core";

// Stand-in services — swap for your real order + payment code.
const Orders = { create: (cart: string[]) => ({ id: "ord-9001", total: 249.9 }) };
const Payments = { charge: (order: { id: string; total: number }, ok: string) => "charged" };

/** Checkout state — the cart in, an append-only log out. */
const CheckoutState = {
cart: channel.lastWrite<string[]>([]),
log: channel.append<string>(),
} satisfies ChannelMap;

type CheckoutState = StateOf<typeof CheckoutState>;

const g = graph("checkout", CheckoutState)
.node("checkout", async (state, ctx) => {
/** Called once, ever. On resume this returns the journaled order. */
const order = await ctx.step("create_order", () => Orders.create(state.cart));

// First pass halts here; resume pass returns the human's answer.
const ok = await ctx.interrupt<string>({ question: `Charge ${order.total}?` });

await ctx.step("charge", () => Payments.charge(order, ok));
return { log: ["done"] };
})
.edge(START, "checkout").edge("checkout", END)
.compile();

const opts = { threadId: "conv-42", checkpointer: new InMemoryCheckpointer() };
const paused = await run(g, { cart: ["coffee", "mug"] }, opts); // status: "interrupted"
const done = await resume(g, "yes", opts); // status: "done"

InMemoryCheckpointer is enough to feel the model. For a pause that survives a process restart, swap it for SQLite or Postgres — nothing else in your code changes. And resume from the line means exactly this: not a restored call stack, but an effect that cannot happen twice.

Run the demos​

Clone the repo and watch it happen — pause, answer, and see create_order run exactly once:

cd ts && pnpm build
pnpm --filter @ilmek/examples demo # interactive checkout
pnpm --filter @ilmek/examples demo:stream # tokens streaming + a mid-stream cancel
pnpm --filter @ilmek/examples demo:mapreduce # fan-out + routing + safe retry

Next​

  • Concepts — why replay is invisible, and the contract that keeps it that way.
  • Interrupts & resume — multiple pauses, loops, and the id vs key rule you should know up front.