Skip to main content
Multi-workflow HTTP server exposing workflows via REST. Supports run management, SSE event streaming, and human-in-the-loop approvals. For a lighter single-workflow server that runs alongside smithers up, see Serve Mode.

Import

import { startServer } from "smithers-orchestrator";

Quick Start

import { startServer } from "smithers-orchestrator";
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,
});

ServerOptions

type ServerOptions = {
  port?: number;
  db?: BunSQLiteDatabase<any>;
  authToken?: string;
  maxBodyBytes?: number;
  rootDir?: string;
  allowNetwork?: boolean;
};
OptionTypeDefaultDescription
portnumber7331TCP port
dbBunSQLiteDatabaseundefinedSQLite database for mirroring run/event data; enables GET /v1/runs
authTokenstringprocess.env.SMITHERS_API_KEYBearer token. Falls back to env var. Disabled if neither is set.
maxBodyBytesnumber1048576 (1MB)Max request body size. Returns 413 if exceeded.
rootDirstringundefinedRoot for workflow path resolution and tool sandboxing
allowNetworkbooleanfalseAllow network access in bash tool
Returns an http.Server instance, already listening.

Authentication

When authToken is configured, every request must include:
  • Authorization: Bearer <token>, or
  • x-smithers-key: <token>
Missing/invalid tokens receive 401.

Observability

The server participates in the standard observability pipeline:
  • smithers.http.requests counter
  • smithers.http.request_duration_ms histogram
  • Request handling, workflow loading, and body parsing wrapped in spans
  • Prometheus scrape endpoint at /metrics
OTLP export:
export SMITHERS_OTEL_ENABLED=1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=smithers-server
Local collector stack:
docker compose -f observability/docker-compose.otel.yml up
See Observability for the full metrics list.

API Routes

All routes use JSON request/response bodies except GET /v1/runs/:runId/events (SSE) and GET /metrics (Prometheus text). JSON responses include Content-Type: application/json, Cache-Control: no-store, and X-Content-Type-Options: nosniff.

GET /metrics

Prometheus text exposition of runtime metrics.
# TYPE smithers_http_requests counter
smithers_http_requests 12
scrape_configs:
  - job_name: smithers
    static_configs:
      - targets: ["localhost:7331"]

POST /v1/runs

Start a new workflow run or resume an existing one.
{
  workflowPath: string;          // .tsx workflow file (required)
  input?: Record<string, any>;   // Workflow input (default: {})
  runId?: string;                // Custom run ID (default: auto-generated)
  resume?: boolean;              // Resume existing run (default: false)
  config?: {
    maxConcurrency?: number;
  };
}
Response: { "runId": "smi_abc123" } The workflow is dynamically imported, tables are auto-created, and the run starts asynchronously.
StatusCodeCondition
400INVALID_REQUESTMissing/invalid workflowPath, input, or config
400RUN_ID_REQUIREDresume: true without runId
400WORKFLOW_PATH_OUTSIDE_ROOTPath resolves outside rootDir
404RUN_NOT_FOUNDresume: true but run does not exist
409RUN_IN_PROGRESSRun with this ID already active
409RUN_ALREADY_EXISTSRun with this ID exists (no resume)

POST /v1/runs/:runId/resume

Resume a paused or failed run.
{
  workflowPath: string;
  input?: Record<string, any>;
  config?: { maxConcurrency?: number };
}
Response: { "runId": "smi_abc123" } If currently active, returns 200 with current status. Otherwise reloads the workflow and resumes from last checkpoint.
StatusCodeCondition
400INVALID_REQUESTMissing/invalid workflowPath
404RUN_NOT_FOUNDRun does not exist

POST /v1/runs/:runId/cancel

Cancel a running workflow. Signals the run’s AbortController. Response: { "runId": "smi_abc123" }
StatusCodeCondition
404NOT_FOUNDRun not in active runs

GET /v1/runs/:runId

Run status and summary.
{
  "runId": "smi_abc123",
  "workflowName": "bugfix",
  "status": "running",
  "startedAtMs": 1707500000000,
  "finishedAtMs": null,
  "summary": { "finished": 3, "in-progress": 1, "pending": 2 }
}
FieldTypeDescription
statusstringrunning, waiting-approval, finished, failed, cancelled
startedAtMsnumber | nullStart timestamp (ms)
finishedAtMsnumber | nullFinish timestamp (ms)
summaryobjectNode count by state

GET /v1/runs/:runId/events

SSE stream of lifecycle events.
ParameterTypeDefaultDescription
afterSeqnumber-1Only events after this sequence number
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}
  • Events named smithers with JSON payloads matching SmithersEvent.
  • Polls database every 500ms.
  • Keep-alive comment every 10s.
  • Closes on terminal state (finished, failed, cancelled).
  • Reconnect with afterSeq to resume.

GET /v1/runs/:runId/frames

List render frames.
ParameterTypeDefaultDescription
limitnumber50Max frames
afterFrameNonumberundefinedFrames after this number

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

Approve a node waiting for human approval.
{
  iteration?: number;     // Default: 0
  note?: string;
  decidedBy?: string;
}

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

Deny a node waiting for human approval.
{
  iteration?: number;     // Default: 0
  note?: string;
  decidedBy?: string;
}

GET /v1/runs

List all runs. Requires server-level db.
ParameterTypeDefaultDescription
limitnumber50Max runs
statusstringundefinedFilter by status
Returns 400 DB_NOT_CONFIGURED if no database was provided.

Error Response Format

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": {}
  }
}
Unhandled errors return 500 with code SERVER_ERROR.

Database Mirroring

When a server-level db is provided and the workflow uses a different database, run metadata and events are mirrored asynchronously to the server database. This enables cross-workflow listing via GET /v1/runs.

Example

import { startServer } from "smithers-orchestrator";
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,
  allowNetwork: false,
});
# Start a run
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", "decidedBy": "alice"}'