Skip to main content
Symphony’s architecture is built around several core concepts that work together to provide reliable, isolated agent execution.

The Orchestrator

The orchestrator is the heart of Symphony—a single authoritative process that owns all scheduling decisions.

Responsibilities

Polling

Runs a tick every configured interval (default 30 seconds) to fetch candidate issues from Linear

Dispatch

Decides which issues to assign to agents based on eligibility rules and concurrency limits

State Tracking

Maintains in-memory runtime state for all running sessions, retry queues, and metrics

Reconciliation

Continuously checks issue states in Linear and stops agents when issues become ineligible

Orchestration States

Each issue in Symphony moves through distinct orchestration states (separate from Linear issue states):
  • Unclaimed – Issue is not running and has no retry scheduled
  • Claimed – Orchestrator has reserved the issue to prevent duplicate dispatch
  • Running – Worker task exists and agent session is active
  • RetryQueued – Worker stopped but scheduled to retry after backoff delay
  • Released – Claim removed because issue is terminal, non-active, or completed
A successful agent exit doesn’t mean the issue is done forever. If the issue remains in an active Linear state, Symphony schedules a continuation retry to give the agent another chance to complete the work.

Concurrency Control

Symphony provides two levels of concurrency control: Global Limit
agent:
  max_concurrent_agents: 10
Per-State Limits
agent:
  max_concurrent_agents_by_state:
    "in progress": 5
    "rework": 3
State names are normalized (trimmed and lowercased) for matching. If no per-state limit is configured, the global limit applies.

Workspaces

Workspaces are isolated directory environments where agents execute their work.

Workspace Layout

Each issue gets its own workspace directory:
<workspace.root>/
├── ABC-123/          # Workspace for issue ABC-123
│   ├── .git/
│   ├── src/
│   └── ...
├── ABC-124/          # Workspace for issue ABC-124
│   ├── .git/
│   └── ...
└── ABC-125/
The workspace directory name is derived from the issue identifier with sanitization:
  • Only [A-Za-z0-9._-] characters are allowed
  • All other characters are replaced with _

Workspace Lifecycle

1

Creation

When Symphony first encounters an issue, it creates the workspace directory. This happens only once per issue.
2

after_create Hook

If configured, runs setup commands (like git clone) in the new workspace. This hook runs only when the directory is first created.
3

Reuse Across Runs

The same workspace is reused for all subsequent runs of the same issue—retries, continuations, and rework cycles.
4

before_run Hook

Runs before each agent attempt, after workspace preparation. Useful for syncing latest changes or rebuilding dependencies.
5

Agent Execution

The coding agent runs with the workspace directory as its working directory (cwd).
6

after_run Hook

Runs after each agent attempt completes (success or failure). Failures are logged but ignored.
7

Cleanup

When an issue reaches a terminal state, Symphony runs the before_remove hook and deletes the workspace directory.

Safety Invariants

Critical safety rules that all implementations must enforce:
  1. Agent runs only in workspace path – Before launching the agent subprocess, validate that cwd == workspace_path
  2. Workspace stays inside root – All workspace paths must have workspace_root as a prefix directory after normalization
  3. Workspace key is sanitized – Only alphanumeric, dot, underscore, and hyphen characters allowed in directory names
These invariants prevent agents from accidentally modifying files outside their designated workspace.

Workspace Hooks

Hooks are shell scripts that run at specific points in the workspace lifecycle:
hooks:
  after_create: |
    git clone --depth 1 https://github.com/your-org/repo.git .
    npm install
  
  before_run: |
    git fetch origin main
    git rebase origin/main
  
  after_run: |
    git push --force-with-lease
  
  before_remove: |
    mix workspace.before_remove
  
  timeout_ms: 60000
  • Hooks execute in a shell context with the workspace directory as cwd
  • On POSIX systems, hooks run via bash -lc <script>
  • Hook timeout is configurable (default: 60 seconds)
  • after_create and before_run failures abort the current attempt
  • after_run and before_remove failures are logged but ignored

