openai-symphony-autonomous-agents

Symphony turns project work into isolated, autonomous implementation runs, allowing teams to manage work instead of supervising coding agents.

INSTALLATION
npx skills add https://github.com/aradotso/trending-skills --skill openai-symphony-autonomous-agents
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Option 1: Ask an agent to build it

Paste this prompt into Claude Code, Cursor, or Codex:

Implement Symphony according to the following spec:

https://github.com/openai/symphony/blob/main/SPEC.md

Option 2: Use the Elixir reference implementation

git clone https://github.com/openai/symphony.git

cd symphony/elixir

Follow elixir/README.md, or ask an agent:

Set up Symphony for my repository based on

https://github.com/openai/symphony/blob/main/elixir/README.md

Elixir Reference Implementation Setup

Requirements

  • Elixir + Mix installed
  • An OpenAI API key (for Codex agent)
  • A Linear API key (if using Linear integration)
  • A GitHub token (for PR operations)

Environment Variables

export OPENAI_API_KEY="sk-..."           # OpenAI API key for Codex

export LINEAR_API_KEY="lin_api_..."      # Linear integration

export GITHUB_TOKEN="ghp_..."           # GitHub PR operations

export SYMPHONY_REPO_PATH="/path/to/repo"  # Target repository

Install Dependencies

cd elixir

mix deps.get

Configuration ( elixir/config/config.exs )

import Config

config :symphony,

  openai_api_key: System.get_env("OPENAI_API_KEY"),

  linear_api_key: System.get_env("LINEAR_API_KEY"),

  github_token: System.get_env("GITHUB_TOKEN"),

  repo_path: System.get_env("SYMPHONY_REPO_PATH", "./"),

  poll_interval_ms: 30_000,

  max_concurrent_agents: 3

Run Symphony

mix symphony.start

# or in IEx for development

iex -S mix

Core Concepts

Isolated Implementation Runs

Each task gets its own isolated run:

  • Fresh git branch per task
  • Agent operates only within that branch
  • No shared state between runs
  • Proof of work collected before PR merge

Proof of Work

Before a PR is accepted, Symphony collects:

  • CI/CD pipeline status
  • PR review feedback
  • Complexity analysis
  • (optionally) walkthrough videos

Key Elixir Modules & Patterns

Starting the Symphony supervisor

# In your application.ex or directly

defmodule MyApp.Application do

  use Application

  def start(_type, _args) do

    children = [

      Symphony.Supervisor

    ]

    Supervisor.start_link(children, strategy: :one_for_one)

  end

end

Defining a Task (Symphony Task struct)

defmodule Symphony.Task do

  @type t :: %__MODULE__{

    id: String.t(),

    title: String.t(),

    description: String.t(),

    source: :linear | :manual,

    status: :pending | :running | :completed | :failed,

    branch: String.t() | nil,

    pr_url: String.t() | nil,

    proof_of_work: map() | nil

  }

  defstruct [:id, :title, :description, :source,

             status: :pending, branch: nil,

             pr_url: nil, proof_of_work: nil]

end

Spawning an Agent Run

defmodule Symphony.AgentRunner do

  @doc """

  Spawns an isolated agent run for a given task.

  Each run gets its own branch and Codex session.

  """

  def run(task) do

    branch = "symphony/#{task.id}-#{slugify(task.title)}"

    with :ok <- Git.create_branch(branch),

         {:ok, result} <- Codex.implement(task, branch),

         {:ok, pr_url} <- GitHub.open_pr(branch, task),

         {:ok, proof} <- ProofOfWork.collect(pr_url) do

      {:ok, %{task | status: :completed, pr_url: pr_url, proof_of_work: proof}}

    else

      {:error, reason} -> {:error, reason}

    end

  end

  defp slugify(title) do

    title

    |> String.downcase()

    |> String.replace(~r/[^a-z0-9]+/, "-")

    |> String.trim("-")

  end

end

Linear Integration — Polling for Tasks

