How the industry solves it
Every mature orchestrator grew a live UI, and two adjacent traditions feed into agent steering: “UI from your code” frameworks and agent-UI protocols.
Two lessons recur. First, the steerable systems (Temporal, LangGraph, Argo) all put a durable state layer under the UI — the UI reads and writes checkpoints, it doesn’t host the computation. Second, the systems people actually enjoy building UIs for (Streamlit, Gradio, AI SDK generative UI) made the UI belong to the code, declared next to the logic instead of built as a separate frontend project. Smithers takes both: durable state under every surface, and a
<UI> element declared inside the workflow itself.
How Smithers does it
The Gateway is the control plane
smithers gateway serves one process per workspace: a domain REST API, an RPC surface, and WebSocket event streams over every run in the workspace database. Clients authenticate with bearer tokens carrying typed scopes (Authorization: Bearer <token> or x-smithers-key; JWT and trusted-proxy modes exist for shared deployments). On connect, the WebSocket handshake returns a hello payload with protocol, features, and the session’s role/scopes, then pushes live event frames filtered by runId subscription. Runs launched through the Gateway expose ctx.auth = { triggeredBy, role, scopes, createdAt }, and <Approval> can restrict who may decide with allowedScopes / allowedUsers — enforced server-side before submitApproval is accepted.
Because all UI surfaces are Gateway clients over that durable state, the run/UI decoupling is structural, not a feature flag. Close the Monitor tab; the run continues. Reboot your laptop mid-run; smithers ps still lists it, and reopening the UI resumes the same view from the database.
Three surfaces, one state
smithers gui opens a directory’s workspace at the most recent run’s workflow UI; smithers monitor RUN_ID deep-links one run. All three autostart a Gateway if none is running (--no-autostart to opt out, --gateway <url> to point at a remote one).
Workflow-owned UIs
A custom UI is declared in the workflow with<UI> — it renders nothing in the graph; the Gateway discovers it at gateway.register(), bundles the entry with Bun.build into a single browser script, and serves it with a boot config (__SMITHERS_GATEWAY_UI__) injected before your code runs:
.smithers/workflows/triage.tsx
smithers-orchestrator/gateway-react hooks (useGatewayRun, useGatewayRuns, useGatewayRunEvents, useGatewayNodeOutput, useGatewayNodeEvents, useGatewayApprovals, useGatewayActions, useGatewayRunDiff, useGatewayScores, useGatewayRunTokenUsage, and more, over TanStack DB collections), smithers-orchestrator/gateway-ui run-aware widgets (RunList, RunTree, RunEventLog, ApprovalPanel, NodeChatStream, NodeOutputCard, WorkflowGraph, LaunchButton, StatusPill, SimpleWorkflowDashboard), and the smithers-orchestrator/ui shadcn-anatomy base library, all themed by the workflow UI design tokens so light/dark is automatic:
.smithers/ui/triage.tsx
useGatewayActions() launches runs and submits decisions, smithers signal delivers durable signals to <Signal>/<WaitForEvent> nodes, and pause/resume/cancel/retry-task are one-shot RPCs. Mid-run correction — the property interruptible-agent arguments center on — comes from the durable substrate: rewind, fork-from-checkpoint, and revert-attempt are all exposed through the same Gateway (see Time Travel).
Hot reload
Workflow code hot-reloads underRunOptions.hot (schema-shape changes are blocked; in-flight tasks finish on the code they launched with — see Recipes). For UI iteration, set SMITHERS_GATEWAY_UI_NO_CACHE=1 on the Gateway to rebuild the bundle on every request, so edits to the entry or any import show up on a plain page reload.
Guarantees and limits
Guaranteed: every surface is a stateless client over durable state — closing it never affects the run; reconnecting WebSocket streams resume viaafterSeq; approvals and signals are durable RPCs that survive the deciding tab. Scoped tokens gate every method.
Honest limits, verified in source and the tracker:
- Pack-extension UIs have no domain-data path. The client side of the Gateway extension surface ships (
SmithersGatewayClient.extensionRpc,useGatewayExtensionResource/useGatewayExtensionAction/useGatewayExtensionStream), butsmithers gatewaycannot load pack-declared extension servers, so a custom UI cannot yet read pack-specific domain data through the Gateway (smithers#1438, open). Workaround: ship the data as node outputs and read them withuseGatewayNodeOutput. - UI bundle caching is build-once by default. Without
SMITHERS_GATEWAY_UI_NO_CACHE=1a long-lived Gateway serves the cached bundle, so UI edits do not appear on refresh (smithers#1440, open). --mint-tokengateways can’t serve browser UIs yet. The CLI reads the minted bearer from the runtime state file, but plain browser navigation cannot send it; run the local Gateway without--mint-tokenwhen you need browser-served workflow UIs.- The Monitor is workspace-local by design; multi-workspace fleet views are not a shipped surface.
Operating it
watch_run, get_run_events, list_pending_approvals, resolve_approval, ask_human.
See also
- Custom Workflow UIs — the full build guide (boot config, stale-data-free model, testing, proxying)
- The Smithers Monitor ·
<UI>·<TUI> - Gateway — RPC/WS protocol, auth modes, scopes
- Workflow UI Design Principles · Sync