Workflow Files

The WORKFLOW.md file is the contract between Symphony and your repository.

File Format

A WORKFLOW.md file contains YAML front matter for configuration and a Markdown body for the agent prompt:
---
tracker:
  kind: linear
  project_slug: "your-project"
workspace:
  root: ~/symphony-workspaces
agent:
  max_concurrent_agents: 10
---

You are working on issue {{ issue.identifier }}.

Title: {{ issue.title }}
Description: {{ issue.description }}

Instructions:
1. Analyze the issue requirements
2. Implement the solution
3. Run tests and validation
4. Create a pull request

Front Matter Configuration

The YAML front matter accepts these top-level sections:
tracker:
  kind: linear                    # Currently only "linear" supported
  endpoint: https://api.linear.app/graphql
  api_key: $LINEAR_API_KEY       # Environment variable reference
  project_slug: "my-project"     # Required for Linear
  active_states:
    - Todo
    - In Progress
  terminal_states:
    - Closed
    - Done
polling:
  interval_ms: 30000  # Check for new work every 30 seconds
workspace:
  root: ~/code/workspaces  # Supports ~ expansion and $ENV_VARS
agent:
  max_concurrent_agents: 10
  max_turns: 20                    # Max continuation turns per session
  max_retry_backoff_ms: 300000     # 5 minutes
  max_concurrent_agents_by_state:
    "in progress": 5
codex:
  command: "codex app-server"
  approval_policy: never
  thread_sandbox: workspace-write
  turn_sandbox_policy:
    type: workspaceWrite
  turn_timeout_ms: 3600000     # 1 hour per turn
  stall_timeout_ms: 300000     # 5 minutes without events = stalled
hooks:
  after_create: "git clone ..."
  before_run: "git pull --rebase"
  after_run: "git push"
  before_remove: "cleanup_script.sh"
  timeout_ms: 60000

Prompt Template

The Markdown body is rendered as a template with these variables: issue – The normalized issue object:
{{ issue.identifier }}   # "ABC-123"
{{ issue.title }}        # "Fix login bug"
{{ issue.description }}  # Full issue body
{{ issue.state }}        # "In Progress"
{{ issue.url }}          # Linear issue URL
{{ issue.priority }}     # 1-4 (lower is higher priority)
{{ issue.labels }}       # Array of label strings
{{ issue.blocked_by }}   # Array of blocker refs
attempt – Retry/continuation metadata:
{% if attempt %}
This is retry attempt #{{ attempt }}.
Resume from current workspace state.
{% endif %}
Templates use Liquid-compatible syntax. Unknown variables or filters cause rendering to fail, preventing silent errors.

Dynamic Reload

Symphony watches WORKFLOW.md for changes and reloads configuration dynamically without restart. Changes apply to:
  • Future dispatch decisions
  • Polling interval (affects next tick scheduling)
  • Concurrency limits
  • Agent launch parameters
  • Prompt content for new sessions
Running agent sessions are not automatically restarted when config changes.

Linear Integration

Symphony integrates deeply with Linear to fetch work and track progress.

Issue Selection

On each poll tick, Symphony fetches candidate issues that meet these criteria:
1

Has Required Fields

Issue must have id, identifier, title, and state
2

Active State

Issue state must be in the configured active_states list and not in terminal_states
3

Not Already Running

Issue is not already claimed or running in Symphony’s orchestrator state
4

Blocker Rule for Todo

If issue state is “Todo”, all blockers must be in terminal states. Issues with active blockers are skipped.
5

Concurrency Available

Global and per-state concurrency slots must be available

Dispatch Priority

Eligible issues are sorted before dispatch:
  1. Priority ascending (1 is highest, 4 is lowest; null sorts last)
  2. Created at oldest first
  3. Identifier lexicographic (tie-breaker)
This ensures high-priority, older issues are picked up first.

Issue Normalization

