Skip to main content
Smithers includes a built-in HTTP server that exposes workflows via a REST API. You can start runs, resume interrupted workflows, stream lifecycle events via Server-Sent Events, and approve or deny human-in-the-loop gates — all over HTTP.

Import

import { startServer } from "smithers-orchestrator/server";

Quick Start

import { startServer } from "smithers-orchestrator/server";
import { drizzle } from "drizzle-orm/bun-sqlite";

const db = drizzle("./smithers.db");

const server = startServer({
  port: 7331,
  db,
  authToken: process.env.SMITHERS_API_KEY,
  rootDir: process.cwd(),
  allowNetwork: false,
});

console.log("Smithers server listening on port 7331");

ServerOptions

type ServerOptions = {
  port?: number;
  db?: BunSQLiteDatabase<any>;
  authToken?: string;
  maxBodyBytes?: number;
  rootDir?: string;
  allowNetwork?: boolean;
};
OptionTypeDefaultDescription
portnumber7331TCP port to listen on
dbBunSQLiteDatabaseundefinedOptional server-level SQLite database for mirroring run/event data and enabling the GET /v1/runs endpoint
authTokenstringprocess.env.SMITHERS_API_KEYBearer token for authentication. If not set, falls back to the SMITHERS_API_KEY environment variable. If neither is set, authentication is disabled.
maxBodyBytesnumber1048576 (1MB)Maximum request body size in bytes. Requests exceeding this limit receive a 413 response.
rootDirstringundefinedRoot directory for workflow path resolution and tool sandboxing. Workflow paths must resolve within this directory.
allowNetworkbooleanfalseWhether to allow network access in the bash tool for all runs started by this server

Return Value

startServer returns a Node.js http.Server instance. The server is already listening when returned.

Authentication

If authToken is configured (or SMITHERS_API_KEY is set), every request must include a valid token via one of:
  • Authorization header: Authorization: Bearer <token>
  • Custom header: x-smithers-key: <token>
Unauthenticated requests receive a 401 response:
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid authorization token"
  }
}

API Routes

All routes use JSON request/response bodies (except the SSE endpoint). Responses include Content-Type: application/json, Cache-Control: no-store, and X-Content-Type-Options: nosniff headers.

POST /v1/runs

Start a new workflow run, or resume an existing one. Request body:
{
  workflowPath: string;          // Path to the .tsx workflow file (required)
  input?: Record<string, any>;   // Input data passed to the workflow (default: {})
  runId?: string;                // Custom run ID (default: auto-generated)
  resume?: boolean;              // Resume an existing run (default: false)
  config?: {
    maxConcurrency?: number;     // Max parallel tasks
  };
}
Response (200):
{
  "runId": "smi_abc123"
}
Behavior:
  • The workflow file is dynamically imported from workflowPath (must export default).
  • The workflow’s SQLite tables are automatically created if needed.
  • The run is started asynchronously. The response is returned immediately with the runId.
  • If resume: true, the runId is required and the run must already exist in the database.
  • If a run with the same runId is already in progress and resume is not set, a 409 error is returned.
Error responses:
StatusCodeCondition
400INVALID_REQUESTMissing or invalid workflowPath, input, or config
400RUN_ID_REQUIREDresume: true but no runId provided
400WORKFLOW_PATH_OUTSIDE_ROOTWorkflow path resolves outside rootDir
404RUN_NOT_FOUNDresume: true but the run does not exist
409RUN_IN_PROGRESSA run with this runId is already active
409RUN_ALREADY_EXISTSA run with this runId already exists (and resume is false)

POST /v1/runs/:runId/resume

Resume a previously paused or failed workflow run. Request body:
{
  workflowPath: string;          // Path to the .tsx workflow file (required)
  input?: Record<string, any>;   // Input data (default: {})
  config?: {
    maxConcurrency?: number;
  };
}
Response (200):
{
  "runId": "smi_abc123"
}
Behavior:
  • The run must already exist in the database.
  • If the run is currently active, returns 200 with the current status without starting a new process.
  • Otherwise, reloads the workflow and resumes execution from the last checkpoint.
Error responses:
StatusCodeCondition
400INVALID_REQUESTMissing or invalid workflowPath
404RUN_NOT_FOUNDRun does not exist

POST /v1/runs/:runId/cancel

Cancel a running workflow. Response (200):
{
  "runId": "smi_abc123"
}
Behavior:
  • Signals the run’s AbortController to cancel execution.
  • If the run is not found in the active run map, returns a 404.
Error responses:
StatusCodeCondition
404NOT_FOUNDRun not found in active runs

GET /v1/runs/:runId

