SWIRLS_
ReferenceSDK

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/sdk

Quick 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.

MethodDescription
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 rootNodeThe node with no incoming edges (single root).
get nodeListArray of all nodes.
get edgeListArray 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 / methodDescription
id, name, label, type, configNode data.
description, inputSchema, outputSchema, positionOptional fields.
incomingEdgesEdges whose target is this node.
outgoingEdgesEdges whose source is this node.
isRootNodetrue 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.

PropertyDescription
id, labelEdge data.
sourceSource NodeRef.
targetTarget NodeRef.

GraphValidationError

Thrown by validate(), addEdge(), addNode(), or setName() when the graph or input is invalid.

  • message: Human-readable message.
  • code: One of CYCLE_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.

TypeConfig 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 --> summarize

Learn more

On this page