Symphony normalizes Linear’s GraphQL responses into a stable issue model:
{
  id: "abc123",              // Stable tracker-internal ID
  identifier: "ABC-123",     // Human-readable key
  title: "Fix login bug",
  description: "When users...",
  priority: 1,               // 1-4, or null
  state: "In Progress",
  branch_name: "fix/login",  // From Linear metadata
  url: "https://linear.app/...",
  labels: ["bug", "auth"],  // Normalized to lowercase
  blocked_by: [              // Derived from inverse relations
    {
      id: "def456",
      identifier: "ABC-100",
      state: "Done"
    }
  ],
  created_at: "2026-03-01T10:00:00Z",
  updated_at: "2026-03-04T15:30:00Z"
}

State Reconciliation

Every poll tick, Symphony reconciles running agent sessions: Part A: Stall Detection
  • If no Codex events received within stall_timeout_ms, terminate the worker and queue retry
  • Disabled if stall_timeout_ms <= 0
Part B: Tracker State Refresh
  • Fetch current states for all running issue IDs
  • If issue moved to terminal state → stop agent and clean workspace
  • If issue moved to non-active state → stop agent, keep workspace
  • If issue still active → update in-memory snapshot and continue
This reconciliation prevents agents from continuing work on issues that humans have marked as complete, cancelled, or blocked.

Tracker Write Boundary

Important design principle:Symphony does not write to Linear directly. All ticket mutations (state transitions, comments, PR links) are performed by the coding agent using tools defined in the workflow prompt.Symphony remains a scheduler and runner—it reads from Linear to discover work but delegates all tracker writes to the agents.
This separation allows workflow prompts to define exactly how agents should interact with Linear and GitHub for your team’s specific processes.

Agent Sessions

An agent session is a single execution of a coding agent for one issue.

Session Lifecycle

1

Workspace Preparation

Ensure workspace directory exists and run after_create hook if newly created
2

Prompt Rendering

Render the workflow template with issue data and attempt number
3

Before Run Hook

Execute before_run hook if configured
4

Launch App Server

Start Codex in app-server mode via bash -lc <codex.command> with workspace as cwd
5

Initialize Session

Send initialize, initialized, thread/start, and turn/start protocol messages
6

Stream Turn

Read line-delimited JSON from stdout until turn completes, fails, times out, or is cancelled
7

Continuation Turns

If turn succeeds and issue still active, start additional turns (up to max_turns) on the same thread
8

After Run Hook

Execute after_run hook when session ends (success or failure)
9

Report Outcome

Send completion/failure event to orchestrator for retry/continuation scheduling

Session Metadata

While an agent session runs, Symphony tracks:
session_id: "thread-abc-turn-123"  # Composed thread_id-turn_id
thread_id: "thread-abc"            # Persistent across continuation turns
turn_id: "turn-123"                # Unique per turn
codex_app_server_pid: "12345"     # Process ID
last_codex_event: "turn_completed" # Latest protocol event
last_codex_timestamp: "2026-03-04T16:45:30Z"
last_codex_message: {...}          # Summarized payload
turn_count: 7                      # Number of turns in this worker lifetime
codex_input_tokens: 15234
codex_output_tokens: 8901
codex_total_tokens: 24135
This metadata powers observability surfaces (logs, dashboards, metrics).

Retry and Continuation

Symphony distinguishes between two retry scenarios: Continuation Retry (after normal exit):
  • Agent completed a turn successfully
  • Issue is still in an active Linear state
  • Symphony schedules a short retry (~1 second) to re-check and possibly start another session
  • Uses same workspace, increments attempt counter
Failure Retry (after error/timeout):
  • Agent failed, timed out, or was cancelled
  • Symphony schedules exponential backoff retry: min(10000 * 2^(attempt-1), max_retry_backoff_ms)
  • Default max backoff: 5 minutes
Both retry types reuse the existing workspace—agents can resume from partial work.

Next Steps

Quick Start

Get Symphony running in your environment

Workflow Configuration

Learn how to customize your WORKFLOW.md file

Build docs developers (and LLMs) love