Get the current status and summary of a run. Response (200):
{
  "runId": "smi_abc123",
  "workflowName": "bugfix",
  "status": "running",
  "startedAtMs": 1707500000000,
  "finishedAtMs": null,
  "summary": {
    "finished": 3,
    "in-progress": 1,
    "pending": 2
  }
}
Fields:
FieldTypeDescription
runIdstringThe run identifier
workflowNamestringName derived from the workflow file
statusstringCurrent status: running, waiting-approval, finished, failed, cancelled
startedAtMsnumber | nullStart timestamp in milliseconds
finishedAtMsnumber | nullFinish timestamp in milliseconds
summaryobjectNode count by state (e.g., { finished: 3, pending: 2 })

GET /v1/runs/:runId/events

Stream lifecycle events via Server-Sent Events (SSE). Query parameters:
ParameterTypeDefaultDescription
afterSeqnumber-1Only return events after this sequence number
Response: text/event-stream
retry: 1000

event: smithers
data: {"type":"RunStarted","runId":"smi_abc123","timestampMs":1707500000000}

event: smithers
data: {"type":"NodeStarted","runId":"smi_abc123","nodeId":"analyze","iteration":0,"attempt":0,"timestampMs":1707500001000}

: keep-alive

event: smithers
data: {"type":"NodeFinished","runId":"smi_abc123","nodeId":"analyze","iteration":0,"attempt":0,"timestampMs":1707500010000}
Behavior:
  • Events are named smithers with JSON payloads matching the SmithersEvent union type.
  • The server polls the database every 500ms for new events.
  • A : keep-alive comment is sent every 10 seconds when no events are available.
  • The stream closes automatically when the run reaches a terminal state (finished, failed, cancelled) and all events have been delivered.
  • The client can reconnect using afterSeq to resume from where it left off.

GET /v1/runs/:runId/frames

List render frames for a run. Query parameters:
ParameterTypeDefaultDescription
limitnumber50Maximum number of frames to return
afterFrameNonumberundefinedOnly return frames after this frame number
Response (200): Array of frame objects.

POST /v1/runs/:runId/nodes/:nodeId/approve

Approve a node that is waiting for human approval. Request body:
{
  iteration?: number;     // Iteration to approve (default: 0)
  note?: string;          // Optional approval note
  decidedBy?: string;     // Who approved
}
Response (200):
{
  "runId": "smi_abc123"
}

POST /v1/runs/:runId/nodes/:nodeId/deny

Deny a node that is waiting for human approval. Request body:
{
  iteration?: number;     // Iteration to deny (default: 0)
  note?: string;          // Optional denial reason
  decidedBy?: string;     // Who denied
}
Response (200):
{
  "runId": "smi_abc123"
}

GET /v1/runs

List all runs. Requires a server-level db to be configured. Query parameters:
ParameterTypeDefaultDescription
limitnumber50Maximum number of runs to return
statusstringundefinedFilter by status (e.g., running, finished)
Response (200): Array of run summary objects. Error responses:
StatusCodeCondition
400DB_NOT_CONFIGUREDNo server-level database was provided to startServer

Error Response Format

All error responses follow a consistent format:
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": {}
  }
}
Unhandled errors return a 500 with code SERVER_ERROR.

Database Mirroring

When a server-level db is provided and the workflow uses a different database, the server automatically mirrors run metadata and events to the server database. This enables the GET /v1/runs endpoint to list runs across all workflows. Events are mirrored asynchronously and do not block the workflow execution.

Example: Full Server Setup

import { startServer } from "smithers-orchestrator/server";
import { drizzle } from "drizzle-orm/bun-sqlite";

const db = drizzle("./server.db");

const server = startServer({
  port: 7331,
  db,
  authToken: "sk-my-secret-token",
  rootDir: "/home/workflows",
  maxBodyBytes: 2 * 1024 * 1024,  // 2MB
  allowNetwork: false,
});
Start a run with curl:
curl -X POST http://localhost:7331/v1/runs \
  -H "Authorization: Bearer sk-my-secret-token" \
  -H "Content-Type: application/json" \
  -d '{
    "workflowPath": "./bugfix.tsx",
    "input": { "description": "Fix the auth token expiry bug" }
  }'
Stream events:
curl -N http://localhost:7331/v1/runs/smi_abc123/events \
  -H "Authorization: Bearer sk-my-secret-token"
Approve a node:
curl -X POST http://localhost:7331/v1/runs/smi_abc123/nodes/deploy/approve \
  -H "Authorization: Bearer sk-my-secret-token" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Looks good, ship it", "decidedBy": "alice" }'