Architecture
If you are here to edit a specific area, AGENTS.md has a "working on X? read Y" table.
The shape of it
A TanStack Start application — React 19 UI, server functions, SQLite — that schedules local coding-agent CLIs as child processes.
There is no model API. Open Run never sends a token to Anthropic or OpenAI. It
drives the claude, codex, grok, gemini and agy binaries the user is
already logged into.
1. A run is a conversation, not a log
The first turn is the automation's prompt. Follow-up turns resume the same
agent session (claude --resume, codex exec resume), so context survives
across turns. Each turn snapshots git state, which is what makes the diff
viewer and "open a PR" possible.
Automations can resume a native CLI chat started outside Open Run in the
same workspace folder (Claude, Codex, Grok, Antigravity). That still produces
a normal Open Run run — native chats are not listed on /runs. A Once at
trigger fires the cron once, then pauses the automation.
2. Every runtime speaks one event vocabulary
Each CLI has a transport: cli parses the binary's own JSON output, acp
drives it over the Agent Client Protocol.
Either way, what lands in the database is the same ACP-shaped vocabulary
(lib/acp.ts) — tool calls with a title, kind, status and file locations;
approvals as an options list with an outcome.
That subset is hand-written so lib/ stays dependency-free.
lib/acpConformance.ts type-checks it against @agentclientprotocol/sdk.
Request path
routes/*.tsx → lib/queries.ts (React Query) → fns/index.ts (createServerFn RPC)
↓ lazy import()
server/core.ts (facade, boots scheduler)
↓ ↓ ↓
server/db.ts server/executor.ts server/scheduler.ts
(better-sqlite3) (spawn CLI) (node-cron)src/server/** is server-only. UI routes reach it exclusively through
src/fns/index.ts, where every handler does await import('../server/core')
lazily. That keeps better-sqlite3, node-cron and child_process out of
the client bundle. A static import of server/* from a route component breaks
the client build.
Live path
executor → server/runLive.ts + server/activityLive.ts (in-process pub/sub)
→ routes/api/runs/$runId/stream.ts (SSE, one run)
routes/api/activity/stream.ts (SSE, run started/finished)
→ lib/useRunLive.ts + lib/useActivityLive.tsx (EventSource)
→ lib/applyRunLiveEvent.ts (patches the React Query cache)HTTP polling is the fallback when a stream is down.
Access control
One global request middleware (src/start.ts) sits in front of every server
function and API route; the bind address is settled before the socket opens
(scripts/start.ts). Both apply the same tested rules from
lib/serverAccess.ts. See security.md.
Why lib/ is dependency-free
src/lib/** has no node: imports, no SQLite, no filesystem access. The
same rule module runs in the browser and on the server write path, so the UI
can disable a button with the exact message the server would have thrown.
That is what the gate modules are — lib/runPrereqGate.ts holds the shared
workspace/PATH/prompt checks; lib/enableGate.ts, lib/runNowGate.ts,
lib/projectGate.ts and lib/gitActionGate.ts mirror the server's refusals.
A new refuse condition goes in the server path and the matching gate.
These are the tested modules: pure logic, no I/O, colocated *.test.ts,
node:test.
Data model
Single SQLite file, data/openrun.db, created on first run.
| Table | Holds |
|---|---|
projects, workspaces |
Repositories and their worktrees |
runtimes |
One row per agent CLI: binary, args template, transport |
tasks |
Automations: runtime + prompt + workspace + cron |
runs, messages, turn_events |
Executions and their transcripts |
check_results |
Post-turn verification |
integrations, webhook_deliveries |
Inbound webhooks |
notifiers, notification_deliveries |
Outbound notifications |
run_queue |
Fires waiting on a busy workspace |
devices, device_pairings |
Paired phones |
model_catalog |
Discovered CLI models |
- Migrations are additive-only.
addColumndiffstable_infobecause SQLite has noADD COLUMN IF NOT EXISTS. turn_eventsrows are append-only and forward-compatible. Payload fields are optional. Readers tolerateundefined.
Integrations
GitHub / Jira / Linear push events in. Each provider verifies its signature,
then normalises onto a single CanonicalWebhookEvent that the dispatcher
matches against enabled automations. Adding a provider means implementing
verify and parse.
What is open source
Everything in this repository, under AGPLv3. The commercial planes — fleet
dashboard, hosted history, remote runners, SSO, audit, policy — are separate
proprietary products that attach through lib/edition.ts. That seam only ever
adds surfaces; lib/edition.test.ts fails the build if any local feature
starts consulting it.
Conventions worth knowing early
- Tests are
node:test+node:assert/strict, colocated. No Vitest, no Jest. - Value imports in test-covered
lib/modules carry an explicit.tsextension —--experimental-strip-typeshas no bundler resolution. src/routeTree.gen.tsis generated. Never hand-edit it. Preferpnpm buildoverpnpm generate-routes(the standalone CLI currently emits a differentRegisterblock).- The scheduler and both live pub/sub registries are module singletons
guarded on
globalThisso they survive Vite HMR. changelog.d/takes one file per shipped change, in a negative-relief voice: "You no longer …".
Known limits
- Single user, no roles, no audit trail.
- SQLite secrets are sealed under
~/.openrun/data-key; CLI config files still hold MCP headers in the clear because the CLI reads them. - Windows is untested outside WSL2.
- Runs execute real commands with your credentials — treat run cwd resolution and prompt construction as security-relevant.