How Wire thinks
Five primitives describe the whole library. Internalise them once and the API maps cleanly.
Wire is a canonical JSON diagram, a pure reducer over actions, a small event surface, a typed option catalog, and replaceable render callbacks. Everything else — the editor shell, the panels, the playground — is built from these.
Diagram JSON
A WireDiagram is the source of truth. Nodes carry kind, title, position, size, and a free-form data bag. Edges either come from a node’s from shorthand or appear as explicit edge objects when you need handles, labels, or routing.
{
"version": 1,
"id": "support-agent",
"title": "Support agent",
"layout": "LR",
"nodes": [
{ "id": "webhook", "kind": "trigger", "title": "Ticket webhook" },
{ "id": "plan", "kind": "ai", "title": "Plan answer",
"from": "webhook", "model": "gpt-5.4-mini" },
{ "id": "respond", "kind": "action", "title": "Send response",
"from": "plan", "tone": "success" }
],
"edges": []
}The schema lives in @aigentive/wire-core as Zod. Validation runs both inside the editor and at the network boundary, so an LLM that emits a bad node is told exactly which field is wrong.
Node kinds
Twelve kinds cover the surface. Pick the one whose semantics match what you’re modelling — the rendering is consistent across them, but a custom card can specialize per-kind via renderNodeCard.
| Kind | JSX equivalent | Purpose |
|---|---|---|
| trigger | <TriggerNode> | Entry point of a flow (webhook, schedule, manual). |
| action | <ActionNode> | Side-effect step — call an API, send a notification. |
| ai | <AINode> | LLM call. Carries `model`, `prompt`, optional `tools`. |
| tool | <ToolNode> | Tool invocation referenced by `ref` (e.g. `zendesk.update_ticket`). |
| condition | <ConditionNode> | Branching node. Requires `branches: string[]`. |
| human | <HumanNode> | Manual approval / human-in-the-loop. |
| memory | <MemoryNode> | Read/write to a persistent store. |
| retrieval | <RetrievalNode> | RAG / vector lookup step. |
| guardrail | <GuardrailNode> | Policy/safety check before downstream steps run. |
| end | <EndNode> | Terminal node — explicit flow end. |
| note | <Note> | Annotation; uses `attachedTo` for visual association. |
| group | <Group> | Container; wraps children via `parent` linkage. |
Actions
Every edit — human, agent, MCP, undo — is a WireAction. The reducer in wire-core is pure: (diagram, action) → diagram. React, MCP, and CLI all call the same function.
type WireAction =
| { type: "node.add"; node: WireNode }
| { type: "node.patch"; id: string; patch: Partial<WireNode> }
| { type: "node.remove"; id: string }
| { type: "node.move"; id: string; position: Point }
| { type: "node.resize"; id: string; size: Size }
| { type: "edge.connect"; edge: WireEdge }
| { type: "edge.patch"; id: string; patch: Partial<WireEdge> }
| { type: "edge.disconnect"; from: string; to: string }
| { type: "edge.remove"; id: string }
| { type: "diagram.patch"; patch: Partial<WireDiagram> }
| { type: "metadata.patch"; patch: Record<string, unknown> }
| { type: "layout.apply"; layout: "LR" | "TB" }
| { type: "group.add"; id: string; children: string[] }
| { type: "group.ungroup"; id: string }
| { type: "note.add"; note: WireNote };Events
Actions describe data changes; events describe UI gestures. The two are deliberately separate so you can keep an inspector synced to selection without coupling it to card rendering.
type WireEvent =
| { type: "node.click"; source: WireEventSource; nodeId: string }
| { type: "node.inspect"; source: WireEventSource; nodeId: string }
| { type: "edge.click"; source: WireEventSource; edgeId: string }
| { type: "selection.change"; source: WireEventSource; selection: WireSelection }
| { type: "pane.click"; source: WireEventSource };
type WireEventSource =
| "canvas" | "node-card" | "node-list"
| "option-panel" | "validation-panel" | "workspace" | "api";Built-in React components currently emit from canvas and node-list. The other source labels are reserved for custom cards, panels, workspace wrappers, and programmatic integrations. See Listen for the full surface.
Validation
validate(diagram) returns a flat list of issues. Each issue carries a stable code, a severity, and (for most codes) a repair hint. The same call runs in the playground, in MCP requests, and in the CLI — so an LLM that emits a bad diagram gets actionable feedback instead of a generic error.
| Code | Severity | Meaning |
|---|---|---|
| schema.* | error | Zod schema rejection (top-level shape, missing required fields, etc.). |
| node.duplicate-id | error | Two nodes share the same id. |
| node.attached-to-missing | error | `attachedTo` points to a missing node. |
| node.parent-missing | error | `parent` points to a missing node. |
| node.parent-not-group | error | `parent` points to a node that isn't a `group`. |
| condition.no-branches | error | Condition node has no `branches`. |
| condition.duplicate-branch | error | Repeated branch name on a condition. |
| edge.from-missing | error | Source node referenced by an edge does not exist. |
| edge.to-missing | error | Target node referenced by an edge does not exist. |
| edge.branch-from-non-condition | error | `id.branch` syntax used from a non-condition source. |
| edge.unknown-branch | error | Branch name not declared on the source condition. |
| node.orphan | warning | Node has no incoming or outgoing edges. |
| edge.self-loop | warning | Node references itself. |
Options
A WireOptionCatalog declares per-kind option specs — type, range, choices, storage location. The option panel renders the right control for each spec and dispatches node.patch actions on edit. No imperative form code.
const options: WireOptionCatalog = {
"*": [{ key: "owner", storage: "data" }],
ai: [
{ key: "model", storage: "node", type: "select",
options: ["gpt-5.4-mini", "gpt-4.1-mini", "o4-mini"] },
{ key: "temperature", type: "number", min: 0, max: 2, step: 0.1 },
{ key: "maxSteps", type: "number", min: 1, max: 20 }
],
retrieval: [
{ key: "index", type: "text" },
{ key: "topK", type: "number", min: 1, max: 20 }
]
};storage chooses where the value lives: "data.options" (default — hidden from the schema-typed surface), "data" (a top-level field on node.data), or "node" (a typed field on the node itself, like model).
Renderers
Default cards and group frames already follow the visual rules. When you need something different, pass renderNodeCard and/or renderGroup. Each receives a WireNodeRenderContext — the same context the default uses.
function Card(ctx: WireNodeRenderContext) {
return (
<div aria-selected={ctx.selected}>
<span className="kind">{ctx.kind}</span>
<strong>{ctx.node.title}</strong>
{ctx.optionSpecs.map((spec) => (
<span key={spec.key}>{spec.label}: {String(ctx.options[spec.key])}</span>
))}
</div>
);
}
<WireWorkspace renderNodeCard={Card} renderGroup={GroupFrame} />;