defmodule Symphony.Linear.Poller do

  use GenServer

  @poll_interval Application.compile_env(:symphony, :poll_interval_ms, 30_000)

  def start_link(opts \\ []) do

    GenServer.start_link(__MODULE__, opts, name: __MODULE__)

  end

  def init(_opts) do

    schedule_poll()

    {:ok, %{processed_ids: MapSet.new()}}

  end

  def handle_info(:poll, state) do

    case Symphony.Linear.Client.fetch_todo_tasks() do

      {:ok, tasks} ->

        new_tasks = Enum.reject(tasks, &#x26;MapSet.member?(state.processed_ids, &#x26;1.id))

        Enum.each(new_tasks, &#x26;Symphony.AgentRunner.run/1)

        new_ids = Enum.reduce(new_tasks, state.processed_ids, &#x26;MapSet.put(&#x26;2, &#x26;1.id))

        schedule_poll()

        {:noreply, %{state | processed_ids: new_ids}}

      {:error, reason} ->

        Logger.error("Linear poll failed: #{inspect(reason)}")

        schedule_poll()

        {:noreply, state}

    end

  end

  defp schedule_poll do

    Process.send_after(self(), :poll, @poll_interval)

  end

end

Linear API Client

defmodule Symphony.Linear.Client do

  @linear_api "https://api.linear.app/graphql"

  def fetch_todo_tasks do

    query = """

    query {

      issues(filter: { state: { name: { eq: "Todo" } } }) {

        nodes {

          id

          title

          description

        }

      }

    }

    """

    case HTTPoison.post(@linear_api, Jason.encode!(%{query: query}), headers()) do

      {:ok, %{status_code: 200, body: body}} ->

        tasks =

          body

          |> Jason.decode!()

          |> get_in(["data", "issues", "nodes"])

          |> Enum.map(&#x26;to_task/1)

        {:ok, tasks}

      {:error, reason} ->

        {:error, reason}

    end

  end

  defp headers do

    [

      {"Authorization", System.get_env("LINEAR_API_KEY")},

      {"Content-Type", "application/json"}

    ]

  end

  defp to_task(%{"id" => id, "title" => title, "description" => desc}) do

    %Symphony.Task{id: id, title: title, description: desc, source: :linear}

  end

end

Proof of Work Collection

defmodule Symphony.ProofOfWork do

  @doc """

  Collects proof of work for a PR before it can be merged.

  Returns a map with CI status, review feedback, and complexity.

  """

  def collect(pr_url) do

    with {:ok, ci_status} <- wait_for_ci(pr_url),

         {:ok, reviews} <- fetch_reviews(pr_url),

         {:ok, complexity} <- analyze_complexity(pr_url) do

      {:ok, %{

        ci_status: ci_status,

        reviews: reviews,

        complexity: complexity,

        collected_at: DateTime.utc_now()

      }}

    end

  end

  defp wait_for_ci(pr_url, retries \\ 30) do

    case GitHub.get_pr_ci_status(pr_url) do

      {:ok, :success} -> {:ok, :success}

      {:ok, :pending} when retries > 0 ->

        Process.sleep(60_000)

        wait_for_ci(pr_url, retries - 1)

      {:ok, status} -> {:ok, status}

      {:error, reason} -> {:error, reason}

    end

  end

  defp fetch_reviews(pr_url), do: GitHub.get_pr_reviews(pr_url)

  defp analyze_complexity(pr_url), do: GitHub.get_pr_diff_complexity(pr_url)

end

Implementing the SPEC.md (Custom Implementation)

When building Symphony in another language, the spec defines:

  • Task Source — poll Linear/GitHub/Jira for tasks in a specific state
  • Agent Invocation — call Codex (or another agent) with task context
  • Isolation — each run on a fresh branch, containerized if possible
  • Proof of Work — CI, review, and analysis before merge
  • Landing — auto-merge or present to engineer for approval

Minimal implementation loop in pseudocode:

# Core symphony loop

def symphony_loop(state) do

  tasks = fetch_new_tasks(state.source)

  tasks

  |> Enum.filter(&#x26;(&#x26;1.status == :todo))

  |> Enum.each(fn task ->

    Task.async(fn ->

      branch = create_isolated_branch(task)

      invoke_agent(task, branch)         # Codex / Claude / etc.

      proof = collect_proof_of_work(branch)

      present_for_review(task, proof)

    end)

  end)

  Process.sleep(state.poll_interval)

  symphony_loop(state)

end

Common Patterns

Limiting Concurrent Agent Runs

defmodule Symphony.AgentPool do

  use GenServer

  @max_concurrent 3

  def start_link(_), do: GenServer.start_link(__MODULE__, %{running: 0, queue: []}, name: __MODULE__)

  def submit(task) do

    GenServer.cast(__MODULE__, {:submit, task})

  end

  def handle_cast({:submit, task}, %{running: n} = state) when n < @max_concurrent do

    spawn_agent(task)

    {:noreply, %{state | running: n + 1}}

  end

  def handle_cast({:submit, task}, %{queue: q} = state) do

    {:noreply, %{state | queue: q ++ [task]}}

  end

  def handle_info({:agent_done, _result}, %{running: n, queue: [next | rest]} = state) do

    spawn_agent(next)

    {:noreply, %{state | running: n, queue: rest}}

  end

  def handle_info({:agent_done, _result}, %{running: n} = state) do

    {:noreply, %{state | running: n - 1}}

  end

  defp spawn_agent(task) do

    parent = self()

    spawn(fn ->

      result = Symphony.AgentRunner.run(task)

      send(parent, {:agent_done, result})

    end)

  end

end

Manual Task Injection (No Linear)

# In IEx or a Mix task

Symphony.AgentPool.submit(%Symphony.Task{

  id: "manual-001",

  title: "Add rate limiting to API",

  description: "Implement token bucket rate limiting on /api/v1 endpoints",

  source: :manual

})

Troubleshooting

Problem

Likely Cause

Fix

Agents not spawning

Missing OPENAI_API_KEY

Check env var is exported

Linear tasks not detected

Wrong Linear state filter

Update query filter to match your board's state name

PRs not opening

Missing GITHUB_TOKEN or wrong repo

Verify token has repo scope

CI never completes

Timeout too short

Increase retries in wait_for_ci/2

Too many concurrent runs

Default pool size

Set max_concurrent_agents in config

Branch conflicts

Agent reusing branch names

Ensure task IDs are unique per run

Debug Mode

# In config/dev.exs

config :symphony, log_level: :debug

# Or at runtime

Logger.put_module_level(Symphony.AgentRunner, :debug)

Logger.put_module_level(Symphony.Linear.Poller, :debug)

Resources

  • SPEC.md — Full Symphony specification
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card