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 (dev drawer → 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 Desktop and Cursor; 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 executes exported flows in your own Node.js process — same engine as the hosted API. 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.