OpenAI Symphony

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

ELI5

What is Symphony, explained simply

The Problem: Babysitting Robots

Today, AI coding agents are powerful but need constant supervision.

Without Symphony

  • You open a ticket: "Fix the login bug"
  • You manually start a coding agent
  • You watch it work, approve file changes
  • You check if it finished correctly
  • You start the next ticket... repeat

With Symphony

  • You write tickets on a board (Linear)
  • Symphony sees the tickets automatically
  • It assigns an agent to each one
  • Agents work in parallel, in isolation
  • You review PRs when they are ready

The shift: Instead of supervising agents, you manage work. Think Jira board, but the tickets do themselves.

Think of it Like a Restaurant Kitchen

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).

Restaurant

  • Orders come in from the dining room
  • Head chef reads and assigns them
  • Each cook works at their own station
  • If a dish fails, chef says "try again"
  • Finished dishes go to the pass

Symphony

  • Linear tickets appear on the board
  • Orchestrator polls and dispatches them
  • Each Codex agent gets its own workspace
  • On failure, Symphony retries with backoff
  • Completed work becomes a pull request

The Loop, Simply

Every 30 seconds, Symphony does this:

Check Linear
Any new tickets?
→
Create Workspace
Fresh git clone
→
Launch Agent
Codex starts coding
→
Monitor
Is it done? Stuck?
→
Cleanup
Remove workspace

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.

Why This Matters

"From agent supervision to work management"

This is early-stage ("low-key engineering preview") but represents a real architectural pattern: the operating system for coding agents.

Part Two

Technical Deep Dive

Architecture, protocols, and implementation details

Repository Structure

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...

Two-Layer Design

SPEC.md

Language-agnostic contract defining all components, states, protocols, and behaviors. Anyone can build a conforming implementation.

elixir/

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

Seven Core Components

The SPEC.md defines these as the required building blocks of any Symphony implementation.

1. Workflow Loader

Reads WORKFLOW.md — YAML front matter (config) + Markdown body (agent prompt). Hot-reloads on change.

2. Config Layer

Typed getters with defaults, env var indirection ($VAR), path expansion, nested validation per section.

3. Issue Tracker Client

Linear GraphQL adapter. Fetches candidates, refreshes state, normalizes issue fields for prompt rendering.

4. Orchestrator

The brain. GenServer polling loop, dispatch decisions, concurrency limits, retry management, reconciliation.

5. Workspace Manager

Per-issue directory isolation, lifecycle hooks (after_create, before_run, after_run, before_remove), path safety.

6. Agent Runner

Launches Codex subprocess via JSON-RPC over stdio. Multi-turn loop with state checking between turns.

7. Status Surface

Phoenix LiveView dashboard, JSON API (/api/v1/state), structured logging, token accounting.

The Orchestrator: Heart of Symphony

An OTP GenServer that runs the core polling loop every 30 seconds (configurable).

Each Tick Performs Five Steps

1. Reconcile
Check running agents
→
2. Validate
Check config
→
3. Fetch
Get candidates
→
4. Dispatch
Spawn workers
→
5. Notify
Update observers

GenServer State Tracks

  • Running tasks map with per-issue metadata
  • Completed/claimed issue sets
  • Retry attempt counts and backoff timers
  • Codex token accounting (input/output/total)
  • Rate limit tracking per bucket
  • Worker host load balancing state

Issue State Machine

The orchestrator tracks each issue through claim states (internal) and issue states (from Linear).

Internal Claim States

Unclaimed → Claimed → Running
Running → RetryQueued → Running
Running → Released

Prevents duplicate dispatch — an issue can only have one active agent.

Linear Issue States (Default)

CategoryStates
ActiveTodo, In Progress
CustomRework, Human Review, Merging
TerminalDone, Closed, Cancelled, Duplicate

When an issue reaches a terminal state, Symphony kills the agent and removes the workspace.

Dispatch Logic

Not every ticket gets an agent immediately. The orchestrator applies several filters.

Eligibility Requirements

Priority Sorting

Eligible issues are sorted by: priority (ascending, urgent first) → creation date (oldest first) → identifier (lexicographic tiebreaker)

Worker Host Selection

The orchestrator picks the least-loaded available host, respecting per-host concurrency limits. Retries prefer the original host for workspace affinity.

WORKFLOW.md: Single-File Config

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.

Configuration Schema

Nine typed, validated configuration sections with environment variable support.

