@nodetool-ai/workflow-runner runs a workflow graph without the NodeTool server. It wraps the kernel WorkflowRunner in two things: an async generator that yields live messages, and a (Request) => Response handler that streams those messages as Server-Sent Events. Both use only Web-standard APIs, so the same code runs on serverless and edge platforms. A separate ./browser entry runs graphs inside a web page.
The runner has no file system, subprocess, or transport assumptions. You supply the graph, a node registry, and optionally a processing context. Storage, secrets, and the cache come in through the context.
Install
npm install @nodetool-ai/workflow-runner
The package needs Node 22 or newer when it runs on Node. It exports two entries:
| Entry | Use |
|---|---|
@nodetool-ai/workflow-runner |
runWorkflow, createWorkflowHandler, envSecretResolver, and the browser helpers |
@nodetool-ai/workflow-runner/browser |
The browser helpers, built for page bundles |
Exported symbols
| Symbol | What it does |
|---|---|
runWorkflow(options) |
Runs a graph and yields each ProcessingMessage live. The generator’s return value is the final RunResult |
createWorkflowHandler(options) |
Returns a (Request) => Promise<Response> handler that streams a run as Server-Sent Events |
envSecretResolver(env) |
Turns an environment-variable object into a secret resolver for a context |
createBrowserRegistry(nodeClasses) |
Builds a registry that keeps only classes that support the browser platform |
runBrowserWorkflow(options) |
Like runWorkflow, checked against the browser platform, with every message stamped with job_id and workflow_id |
graphRunsInRegistry(graph, registry) |
True when every node in the graph is in the registry, so the graph can run client-side |
Build a registry
A registry decides which nodes the deployment can run. Register the node classes your graphs use, then optionally filter for a platform:
import { NodeRegistry } from "@nodetool-ai/node-sdk";
import { CONSTANT_NODES, CONTROL_NODES } from "@nodetool-ai/core-nodes";
const registry = new NodeRegistry();
for (const nodeClass of [...CONSTANT_NODES, ...CONTROL_NODES]) {
registry.register(nodeClass);
}
const edgeRegistry = registry.forPlatform("edge");
A graph is a list of nodes and edges. Each node has an id, a type, and properties. A name on a node names its entry in the result outputs.
Run a workflow in Node
Call runWorkflow and read the messages:
import { runWorkflow } from "@nodetool-ai/workflow-runner";
const graph = {
nodes: [
{
id: "s",
type: "nodetool.constant.String",
name: "text",
properties: { value: "hello" }
}
],
edges: []
};
const run = runWorkflow({ graph, registry });
let next = await run.next();
while (!next.done) {
console.log(next.value.type);
next = await run.next();
}
console.log(next.value.status, next.value.outputs);
for await also works, but it discards the final RunResult. Pass params to set the values of input nodes, signal to cancel the run, and workflowId or jobId to label it.
runWorkflow builds a minimal context unless you pass one. To supply your own pieces without building a context, pass storage, workspaceStorage, cache, environment, secretResolver, or sandboxModuleCatalog. These are ignored when you pass a full context.
Serve a workflow over HTTP
createWorkflowHandler takes a registry and returns a handler:
import { createWorkflowHandler } from "@nodetool-ai/workflow-runner";
export const handler = createWorkflowHandler({ registry, platform: "edge" });
The handler accepts a POST with a JSON body:
{
"graph": { "nodes": [], "edges": [] },
"params": {},
"workflow_id": "optional",
"job_id": "optional"
}
The response is text/event-stream. Each message is a data: event, and the run ends with an event: result event that carries the RunResult. Request problems return JSON with a 4xx status: 405 for a method other than POST, 400 for invalid JSON, a missing graph, or a failing beforeRun hook. After the stream starts, a failure arrives as an event: error event with the message “Workflow execution failed” and the HTTP status stays 200.
| Option | What it does |
|---|---|
registry |
The nodes available in this deployment |
createContext(req) |
Builds a ProcessingContext per request, for example with secrets and storage. Return nothing for a minimal context |
beforeRun(body, req) |
Authorizes or rewrites the parsed body. Throw to reject with a 400 |
platform |
Rejects graphs that contain nodes that do not support this platform, before any node starts |
Secrets come from the context. This builds one from environment variables:
import { ProcessingContext } from "@nodetool-ai/runtime/context";
import { createWorkflowHandler, envSecretResolver } from "@nodetool-ai/workflow-runner";
const handler = createWorkflowHandler({
registry,
createContext: () =>
new ProcessingContext({
jobId: crypto.randomUUID(),
secretResolver: envSecretResolver(process.env)
})
});
createContext must return a ProcessingContext instance. Any other value is ignored and the run uses a minimal context.
Run on each platform
The README for the package lists Vercel (Node and Edge), Cloudflare Workers, Bun, and Deno. The handler is a plain fetch handler, so each platform wraps it in its own entry point. The platform wrappers below are standard for each runtime and are not shipped by the package.
| Platform | Entry point |
|---|---|
| Vercel | export const POST = handler; in a route file |
| Cloudflare Workers | export default { fetch: handler }; |
| Bun | Bun.serve({ fetch: handler }); |
| Deno | Deno.serve(handler); |
| Node | Call runWorkflow directly, or adapt the handler with a Web-standard server of your choice |
On Cloudflare Workers, secrets arrive in the env argument of fetch, which the handler does not receive. Build the handler inside fetch, so createContext can use that env:
export default {
fetch(req: Request, env: Record<string, string>) {
return createWorkflowHandler({
registry,
platform: "workers",
createContext: () =>
new ProcessingContext({
jobId: crypto.randomUUID(),
secretResolver: envSecretResolver(env)
})
})(req);
}
};
Run in the browser
runBrowserWorkflow runs a graph of browser-safe nodes in a web page, with no server round trip. It checks the graph against the browser platform first, so a server-only node fails before any node starts. Every message carries job_id and workflow_id, so a client can route it through the same code as messages from the server.
import {
createBrowserRegistry,
graphRunsInRegistry,
runBrowserWorkflow
} from "@nodetool-ai/workflow-runner/browser";
import { ALL_BROWSER_NODES } from "@nodetool-ai/base-nodes/platforms/browser";
const registry = createBrowserRegistry(ALL_BROWSER_NODES);
if (graphRunsInRegistry(graph, registry)) {
const run = runBrowserWorkflow({ graph, registry, workflowId, jobId });
let next = await run.next();
while (!next.done) {
deliver(next.value);
next = await run.next();
}
const result = next.value;
}
When graphRunsInRegistry returns false, send the graph to a server instead. A Code node in the browser needs a sandboxModuleCatalog, because a browser has no process-wide catalog to resolve imports from.
Which nodes run where
Each node class lists the platforms it supports. A node that lists none supports only node. Platforms are:
| Platform | Runtime |
|---|---|
node |
Full Node.js: native modules, subprocesses, file system. Electron, RunPod, Vercel Node functions, plain servers |
workers |
Cloudflare Workers and Durable Objects, with the Node compatibility layer. No native modules or subprocesses |
edge |
Web APIs only. Vercel Edge Runtime and Deno Deploy |
browser |
A browser page with WebGPU, the DOM, canvas, and IndexedDB |
In the repository, node groups are tagged like this:
- All four platforms. The constant, input, control, variable, compare, subgraph, app, workflow, and vector nodes in the core node package, the fake media nodes, and the Code node.
- Node, Workers, and Edge. Server-oriented groups such as the LLM provider nodes, document, text extras, data, sketch, storyboard, script, timeline, entity, and messaging nodes.
- Node only. Any node that declares nothing, and the nodes that say so. An image
Scalenode, for example, reports “not supported on platform ‘browser’ (supports: node)”.
The curated browser set is ALL_BROWSER_NODES, exported from @nodetool-ai/base-nodes/platforms/browser. It holds the app, compare, constant, control, input, subgraph, vector, and workflow nodes, the placeholder and fake media nodes, and the Code node. Check a node’s class for its exact platforms value before you rely on it.
Limitations
- Only the registry you build is available. The package does not bundle any nodes, so a graph that names an unregistered node type is rejected.
- Nodes that need native modules, subprocesses, or the file system run only on
node. The Blender nodes are an example. See Blender Nodes. - Browser runs cover only browser-tagged nodes. Graphs that use other nodes must run on a server.
- The handler sends no authentication and does not limit request size or run time. Add checks in
beforeRunand in your platform. - The request body is JSON, and the response is Server-Sent Events as text. It does not use the MsgPack WebSocket protocol of the NodeTool server.
- There is no persistence. Jobs, assets, and run history are not stored unless you pass a context with storage.
- Request cancellation passes through
req.signal. A client that disconnects stops the run.
See also
- TypeScript DSL Guide for building graphs in code
- Custom Nodes Guide for writing nodes and the
platformssetting - Architecture for the kernel that does the execution