Graph Builder
Build and manipulate Swirls workflow graphs in code with @swirls/sdk/graph.
Build and manipulate Swirls workflow graphs in code with a fluent API. Define nodes and edges locally, validate structure (DAG, single root), then persist to the Swirls API or serialize for storage.
Installation
bun add @swirls/sdk
# or
npm install @swirls/sdkQuick start
import { Swirls } from '@swirls/sdk/client'
import { GraphBuilder } from '@swirls/sdk/graph'
const swirls = new Swirls({ apiKey: process.env.SWIRLS_API_KEY! })
const graph = new GraphBuilder({ client: swirls.client })
.setName('my_workflow')
.setLabel('My Workflow')
.setDescription('Fetches data and summarizes with LLM')
.addNode('fetch_data', {
type: 'http',
label: 'Fetch Data',
config: {
type: 'http',
url: 'https://api.example.com/data',
method: 'GET',
},
})
.addNode('summarize', {
type: 'ai',
label: 'Summarize',
config: {
type: 'ai',
kind: 'text',
model: 'gpt-4o',
// TypeScript function body: context.nodes holds upstream node outputs (typed when you set outputSchema)
prompt: 'return `Summarize the following: ${context.nodes.fetch_data.output}`',
},
})
.addEdge({ source: 'fetch_data', target: 'summarize' })
await graph.save({ projectId: 'your-project-id' })
const executionId = await graph.execute({ input: {} })API reference
GraphBuilder
Main entry. Create a new graph or load an existing one.
| Method | Description |
|---|---|
constructor(options?: { client?: SwirlsClient }) | New builder. Pass client (e.g. swirls.client) to enable save() and execute(). Use SwirlsClient from @swirls/sdk/client. |
setName(name) | Set graph name (must match ^[a-zA-Z0-9_]+$). Returns this. |
setLabel(label) | Set display label. Returns this. |
setDescription(desc) | Set optional description. Returns this. |
addNode(name, options) | Add a node by name. Options: type, label, config, optional description, position, inputSchema, outputSchema, reviewConfig. Returns this. |
addEdge({ source, target, label? }) | Add edge by node names. Returns this. |
getNode(name) | Get NodeRef by name, or undefined. |
removeNode(name) | Remove node and its edges. Returns this. |
removeEdge({ source, target }) | Remove edge by source/target names. Returns this. |
get rootNode | The node with no incoming edges (single root). |
get nodeList | Array of all nodes. |
get edgeList | Array of all edges. |
validate() | Throws GraphValidationError if not a valid DAG (cycle, multiple roots, invalid refs). |
save({ projectId, folderId? }) | Create graph (if new) and sync nodes/edges via API. Requires client. |
saveExisting({ projectId, folderId? }) | Sync only (for graphs loaded via fromAPI). |
execute(input?) | Run the graph; returns executionId. Requires client. |
toJSON() | Serialize to a plain object. |
toMermaid() | Return a Mermaid flowchart LR string. |
static fromJSON(json) | Deserialize from object or JSON string. |
static fromAPI(client, graphId) | Load existing graph from the API. |
NodeRef
Reference to a node. Obtained via graph.getNode(name).
| Property / method | Description |
|---|---|
id, name, label, type, config | Node data. |
description, inputSchema, outputSchema, position | Optional fields. |
incomingEdges | Edges whose target is this node. |
outgoingEdges | Edges whose source is this node. |
isRootNode | true when there are no incoming edges. |
update(options) | Update label, config, position, etc. Returns this. |
EdgeRef
Reference to an edge. Obtained via graph.edgeList or node.incomingEdges / node.outgoingEdges.
| Property | Description |
|---|---|
id, label | Edge data. |
source | Source NodeRef. |
target | Target NodeRef. |
GraphValidationError
Thrown by validate(), addEdge(), addNode(), or setName() when the graph or input is invalid.
message: Human-readable message.code: One ofCYCLE_DETECTED,NO_ROOT_NODE,MULTIPLE_ROOT_NODES,INVALID_SOURCE_NODE,INVALID_TARGET_NODE,SELF_LOOP,DUPLICATE_NODE_NAME,INVALID_NODE_NAME.details: Optional object with context (e.g.{ name },{ sourceNodeId }).
Node types and config
Node type and config are a discriminated union. config must include type matching the node type.
| Type | Config shape |
|---|---|
ai | { type: 'ai', kind: 'text'|'object'|'image'|'video'|'embed', model?, prompt?, temperature?, maxTokens?, options? } — for text/object, prompt is a TypeScript function body; use context.nodes for upstream outputs. |
code | { type: 'code', code? } — code is a TypeScript function body that receives context and returns the node output. |
http | { type: 'http', url?, method?: 'GET'|'POST'|'PUT'|'DELETE'|'PATCH', headers?, body? } |
stream | { type: 'stream', streamId?, query?: { filter?, sort?, limit?, offset? } } |
email | { type: 'email', from?, to?, subject?, text?, html?, replyTo? } |
scrape | { type: 'scrape', url?, onlyMainContent?, formats?, maxAge?, parsers? } |
wait | { type: 'wait', amount?, unit?: 'seconds'|'minutes'|'hours'|'days', secondsFromConfig? } |
document | { type: 'document', documentId?: uuid } |
graph | { type: 'graph', graphId?: uuid } (subgraph) |
bucket | { type: 'bucket', operation: 'download'|'upload', path? } |
switch | { type: 'switch', cases: string[], router? } (case names must match ^[a-zA-Z0-9_]+$) |
For AI, code, switch, and other nodes that accept dynamic values, config fields (e.g. prompt, code, router, url, body) use TypeScript function bodies. You provide only the body of the function; the runtime injects a context object (nodes, reviews, secrets, meta). When you define inputSchema / outputSchema on nodes, context.nodes is typed accordingly for intellisense and type checking.
Example — AI prompt (function body only):
prompt: 'return `Summarize in one paragraph: ${context.nodes.fetch_data.output}`'Example — code node (function body only):
code: 'return { summary: String(context.nodes.fetch_data.output).slice(0, 500), source: "fetch_data" }'Use context.nodes.<node_name> to read upstream node outputs; the shape follows upstream nodes’ outputSchema when set.
Input and output schemas
You can attach inputSchema and outputSchema (JSON Schema) to nodes. They drive execution input validation, structured AI output, and typing of context.nodes in TypeScript functions.
Graph execution input (root node inputSchema)
The root node’s inputSchema defines the shape of the object you pass to graph.execute({ input }). Execution validates the payload against this schema before running.
graph.addNode('entry', {
type: 'http',
label: 'Entry',
config: { type: 'http', url: 'https://api.example.com' },
inputSchema: {
type: 'object',
required: ['topic'],
properties: {
topic: { type: 'string', description: 'Topic to fetch' },
limit: { type: 'number', default: 10 },
},
additionalProperties: false,
},
})
// Later: await graph.execute({ input: { topic: 'AI', limit: 5 } })Node outputSchema (downstream inputs and structured AI output)
A node’s outputSchema describes the shape of that node’s output. Downstream nodes see this shape under context.nodes.<node_name>. For AI nodes with kind: 'object', setting outputSchema also enables structured output (the model is constrained to return JSON matching the schema).
graph.addNode('summarize', {
type: 'ai',
label: 'Summarize',
config: {
type: 'ai',
kind: 'object',
model: 'gpt-4o',
prompt: 'return `Summarize this in JSON with "summary" and "keywords" keys: ${JSON.stringify(context.nodes.fetch_data.output)}`',
},
outputSchema: {
type: 'object',
required: ['summary', 'keywords'],
properties: {
summary: { type: 'string' },
keywords: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
})Example: full flow with schemas
const graph = new GraphBuilder({ client: swirls.client })
.setName('content_pipeline')
.setLabel('Content Pipeline')
.addNode('fetch', {
type: 'http',
label: 'Fetch',
config: { type: 'http', url: 'https://api.example.com/article', method: 'GET' },
inputSchema: {
type: 'object',
required: ['articleId'],
properties: { articleId: { type: 'string' } },
additionalProperties: false,
},
})
.addNode('summarize', {
type: 'ai',
label: 'Summarize',
config: {
type: 'ai',
kind: 'object',
model: 'gpt-4o',
prompt: 'return `Summarize as JSON with "summary" and "keywords": ${JSON.stringify(context.nodes.fetch.output)}`',
},
outputSchema: {
type: 'object',
required: ['summary', 'keywords'],
properties: {
summary: { type: 'string' },
keywords: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
})
.addEdge({ source: 'fetch', target: 'summarize' })
await graph.execute({ input: { articleId: '123' } })Serialization and loading
Offline / version control: Build a graph, call graph.toJSON() and store the result (e.g. in your DB or a file). Later, use GraphBuilder.fromJSON(stored) to get a builder. You can pass a string (JSON) or the parsed object.
Load from API: Use GraphBuilder.fromAPI(swirls.client, graphId) to load an existing graph. Modify it, then call graph.saveExisting({ projectId }) to sync back (or save() for the create-then-sync flow on new graphs).
Validation
Graphs must be a DAG with exactly one root node (no incoming edges). Before save() or execute(), call validate() to catch:
- Cycles
- Multiple roots
- Missing root
- Self-loops
- Edges referencing missing nodes
- Duplicate node names
- Invalid resource names (only
[a-zA-Z0-9_])
Mermaid diagrams
Use graph.toMermaid() to get a Mermaid flowchart LR string for documentation or previews.
const mermaid = graph.toMermaid()
// flowchart LR
// fetch_data["Fetch Data"]
// summarize["Summarize"]
// fetch_data --> summarizeLearn more
- Swirls Documentation
- Client documentation – API client and TanStack Query