Example projects
Recipes for the integrations people build first. Each one is a complete, working shape — the essential code is inline here, ready to adapt.
Every hosted-API recipe needs two things from Workbench: a flow's Flow ID (Devtools → Quickstart) and an API key with the workbench:flows:run scope. Keys are secrets — they belong in server-side environment variables, never in browser code.
Run a flow from Node.js
Zero dependencies. Chat-shaped input for agent flows; swap kind: "form" with values keyed by your input nodes for data flows. (Full reference)
const res = await fetch(
`https://api.zerowidth.ai/1.0/flows/${process.env.FLOW_UUID}/runs`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZEROWIDTH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input: { kind: "chat", messages: [{ role: "user", content: "Hello!" }] },
source: { kind: "published", version: "1.0.0" }, // pin for production
}),
},
)
const { status, outputs, costSummary } = await res.json()
Run a flow from Python
import os, requests
res = requests.post(
f"https://api.zerowidth.ai/1.0/flows/{os.environ['FLOW_UUID']}/runs",
headers={"Authorization": f"Bearer {os.environ['ZEROWIDTH_API_KEY']}"},
json={"input": {"kind": "chat", "messages": [{"role": "user", "content": "Hello!"}]}},
timeout=120,
)
result = res.json()
print(result["outputs"])
A streaming chat app
Two pieces: a tiny server-side proxy that adds the API key and forwards the streamed events (so the key never reaches the browser), and a client that appends node_update token deltas as they arrive. The streaming guide covers the event protocol; the client side reduces to:
// POST { messages } to your proxy, which forwards to the run API
// with stream: true — then read the streamed frames:
if (type === "node_update") appendToUi(data.data?.content ?? "")
if (type === "run_complete") reconcile(data.outputs)
Resend the full message history each turn — that's the shape the Agent pattern's chat input expects.
Batch structured extraction
Point a Structurer flow at a whole CSV: a bounded worker pool posts one form-shaped run per row and writes JSONL — failures are captured per-row instead of stopping the batch.
const result = await runFlow({ kind: "form", values: { data: row.text } })
out.push(result.status === "error" ? { row: i, error: result.message } : { row: i, outputs: result.outputs })
Pin a published version so a mid-batch edit can't change the extraction.
Call workspace tools over MCP, from code
The MCP page covers Claude Code, Cursor, Codex and Claude Desktop; the same server works programmatically with the official MCP SDK:
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
const client = new Client({ name: "my-app", version: "1.0.0" })
await client.connect(
new StreamableHTTPClientTransport(new URL("https://api.zerowidth.ai/mcp"), {
requestInit: { headers: { Authorization: `Bearer ${process.env.ZEROWIDTH_PAT}` } },
}),
)
const { tools } = await client.listTools()
Run flows in your own infrastructure
The SDK runs an exported flow in your own Node.js process, the same way the hosted API would run it. Two shapes worth knowing:
Plain embed — your process, your provider keys:
import Workbench from "@zerowidth/workbench-sdk"
const engine = await Workbench.create(flow, {
keys: { openrouter: process.env.OPENROUTER_API_KEY },
})
const result = await engine.run({ query: "..." })
Bring your own database as a knowledge base — for corpora beyond a managed knowledge base's scale, implement the SDK's KnowledgeBaseInterface over your own store and inject it. Every knowledge node in the flow reads from your database; the flow itself doesn't change:
import Workbench, { KnowledgeBaseInterface } from "@zerowidth/workbench-sdk"
class MySqlKnowledge extends KnowledgeBaseInterface {
async keywordSearch(query, { limit = 10 } = {}) {
const rows = await myDb.query(
"SELECT id, title, body FROM articles WHERE body ILIKE $1 LIMIT $2",
[`%${query}%`, limit],
)
return rows.map((r, i) => ({
id: r.id, document_id: r.id, document_name: r.title,
chunk_index: i, content: r.body,
}))
}
async disconnect() { await myDb.end() }
}
const engine = await Workbench.create(flow, {
knowledgeBase: { instance: new MySqlKnowledge() },
})
Implement only the methods your flow's nodes use — semanticSearch against your vector store, query for the SQL node, listDocuments for browsing. Unimplemented methods return empty results, so flows degrade gracefully.