Autonomous Coding Agents, Orchestrated at Scale
From issue tracker to merged PR — without human supervision
github.com/openai/symphony · Apache 2.0 · Elixir/OTP
Part One
What is Symphony, explained simply
Today, AI coding agents are powerful but need constant supervision.
The shift: Instead of supervising agents, you manage work. Think Jira board, but the tickets do themselves.
Symphony is the head chef who reads incoming orders (tickets), assigns each dish to a cook (coding agent) at their own station (workspace), checks if the dish is done, and sends it out (PR).
Every 30 seconds, Symphony does this:
Key insight: Symphony is a specification, not a product. The repo publishes a detailed SPEC.md that anyone can implement. OpenAI provides an Elixir reference implementation built on OTP — perfect for supervising long-running processes.
This is early-stage ("low-key engineering preview") but represents a real architectural pattern: the operating system for coding agents.
Part Two
Architecture, protocols, and implementation details
The repo is a specification + reference implementation pattern.
openai/symphony/ SPEC.md # Language-agnostic spec README.md # Overview + setup LICENSE # Apache 2.0 elixir/ WORKFLOW.md # Config + agent prompt AGENTS.md # Coding guidelines lib/ orchestrator.ex agent_runner.ex workspace.ex codex/app_server.ex codex/dynamic_tool.ex linear/client.ex config/schema.ex ... + web dashboard .codex/skills/ # commit, push, land...
Language-agnostic contract defining all components, states, protocols, and behaviors. Anyone can build a conforming implementation.
Reference implementation using Elixir/OTP. Chosen because BEAM is purpose-built for supervising long-running, concurrent processes.
Primary language: Elixir · Web: Phoenix LiveView · Agent protocol: JSON-RPC 2.0 over stdio
The SPEC.md defines these as the required building blocks of any Symphony implementation.
Reads WORKFLOW.md — YAML front matter (config) + Markdown body (agent prompt). Hot-reloads on change.
Typed getters with defaults, env var indirection ($VAR), path expansion, nested validation per section.
Linear GraphQL adapter. Fetches candidates, refreshes state, normalizes issue fields for prompt rendering.
The brain. GenServer polling loop, dispatch decisions, concurrency limits, retry management, reconciliation.
Per-issue directory isolation, lifecycle hooks (after_create, before_run, after_run, before_remove), path safety.
Launches Codex subprocess via JSON-RPC over stdio. Multi-turn loop with state checking between turns.
Phoenix LiveView dashboard, JSON API (/api/v1/state), structured logging, token accounting.
An OTP GenServer that runs the core polling loop every 30 seconds (configurable).
The orchestrator tracks each issue through claim states (internal) and issue states (from Linear).
Prevents duplicate dispatch — an issue can only have one active agent.
| Category | States |
|---|---|
| Active | Todo, In Progress |
| Custom | Rework, Human Review, Merging |
| Terminal | Done, Closed, Cancelled, Duplicate |
When an issue reaches a terminal state, Symphony kills the agent and removes the workspace.
Not every ticket gets an agent immediately. The orchestrator applies several filters.
Eligible issues are sorted by: priority (ascending, urgent first) → creation date (oldest first) → identifier (lexicographic tiebreaker)
The orchestrator picks the least-loaded available host, respecting per-host concurrency limits. Retries prefer the original host for workspace affinity.
One file defines everything: infrastructure config (YAML) + agent instructions (Markdown).
--- tracker: kind: linear project_slug: "my-project-abc123" active_states: ["Todo", "In Progress", "Rework"] terminal_states: ["Done", "Cancelled"] workspace: root: ~/code/workspaces hooks: after_create: | git clone git@github.com:your-org/repo.git . agent: max_concurrent_agents: 10 max_turns: 20 codex: command: codex app-server thread_sandbox: workspace-write --- You are working on Linear issue {{ issue.identifier }}. Title: {{ issue.title }} Description: {{ issue.description }} Implement the requested changes and open a PR.
Nine typed, validated configuration sections with environment variable support.
| Section | Key Fields | Defaults |
|---|---|---|
| tracker | kind, endpoint, api_key, project_slug, active/terminal_states | Linear GraphQL |
| polling | interval_ms | 30,000 ms |
| workspace | root | /tmp/symphony_workspaces |
| worker | ssh_hosts[], max_concurrent_agents_per_host | Local only |
| agent | max_concurrent_agents, max_turns, max_retry_backoff_ms | 10 / 20 / 300s |
| codex | command, approval_policy, thread_sandbox, turn_timeout_ms | workspace-write |
| hooks | after_create, before_run, after_run, before_remove, timeout_ms | 60s timeout |
| observability | dashboard_enabled, refresh_ms | true / 1s |
| server | port, host | 127.0.0.1 |
Environment variable indirection: use $LINEAR_API_KEY in YAML and Symphony resolves it at runtime. Path values expand ~ to home directory.
Symphony communicates with Codex via a structured JSON-RPC 2.0 protocol over standard I/O.
// 1. Start process bash -lc "codex app-server" // 2. Initialize → { "method": "initialize", "id": 1 } ← { "id": 1, "result": {...} } // 3. Start thread (persists across turns) → { "method": "thread/start", "id": 2 } ← { "id": 2, "result": { "threadId": "..." } } // 4. Start turn (one per iteration) → { "method": "turn/start", "id": 3 } ← stream of events... ← { "method": "turn/completed" }
| Event | Meaning |
|---|---|
| turn/completed | Success |
| turn/failed | Error with params |
| turn/cancelled | User cancellation |
| turn/input_required | Needs operator input |
Approval requests for file changes and command execution are auto-approved with acceptForSession. User input requests get a canned "non-interactive session" response.
Same threadId reused across continuation turns. Up to max_turns (default 20) per session.
Every issue gets a completely isolated directory. This is a core safety invariant.
# Workspace is created after_create: git clone --depth 1 \ git@github.com:org/repo.git . # Before agent starts before_run: git fetch origin main # After agent finishes after_run: make test # Before workspace removal before_remove: mix workspace.before_remove
| Policy | Scope |
|---|---|
| read-only | No writes at all |
| workspace-write | Writes only within workspace |
| danger-full-access | Unrestricted (not recommended) |
Symphony distinguishes between continuations (the agent wants another turn) and failures (something broke).
Agent finished a turn but the issue is still active. Retry quickly.
Agent crashed, timed out, or hit an error. Back off exponentially.
No persistent database required. On restart, Symphony polls the tracker and rediscovers what needs work. State is recovered from the source of truth (Linear), not from local storage.
Symphony gives agents runtime tools that extend their capabilities beyond just writing code.
When Codex sends an item/tool/call message, Symphony intercepts and executes it server-side.
// Agent sends: { "method": "item/tool/call", "params": { "name": "linear_graphql", "arguments": { "query": "mutation { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }", "variables": { ... } } } }
The Markdown body of WORKFLOW.md is a template rendered per-issue with strict variable validation.
| Variable | Contents |
|---|---|
| issue.id | Internal Linear UUID |
| issue.identifier | e.g., "PROJ-42" |
| issue.title | Issue title text |
| issue.description | Full description body |
| issue.state | Current state name |
| issue.labels | Array of label names |
| issue.blockers | Blocking issues list |
| attempt | null (first) or integer (retry) |
Unknown variables or filters cause a hard failure, not silent substitution. This catches typos in WORKFLOW.md before agents get malformed prompts.
First turn gets the full rendered prompt. Continuation turns get a shorter prompt indicating "continue where you left off" with updated attempt counter.
Edit WORKFLOW.md while running. New prompt applies to future runs. Invalid edits keep the last-known-good config.
Symphony tracks every token spent across all agents and provides real-time dashboards.
# Enable with --port flag ./bin/symphony WORKFLOW.md --port 4000 # Endpoints GET / # LiveView dashboard GET /api/v1/state # JSON summary GET /api/v1/<id> # Issue detail POST /api/v1/refresh # Force poll now
When no port is specified, a terminal-based dashboard renders at 60fps showing running agents, retry queues, token totals, and credit limits.
Symphony can scale across multiple machines by running agents on remote hosts via SSH.
worker: ssh_hosts: - worker-1.internal - worker-2.internal - worker-3.internal max_concurrent_agents_per_host: 4
select_worker_host/2 picks the host with the fewest running agents. On retry, it prefers the original host for workspace affinity (avoids re-cloning).
Symphony is designed to be resilient at every layer — no persistent database, no single point of failure beyond the orchestrator itself.
Blocks dispatch but keeps the service alive. Last-known-good config stays active. Error logged.
GenServer receives :DOWN message. Schedules retry with exponential backoff. Preserves host affinity.
Skips this polling tick. Retries on next tick (30s later). No state corruption — tracker is always the source of truth.
Re-polls Linear to discover current state. Cleans up stale workspaces for terminal issues. No recovery DB needed.
Reconciliation on every tick: The orchestrator checks each running issue against the tracker. If an issue moved to a terminal state externally (e.g., human closed it), the agent is halted and the workspace removed.
The spec is explicit: workspace isolation is a baseline, not a complete sandbox.
| Policy | Behavior |
|---|---|
| untrusted | Prompt for every action |
| on-failure | Auto-approve until failure |
| on-request | Approve when agent asks |
| never | Full auto-approve |
| reject{} | Object form for granular rules |
OS user isolation per agent, tighter filesystem permissions, network restrictions, per-workspace approval policies.
The reference implementation uses Elixir — a deliberate architectural choice.
Orchestrator state machine: handles ticks, dispatch, retries, reconciliation in a single supervised process.
Each agent runs as a monitored Task. The orchestrator receives :DOWN on completion/failure.
Erlang Ports wrap the Codex subprocess. Provides stdio streaming with backpressure and cleanup guarantees.
The spec is explicit about scope boundaries. Symphony is an orchestrator, not a platform.
"Low-key engineering preview for testing in trusted environments."
Symphony focuses on doing one thing well: turning issue tracker tickets into isolated, autonomous agent sessions. Everything else is either delegated to the agent or left to the implementation.
The spec encourages ports in Python, Go, TypeScript, and other languages. The Elixir implementation is a reference, not the only option.
Putting it all together — from Linear ticket to completed PR.
github.com/openai/symphony — Full repo + spec
SPEC.md — Language-agnostic service specification
elixir/README.md — Setup the reference implementation
WORKFLOW.md — Single-file configuration + prompt template
agent37.club