SectionKey FieldsDefaults
trackerkind, endpoint, api_key, project_slug, active/terminal_statesLinear GraphQL
pollinginterval_ms30,000 ms
workspaceroot/tmp/symphony_workspaces
workerssh_hosts[], max_concurrent_agents_per_hostLocal only
agentmax_concurrent_agents, max_turns, max_retry_backoff_ms10 / 20 / 300s
codexcommand, approval_policy, thread_sandbox, turn_timeout_msworkspace-write
hooksafter_create, before_run, after_run, before_remove, timeout_ms60s timeout
observabilitydashboard_enabled, refresh_mstrue / 1s
serverport, host127.0.0.1

Environment variable indirection: use $LINEAR_API_KEY in YAML and Symphony resolves it at runtime. Path values expand ~ to home directory.

Agent Protocol: JSON-RPC over stdio

Symphony communicates with Codex via a structured JSON-RPC 2.0 protocol over standard I/O.

Session Lifecycle

// 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" }

Turn Events

EventMeaning
turn/completedSuccess
turn/failedError with params
turn/cancelledUser cancellation
turn/input_requiredNeeds operator input

Approval Handling

Approval requests for file changes and command execution are auto-approved with acceptForSession. User input requests get a canned "non-interactive session" response.

Multi-Turn Loop

Same threadId reused across continuation turns. Up to max_turns (default 20) per session.

Workspace Isolation

Every issue gets a completely isolated directory. This is a core safety invariant.

Lifecycle Hooks

# 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

Safety Invariants

  • Agent CWD is always the workspace path, never the source repo
  • Workspace key: identifier sanitized to [A-Za-z0-9._-]
  • Path validated via canonical resolution — prevents symlink escapes
  • Must remain under workspace.root
  • Hook timeout: 60s default

Sandbox Policies

PolicyScope
read-onlyNo writes at all
workspace-writeWrites only within workspace
danger-full-accessUnrestricted (not recommended)

Retry Strategy

Symphony distinguishes between continuations (the agent wants another turn) and failures (something broke).

Continuation Retry

Agent finished a turn but the issue is still active. Retry quickly.

  • Delay: 1 second
  • Attempt counter: 1
  • Preserves workspace and host affinity
  • Same thread ID continues the conversation

Failure Retry

Agent crashed, timed out, or hit an error. Back off exponentially.

  • Formula: min(10s * 2^(attempt-1), max_backoff)
  • Default max backoff: 300 seconds (5 min)
  • Example: 10s, 20s, 40s, 80s, 160s, 300s, 300s...
  • Prefers same worker host for retry

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.

Dynamic Tools: linear_graphql

Symphony gives agents runtime tools that extend their capabilities beyond just writing code.

How It Works

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": { ... }
    }
  }
}

Security Model

  • Uses Symphony's Linear API key — never exposed to the agent
  • Rejects multi-operation queries
  • Returns success/failure + full GraphQL response
  • Enables agents to update tickets, add comments, change status

Agent Capabilities via Tools

  • Move issues to "Human Review" when done
  • Post progress comments (workpad pattern)
  • Query blocking issues
  • Mark issues as Done/Cancelled

Prompt Rendering

The Markdown body of WORKFLOW.md is a template rendered per-issue with strict variable validation.

Available Variables

VariableContents
issue.idInternal Linear UUID
issue.identifiere.g., "PROJ-42"
issue.titleIssue title text
issue.descriptionFull description body
issue.stateCurrent state name
issue.labelsArray of label names
issue.blockersBlocking issues list
attemptnull (first) or integer (retry)

Strict Template Engine

Unknown variables or filters cause a hard failure, not silent substitution. This catches typos in WORKFLOW.md before agents get malformed prompts.

Initial vs. Continuation Prompts

First turn gets the full rendered prompt. Continuation turns get a shorter prompt indicating "continue where you left off" with updated attempt counter.

Hot Reload

Edit WORKFLOW.md while running. New prompt applies to future runs. Invalid edits keep the last-known-good config.

Observability & Token Accounting

Symphony tracks every token spent across all agents and provides real-time dashboards.

Token Tracking

  • Extracts usage from Codex event payloads
  • Computes deltas to prevent double-counting
  • Tracks input tokens, output tokens, total
  • Aggregates per-session and per-orchestrator lifetime
  • Rate limit detection (primary, secondary, credits buckets)

