Skip to main content

Graphs

A graph has three parts:

  • Channels: the run's state. Each has a name, a type label and a default value.
  • Nodes: the steps. A node reads the state and returns the channels it changes.
  • Edges: the order of the steps.
import { createGraph } from "@ailu-ai/graph-sdk";

const app = createGraph({ name: "greeter" })
// Channels are the run's state: a name, a type and a default.
.channel("name", { type: "string", default: "" })
.channel("greeting", { type: "string", default: "" })
// A node reads the state and returns the channels it changes.
.node("greet", async (_input, state) => ({ greeting: `Hello, ${state.channels.name}!` }))
.node("shout", async (_input, state) => ({ greeting: state.channels.greeting.toUpperCase() }))
// Edges set the order. The first node added is the entry.
.edge("greet", "shout")
.compile();

const result = await app.run({ name: "Ada" });
console.log(result.status); // "completed"
console.log(result.channels.greeting); // "HELLO, ADA!"

createGraph returns a builder. compile() checks the graph (unknown nodes, missing handlers, dangling edges) and returns a CompiledGraph you can run() many times. Each run gets its own state and its own runId.

The first node you add is the entry. To start elsewhere, call .entry("nodeId").

Read and write state​

A node handler receives (input, state). Read channels from state.channels; they are typed from your .channel() declarations. Return an object with the channels to update. Return {} to change nothing.

run(data) seeds channels from data, and every channel you don't pass starts at its default. run() resolves with the final state: status, runId, currentNodeId and channels.

Route with conditions​

A conditional edge is taken when its predicate returns true. The predicate has a name, so the graph stays data you can inspect, store and render; the function stays in your code.

import { createGraph } from "@ailu-ai/graph-sdk";

const app = createGraph({ name: "triage" })
.channel("amount", { type: "number", default: 0 })
.channel("route", { type: "string", default: "" })
.node("intake", async () => ({}))
.node("auto", async () => ({ route: "auto-approved" }))
.node("manual", async () => ({ route: "sent to a human" }))
// A conditional edge is taken when its named predicate returns true.
.conditionalEdge("intake", "manual", "isLarge", (state) => state.channels.amount > 1000)
.conditionalEdge("intake", "auto", "isSmall", (state) => state.channels.amount <= 1000)
.compile();

console.log((await app.run({ amount: 50 })).channels.route); // "auto-approved"
console.log((await app.run({ amount: 5000 })).channels.route); // "sent to a human"

When a node finishes, the engine looks at its outgoing edges in the order you added them and follows the first one that applies: a plain edge, or a conditional edge whose predicate returns true. If none applies, the run ends there. A predicate that throws fails the run rather than silently taking another branch.

Combine updates with reducers​

By default a node's value replaces the channel's value. A reducer changes that:

ReducerEffect
replace (default)The new value replaces the old one.
appendThe value is added to the end of the list; an array adds each of its items. Useful for logs and messages.
mergeThe object's keys are written over the existing object, one level deep.
import { createGraph } from "@ailu-ai/graph-sdk";

const app = createGraph({ name: "audit-log" })
// "append" adds each update to the list instead of replacing it.
.channel("log", { type: "string[]", reducer: "append", default: [] as string[] })
.node("open", async () => ({ log: ["opened"] }))
.node("check", async () => ({ log: ["checked"] }))
.node("close", async () => ({ log: ["closed"] }))
.edge("open", "check")
.edge("check", "close")
.compile();

console.log((await app.run()).channels.log); // ["opened", "checked", "closed"]

Retry, then branch on failure​

A node can retry before it fails the run. An error edge sends the run somewhere else once the retries are spent, instead of failing it.

import { createGraph } from "@ailu-ai/graph-sdk";

let calls = 0;
const app = createGraph({ name: "flaky-call" })
.channel("result", { type: "string", default: "" })
// Retry a failing node up to 3 times, 100 ms apart...
.node("fetch", {
retryPolicy: { maxAttempts: 3, backoffMs: 100 },
handler: async () => {
calls += 1;
throw new Error("upstream unavailable");
}
})
// ...then take the error edge instead of failing the run.
.node("fallback", async () => ({ result: "served from cache" }))
.errorEdge("fetch", "fallback")
.compile();

const out = await app.run();
console.log(out.status, out.channels.result); // "completed" "served from cache"

A thrown error that is not caught by an error edge fails the run: status is "failed" and a run_failed event says why.

Keep a channel out of logs​

Mark a channel noLog: true and its value is masked in every run event and log. It is still saved in checkpoints, so the run can resume. See Governance.

Next​