This directory contains the current Elixir/OTP implementation of Symphony, based on
SPEC.md at the repository root.
Warning
Symphony Elixir is prototype software intended for evaluation only and is presented as-is.
We recommend implementing your own hardened version based on SPEC.md.
- Polls Linear for candidate work
- Creates a workspace per issue
- Launches Codex in App Server mode inside the workspace
- Sends a workflow prompt to Codex
- Refreshes new external Linear comments between turns for active sessions
- Keeps Codex working on the issue until the work is done
During app-server sessions, Symphony serves client-side Linear tools:
linear_graphqlfor raw GraphQL accesslinear_create_commentfor supported issue replieslinear_update_commentfor supported workpad/comment edits
Continuation turns prepend a compact New Linear comments since last turn section when Symphony
detects fresh external comments while the issue remains active.
If a claimed issue moves to a terminal state (Done, Closed, Cancelled, or Duplicate),
Symphony stops the active agent for that issue and cleans up matching workspaces.
- Make sure your codebase is set up to work well with agents: see Harness engineering.
- Get a new personal token in Linear via Settings → Security & access → Personal API keys, and
set it as the
LINEAR_API_KEYenvironment variable. - Copy this directory's
WORKFLOW.mdto your repo. - Optionally copy the
commit,push,pull,land, andlinearskills to your repo.- The
linearskill expects Symphony'slinear_graphqlapp-server tool for raw Linear GraphQL operations such as comment editing or upload flows.
- The
- Customize the copied
WORKFLOW.mdfile for your project.- To get your project's slug, right-click the project and copy its URL. The slug is part of the URL.
- When creating a workflow based on this repo, note that it depends on non-standard Linear issue statuses: "Rework", "Human Review", and "Merging". You can customize them in Team Settings → Workflow in Linear.
- Follow the instructions below to install the required runtime dependencies and start the service.
One command to set up everything on Ubuntu 22.04/24.04:
curl -fsSL https://raw.githubusercontent.com/markoinla/symphony/main/install.sh | sudo bashThis installs Docker, Node.js, Claude Code CLI, and GitHub CLI, then starts Symphony with
Postgres. Workflow files are installed to ~/.symphony/workflows/ where you can edit
them — changes are picked up automatically.
After install:
- Open
http://<your-server-ip>:4000/setupand create your admin account (email + password) - Settings → Connect Linear OAuth
- Projects → Create a project (name, GitHub repo, Linear org/project slug)
To add more users:
docker exec symphony-app-1 mix symphony.create_user user@example.com secretpassword --name "Full Name"Default workflow files are installed to ~/.symphony/workflows/:
~/.symphony/workflows/
├── WORKFLOW.md
├── ENRICHMENT.md
├── EPIC_SPLITTER.md
├── MENTION.md
├── REVIEW.md
└── TRIAGE.md
Edit these files to customize agent behavior — Symphony hot-reloads changes automatically.
To add a new workflow, drop any .md file with valid YAML front matter into the directory.
Update — pulls latest images and restarts services:
sudo bash /opt/symphony/install.sh --updateReset authentication — removes all user accounts so you can re-run /setup:
sudo bash /opt/symphony/install.sh --reset-passwordUninstall — stops all services and removes data:
sudo bash /opt/symphony/install.sh --uninstallView logs:
cd /opt/symphony && docker compose -f docker-compose.prod.yml logs -fOn your server, install and authenticate:
- Docker and Docker Compose
- Claude CLI — installed and logged in (
claude auth login) - GitHub CLI — installed and logged in (
gh auth login)
git clone https://github.com/markoinla/symphony.git
cd symphony
echo "HOST_HOME=$HOME" > .env
docker compose up -dOpen http://localhost:4000:
- Visit
/setupto create your admin account (email + password) - Settings → Connect Linear OAuth
- Projects → Create a project (name, GitHub repo, Linear org/project slug)
- Create an issue in Linear — Symphony picks it up
To update: docker compose pull && docker compose up -d
- Add GHCR registry in Dokploy → Settings → Registry:
- URL:
ghcr.io, Username: your GitHub username, Password: GitHub PAT withread:packagesscope
- URL:
- Create service → Docker Compose → GitHub →
markoinla/symphony, branchmain, path./docker-compose.yml - Environment variables:
HOST_HOME=/home/youruser - Deploy, then configure Linear OAuth and projects in the dashboard
Pushes to main auto-build the Docker image via GitHub Actions and trigger a Dokploy redeploy.
We recommend using mise to manage Elixir/Erlang versions.
mise install
mise exec -- elixir --versiongit clone https://github.com/markoinla/symphony.git
cd symphony
mise trust
mise install
mise exec -- mix setup
mise exec -- mix build
mise exec -- ./bin/symphony ./WORKFLOW.mdPass one or more workflow file paths to ./bin/symphony when starting the service:
./bin/symphony /path/to/custom/WORKFLOW.md
./bin/symphony /path/to/WORKFLOW.md /path/to/ENRICHMENT.md
./bin/symphony --workflows /path/to/workflows/If no path is passed, Symphony defaults to ./WORKFLOW.md. When multiple workflow files are
provided, Symphony starts one orchestrator per workflow while sharing the same TaskSupervisor,
SQLite store, PubSub, and dashboard.
Optional flags:
--workflowsexpands a directory of*.mdworkflow files (or accepts an explicit workflow file)--logs-roottells Symphony to write logs under a different directory (default:./log)--portalso starts the Phoenix observability service (default: disabled)
The WORKFLOW.md file uses YAML front matter for configuration, plus a Markdown body used as the
Codex session prompt.
Minimal example:
---
tracker:
kind: linear
filter_by: project
project_slug: "..."
picked_up_label_name: "symphony"
workspace:
root: ~/code/workspaces
hooks:
after_create: |
git clone git@github.com:your-org/your-repo.git .
agent:
max_concurrent_agents: 10
max_turns: 20
codex:
command: codex app-server
---
You are working on a Linear issue {{ issue.identifier }}.
Title: {{ issue.title }} Body: {{ issue.description }}Notes:
- If a value is missing, defaults are used.
- Safer Codex defaults are used when policy fields are omitted:
codex.approval_policydefaults to{"reject":{"sandbox_approval":true,"rules":true,"mcp_elicitations":true}}codex.thread_sandboxdefaults toworkspace-writecodex.turn_sandbox_policydefaults to aworkspaceWritepolicy rooted at the current issue workspace
- Supported
codex.approval_policyvalues depend on the targeted Codex app-server version. In the current local Codex schema, string values includeuntrusted,on-failure,on-request, andnever, and object-formrejectis also supported. - Supported
codex.thread_sandboxvalues:read-only,workspace-write,danger-full-access. - When
codex.turn_sandbox_policyis set explicitly, Symphony passes the map through to Codex unchanged. Compatibility then depends on the targeted Codex app-server version rather than local Symphony validation. agent.max_turnscaps how many back-to-back Codex turns Symphony will run in a single agent invocation when a turn completes normally but the issue is still in an active state. Default:20.- If the Markdown body is blank, Symphony uses a default prompt template that includes the issue identifier, title, and body.
- Use
hooks.after_createto bootstrap a fresh workspace. For a Git-backed repo, you can rungit clone ... .there, along with any other setup commands you need. - If a hook needs
mise execinside a freshly cloned workspace, trust the repo config and fetch the project dependencies inhooks.after_createbefore invokingmiselater from other hooks. tracker.api_keyreads fromLINEAR_API_KEYwhen unset or when value is$LINEAR_API_KEY.tracker.filter_bydefaults toproject. Settracker.filter_by: labeltogether withtracker.label_nameto poll issues by label instead of project slug.tracker.picked_up_label_nameoptionally adds that Linear label when Symphony successfully picks up an issue and prepares its workspace.- For path values,
~is expanded to the home directory. - For env-backed path values, use
$VAR.workspace.rootresolves$VARbefore path handling, whilecodex.commandstays a shell command string and any$VARexpansion there happens in the launched shell.
tracker:
api_key: $LINEAR_API_KEY
filter_by: label
label_name: enrich
workspace:
root: $SYMPHONY_WORKSPACE_ROOT
hooks:
after_create: |
git clone --depth 1 "$SOURCE_REPO_URL" .
codex:
command: "$CODEX_BIN app-server --model gpt-5.3-codex"- If
WORKFLOW.mdis missing or has invalid YAML at startup, Symphony does not boot. - If a later reload fails, Symphony keeps running with the last known good workflow for that file and logs the reload error until the file is fixed.
server.portor CLI--portenables the optional Phoenix observability service, which serves the React dashboard SPA at/plus the JSON and SSE API under/api/v1/*.- The Dashboard URL setting (configurable in the dashboard Settings page) sets the external
base URL used in Linear session links, e.g.
http://my-server:4000.
The observability UI now runs as a React SPA on a minimal Phoenix stack:
- Vite + React serves the dashboard experience from a static bundle under
/ - TanStack Query and SSE drive the live dashboard and session views
- JSON API endpoints power dashboard, history, projects, settings, and session data under
/api/v1/* - Bandit serves both the API and the built SPA assets
Development:
mix phx.server
cd dashboard && npm run devProduction:
mix assets.build
mix releaselib/: application code and Mix taskstest/: ExUnit coverage for runtime behaviorWORKFLOW.md: in-repo workflow contract used by local runs../.codex/: repository-local Codex skills and setup helpers
mix compile --warnings-as-errors && mix format --check-formatted && mix lintRun the real external end-to-end test only when you want Symphony to create disposable Linear
resources and launch a real codex app-server session:
cd elixir
export LINEAR_API_KEY=...
SYMPHONY_RUN_LIVE_E2E=1 mix test test/symphony_elixir/live_e2e_test.exsOptional environment variables:
SYMPHONY_LIVE_LINEAR_TEAM_KEYdefaults toSYME2ESYMPHONY_LIVE_SSH_WORKER_HOSTSuses those SSH hosts when set, as a comma-separated list
The E2E test runs two live scenarios:
- one with a local worker
- one with SSH workers
If SYMPHONY_LIVE_SSH_WORKER_HOSTS is unset, the SSH scenario uses docker compose to start two
disposable SSH workers on localhost:<port>. The live test generates a temporary SSH keypair,
mounts the host ~/.codex/auth.json into each worker, verifies that Symphony can talk to them
over real SSH, then runs the same orchestration flow against those worker addresses. This keeps
the transport representative without depending on long-lived external machines.
Set SYMPHONY_LIVE_SSH_WORKER_HOSTS to target real SSH hosts instead.
The live test creates a temporary Linear project and issue, writes a temporary WORKFLOW.md, runs
a real agent turn, verifies the workspace side effect, requires Codex to comment on and close the
Linear issue, then marks the project completed so the run remains visible in Linear.
Elixir is built on Erlang/BEAM/OTP, which is great for supervising long-running processes. It has an active ecosystem of tools and libraries. It also supports hot code reloading without stopping actively running subagents, which is very useful during development.
Launch codex in your repo, give it the URL to the Symphony repo, and ask it to set things up for
you.
This project is licensed under the Apache License 2.0.
Updated at: 2026-03-26T05:08Z