Structured Logging

  • Every log line tagged with issue_id, session_id
  • Session metrics: turn count, token usage, runtime seconds
  • No API tokens ever logged

Web Dashboard (Phoenix LiveView)

# 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

Status Dashboard (Terminal)

When no port is specified, a terminal-based dashboard renders at 60fps showing running agents, retry queues, token totals, and credit limits.

Distributed Workers via SSH

Symphony can scale across multiple machines by running agents on remote hosts via SSH.

Configuration

worker:
  ssh_hosts:
    - worker-1.internal
    - worker-2.internal
    - worker-3.internal
  max_concurrent_agents_per_host: 4

How It Works

  • Codex app-server launched via SSH on the remote host
  • JSON-RPC messages tunneled over the SSH connection's stdio
  • Workspaces created remotely via shell scripts
  • Orchestrator selects least-loaded host for new dispatches

Load Balancing

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).

Remote Safety

  • Shell escaping prevents injection in path variables
  • Remote path validation: checks for empty paths and control chars
  • Workspace removal is idempotent across hosts
  • E2E tests use Docker containers simulating SSH workers

Failure Recovery

Symphony is designed to be resilient at every layer — no persistent database, no single point of failure beyond the orchestrator itself.

Config/Workflow Error

Blocks dispatch but keeps the service alive. Last-known-good config stays active. Error logged.

Worker Crash

GenServer receives :DOWN message. Schedules retry with exponential backoff. Preserves host affinity.

Tracker API Failure

Skips this polling tick. Retries on next tick (30s later). No state corruption — tracker is always the source of truth.

Full Restart

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.

Security Model

The spec is explicit: workspace isolation is a baseline, not a complete sandbox.

Built-in Protections

  • Workspace path validation prevents directory traversal
  • Canonical path resolution blocks symlink escapes
  • API tokens never exposed to agents
  • Environment variable indirection for secrets
  • Hooks are fully trusted (come from WORKFLOW.md)
  • Agents cannot access other agents' workspaces

Approval Policies

PolicyBehavior
untrustedPrompt for every action
on-failureAuto-approve until failure
on-requestApprove when agent asks
neverFull auto-approve
reject{}Object form for granular rules

Recommended Hardening

OS user isolation per agent, tighter filesystem permissions, network restrictions, per-workspace approval policies.

Why Elixir/OTP?

The reference implementation uses Elixir — a deliberate architectural choice.

BEAM VM Strengths

  • Process supervision — OTP supervisors automatically restart crashed processes
  • Lightweight concurrency — thousands of processes with minimal overhead
  • Fault isolation — one agent crashing cannot bring down the orchestrator
  • Hot code reloading — update agent logic without stopping running agents
  • Built-in distribution — natural path to multi-node orchestration

OTP Patterns Used

GenServer

Orchestrator state machine: handles ticks, dispatch, retries, reconciliation in a single supervised process.

Task.async

Each agent runs as a monitored Task. The orchestrator receives :DOWN on completion/failure.

Port

Erlang Ports wrap the Codex subprocess. Provides stdio streaming with backpressure and cleanup guarantees.

What Symphony Does Not Do

The spec is explicit about scope boundaries. Symphony is an orchestrator, not a platform.

Not Included

  • No rich web UI or multi-tenant control plane
  • No prescribed approval/sandbox policies (implementation choice)
  • No built-in tracker write APIs (agents use tools to update Linear)
  • No general-purpose workflow/job scheduler features
  • No persistent database or durable state storage
  • No built-in CI/CD integration (agents handle that)

Design Philosophy

"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.

Full Architecture Flow

Putting it all together — from Linear ticket to completed PR.

Linear
Issue Tracker
→
Orchestrator
Poll + Dispatch
→
Workspace
git clone
→
Agent Runner
JSON-RPC
→
Codex
Multi-turn

Feedback Loops

  • Codex → Linear: Agent uses linear_graphql tool to update issue state, post comments
  • Orchestrator → Linear: Reconciliation checks for external state changes every tick
  • Codex → Orchestrator: Token usage, rate limits, turn completions reported via events

Concurrency Model

  • Global limit: max_concurrent_agents (default 10)
  • Per-state limit: max_concurrent_agents_by_state
  • Per-host limit: max_concurrent_agents_per_host
  • One agent per issue (enforced by claim state)

Key Takeaways

"Work management, not agent supervision"

Explore Symphony

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