Skip to content

Graphs

The Graph defines the deterministic structure of a workflow prescribing where nodes exist, how they connect, and the conditions under which edges are traversed. A graph can be cyclic or acyclic, depending on what the workflow needs.

import { agent, node, graph, run } from '@cycgraph/orchestrator';
const research = node({
id: 'research',
agent: agent({
model: 'claude-sonnet-4-6',
instructions: 'You are a research specialist. Produce concise, factual notes.',
}),
writes: 'notes',
});
const write = node({
id: 'write',
agent: agent({
model: 'claude-sonnet-4-6',
instructions: 'Turn the research notes into a clear summary under 300 words.',
}),
reads: [research.writes],
writes: 'draft'
});
const workflow = graph({
name: 'research-write',
nodes: [research, write],
edges: [{ from: research, to: write }],
});
const { draft } = await run(workflow, { goal: 'Explain how LLMs work' });

An Edge is a directed connection between a source node and a target node.

When a node completes, the orchestrator evaluates all outgoing edges from that node. The edge’s condition determines whether it is traversed.

If the node is not a declared end node and no outgoing edge’s condition matches, that’s a dead-end. The runner fails the run with NoMatchingEdgeError rather than silently treating it as completion. Make sure every non-terminal node has at least one edge whose condition can match, or list genuinely-terminal nodes in end nodes.

Type Description
from Node reference to “from” node.
to Node reference to “to” node.
when Optional conditional string
edges: [
// always follow this edge
{ from: research, to: review },
// conditionally follow this edge
{ from: review, to: research, when: 'memory.score < 0.7' },
],

Refs:

Graph structure is checked at two gates.

graph is the first. At compile time it rejects duplicate node ids, conflicting agent or tool definitions, and endpoints it cannot infer, throwing a GraphSpecError while the mistake is still in your editor. A graph it returns always has real, existing endpoints by construction.

validateGraph is the second, and it runs on every graph before execution no matter how the graph was authored. The graph runner invokes it when a run starts, and the architect invokes it before persisting a generated graph. It is the trust boundary for graphs that never went through the graph at all: definitions loaded from the database, generated by an LLM, or hand-built as raw JSON.

The validator checks referential integrity, meaning edges, endpoints, and config references such as a map node’s worker must all name real nodes. It compiles every conditional edge expression so a syntax error fails at load instead of silently misrouting mid-run. It walks the graph for unreachable nodes and dead ends, and it verifies each node type carries the config block its executor needs. The result separates errors, which block execution, from warnings for suspicious-but-valid shapes such as wildcard read keys or a declared read key that no node produces.

Refs:

A graph can declare a public signature: the memory keys it expects seeded and the keys it produces, each with a schema. Both are optional, and a graph without them composes exactly as it did before. Declare one when the graph is a contract other graphs depend on.

import { z } from 'zod';
const researchBlock = graph({
name: 'research-block',
nodes: [gather, summarize],
edges: [{ from: gather, to: summarize }],
inputs: { topic: z.string().min(3) },
outputs: { summary: z.string() },
});

You author the schemas as Zod and they serialize to JSON Schema, so a consumer holding nothing but the serialized graph still knows how to call it. This is what makes a declared interface useful across a boundary the authoring code does not cross, such as a graph loaded from the database or installed as a bundle.

The declaration only takes effect where the graph is used as a child. A subgraph node’s mappings are checked against it when the parent compiles, and values crossing the boundary are checked against the schemas at runtime in both directions. See Subgraph for both checks and the errors they raise.

Refs:

Compile authoring values into a wire the Graph. This is the facade counterpart of createGraph and emits exactly the same serializable result; it adds the authoring conveniences on top:

graph(spec: GraphSpec): Graph

Throws GraphSpecError instead of guessing: duplicate node ids, distinct agent definitions pinned to one id, distinct tool definitions sharing a name, or a start/end that can’t be inferred.

The input is a GraphSpec.

createGraph(input: GraphConfig): Graph

The input is a GraphConfig.

Run the structural validation pass on a built graph.

validateGraph(graph: Graph): ValidationResult

The authoring input to graph.

Field Type Default Description
name string required Human-readable name.
description string '' What this graph does.
nodes (NodeValue | NodeConfig)[] required node values.
edges EdgeInput[] [] { from, to, when? } sugar or structured edges, mixed freely. from/to accept node values or id strings.
startNode string | NodeValue inferred First node to execute. Required when more than one node has no inbound edge.
endNodes (string | NodeValue)[] inferred Terminal nodes. Required when every node has an outbound edge. Pass [] for graphs that end by supervisor completion.
strictTaint boolean false Reject routing decisions that reference tainted memory keys. See Taint Tracking.
inputs Record<string, GraphInputSpec> The memory keys this graph expects seeded. Authored as Zod schemas, raw JSON Schema, or full declaration entries, and projected to GraphInputDecl on the wire.
outputs Record<string, GraphOutputSpec> The memory keys this graph produces, projected to GraphOutputDecl on the wire.

Each entry in inputs and outputs is either a bare schema or an entry object:

inputs: {
topic: z.string().min(3),
depth: { schema: z.enum(['brief', 'deep']), description: 'How much detail' },
payload: { type: 'object', properties: { n: { type: 'number' } } },
}

Zod schemas are projected with z.toJSONSchema; raw JSON Schema passes through untouched. For inputs, required is derived rather than declared: a schema that accepts undefined, such as one that is .optional() or carries a .default(), is not required. Set required explicitly on an entry object to override that.

A workflow definition.

Field Type Default Description
id string (UUID) auto-generated Unique identifier for the graph definition.
name string required Human-readable name.
description string required What this graph does.
nodes GraphNode[] required The nodes that define the work. Capped at 10,000.
edges GraphEdge[] required Directed edges defining the flow of execution. Capped at 10,000.
startNode string required ID of the first node to execute.
endNodes string[] required Terminal node IDs. Execution stops when one is reached.
strictTaint boolean false When true, reject routing decisions that reference tainted memory keys instead of only warning. See Taint Tracking.
inputs Record<string, GraphInputDecl> The graph’s declared input keys. Absent when the graph declares no interface, and absent stays absent on the wire. Capped at 1,000 keys.
outputs Record<string, GraphOutputDecl> The graph’s declared output keys. Capped at 1,000 keys.

The declaration of one input key on a built graph. Produced from a GraphSpec inputs entry.

Field Type Default Description
schema Record<string, unknown> required JSON Schema for the value seeded under this key.
required boolean true Whether the key must be provided. Derived from the authored schema unless set explicitly.
description string? What the key is for. Surfaced to anyone reading the graph or a bundle manifest.

The declaration of one output key on a built graph.

Field Type Default Description
schema Record<string, unknown> required JSON Schema for the value the graph produces under this key.
description string? What the key contains.

A directed connection between two nodes.

Field Type Default Description
id string (UUID) auto-generated Unique edge identifier, used in validation messages and debug logs.
source string required Source node ID.
target string required Target node ID.
condition EdgeCondition { type: 'always' } Routing logic.
metadata Record<string, unknown> Arbitrary metadata for tooling and debugging.

The routing condition on an edge. See Edge conditions for what each type does.

Field Type Description
type 'always' | 'conditional' Routing strategy.
condition string? Filtrex expression, such as "memory.decision == 'A'". Required for conditional.
value unknown? Expected value for simple equality checks.

The result of a validateGraph pass.

Field Type Description
valid boolean true when errors is empty and the graph is safe to execute.
errors string[] Fatal issues that prevent execution.
warnings string[] Suspicious configurations that may indicate mistakes, such as unreachable nodes.