Documentation Index Fetch the complete documentation index at: https://mintlify.com/openai/symphony/llms.txt
Use this file to discover all available pages before exploring further.
Symphony is designed to be extended with custom integrations, tools, and orchestrator capabilities. This guide covers the main extension points.
Adding Custom Tracker Types
Symphony’s tracker system is abstracted to support multiple issue tracking systems. While Linear is the reference implementation, you can add support for GitHub Issues, Jira, or custom trackers.
Tracker Interface
Implement these operations for a new tracker:
fetch_candidate_issues()
Return issues in configured active states for a project. @spec fetch_candidate_issues () :: { :ok , [ Issue . t ()]} | { :error , term ()}
fetch_issues_by_states(state_names)
Return issues in specified states (used for startup terminal cleanup). @spec fetch_issues_by_states ([ String . t ()]) :: { :ok , [ Issue . t ()]} | { :error , term ()}
fetch_issue_states_by_ids(issue_ids)
Return current states for specific issue IDs (used for reconciliation). @spec fetch_issue_states_by_ids ([ String . t ()]) :: { :ok , map ()} | { :error , term ()}
Issue Normalization
Normalize tracker payloads to the Symphony issue model:
defmodule SymphonyElixir . Linear . Issue do
@type t :: % __MODULE__ {
id: String . t (),
identifier: String . t (),
title: String . t (),
description: String . t () | nil ,
priority: integer () | nil ,
state: String . t (),
branch_name: String . t () | nil ,
url: String . t () | nil ,
labels: [ String . t ()],
blocked_by: [ blocker ()],
created_at: String . t () | nil ,
updated_at: String . t () | nil
}
@type blocker :: %{
id: String . t () | nil ,
identifier: String . t () | nil ,
state: String . t () | nil
}
end
Key normalization rules:
labels → lowercase strings
priority → integer only (lower numbers = higher priority)
state → trimmed and lowercased for comparison
blocked_by → derived from tracker-specific relations
Configuration Schema
Extend the tracker configuration section:
---
tracker :
kind : github # Your custom tracker type
endpoint : https://api.github.com/graphql
api_key : $GITHUB_TOKEN
repository : org/repo
active_states :
- open
terminal_states :
- closed
---
Implementation Example
GitHub Adapter
Config Integration
defmodule SymphonyElixir . GitHub . Adapter do
@behaviour SymphonyElixir . Tracker
alias SymphonyElixir . Config
alias SymphonyElixir . GitHub .{ Client , Issue }
@impl true
def fetch_candidate_issues do
with { :ok , api_key} <- Config . tracker_api_key (),
{ :ok , repo} <- Config . tracker_repository (),
active_states <- Config . tracker_active_states () do
Client . fetch_issues (api_key, repo, active_states)
|> normalize_issues ()
end
end
@impl true
def fetch_issues_by_states (state_names) do
# Implementation
end
@impl true
def fetch_issue_states_by_ids (issue_ids) do
# Implementation
end
defp normalize_issues ({ :ok , raw_issues}) do
issues = Enum . map (raw_issues, & Issue . from_github_payload / 1 )
{ :ok , issues}
end
defp normalize_issues (error), do: error
end
Error Handling
Handle tracker-specific errors consistently:
defp handle_api_error ({ :error , % HTTPoison . Error { reason: reason}}) do
{ :error , { :github_api_request , reason}}
end
defp handle_api_error ({ :error , status}) when is_integer (status) do
{ :error , { :github_api_status , status}}
end
Dynamic tools are client-side tools that Symphony provides to Codex during app-server sessions. The linear_graphql tool is the reference implementation.
From lib/symphony_elixir/codex/dynamic_tool.ex:
Tool Schema
Tool Specs
Tool Execution
@linear_graphql_tool "linear_graphql"
@linear_graphql_description """
Execute a raw GraphQL query or mutation against Linear using Symphony's configured auth.
"""
@linear_graphql_input_schema %{
"type" => "object" ,
"additionalProperties" => false ,
"required" => [ "query" ],
"properties" => %{
"query" => %{
"type" => "string" ,
"description" => "GraphQL query or mutation document to execute against Linear."
},
"variables" => %{
"type" => [ "object" , "null" ],
"description" => "Optional GraphQL variables object." ,
"additionalProperties" => true
}
}
}
Define the tool schema
@slack_notify_tool "slack_notify"
@slack_notify_description "Send a notification to a Slack channel"
@slack_notify_input_schema %{
"type" => "object" ,
"required" => [ "channel" , "message" ],
"properties" => %{
"channel" => %{ "type" => "string" },
"message" => %{ "type" => "string" }
}
}
Add to tool_specs()
def tool_specs do
[
# ... existing tools
%{
"name" => @slack_notify_tool ,
"description" => @slack_notify_description ,
"inputSchema" => @slack_notify_input_schema
}
]
end
Implement execute() handler
def execute (tool, arguments, opts) do
case tool do
@slack_notify_tool ->
execute_slack_notify (arguments, opts)
# ... other cases
end
end
defp execute_slack_notify (arguments, _opts ) do
with { :ok , channel, message} <- normalize_slack_args (arguments),
{ :ok , response} <- SlackClient . send_message (channel, message) do
success_response (response)
else
{ :error , reason} ->
failure_response ( tool_error_payload (reason))
end
end
Register during app-server startup
Tools are advertised during the thread/start handshake in the Codex app-server protocol.
Return structured responses that Codex can interpret:
# Success response
%{
"success" => true ,
"contentItems" => [
%{
"type" => "inputText" ,
"text" => Jason . encode! (result, pretty: true )
}
]
}
# Failure response
%{
"success" => false ,
"contentItems" => [
%{
"type" => "inputText" ,
"text" => Jason . encode! (%{ "error" => "Tool execution failed" }, pretty: true )
}
]
}
Test tools independently of the app-server:
test "linear_graphql executes valid query" do
arguments = %{
"query" => "query { viewer { id name } }" ,
"variables" => %{}
}
result = DynamicTool . execute ( "linear_graphql" , arguments)
assert result[ "success" ] == true
end
test "linear_graphql handles missing query" do
arguments = %{}
result = DynamicTool . execute ( "linear_graphql" , arguments)
assert result[ "success" ] == false
assert result[ "contentItems" ] |> hd () |> get_in ([ "text" ]) =~ "requires a non-empty"
end
Extending the Orchestrator
The orchestrator is the core coordination layer. Common extension points:
Custom Concurrency Policies
Implement per-state concurrency limits:
---
agent :
max_concurrent_agents : 10
max_concurrent_agents_by_state :
"In Progress" : 5
"Merging" : 2
---
From SPEC.md section 8.3:
Per-state limit:
max_concurrent_agents_by_state[state] if present (state key normalized)
otherwise fallback to global limit
Custom Retry Policies
Extend retry logic with custom backoff strategies:
defp calculate_retry_delay (attempt, reason) do
base_delay = min ( 10_000 * :math . pow ( 2 , attempt - 1 ), max_retry_backoff ())
case reason do
{ :turn_timeout , _ } -> base_delay * 2 # Longer backoff for timeouts
{ :rate_limit , _ } -> 60_000 # Fixed 1min for rate limits
_ -> base_delay
end
end
Observability Extensions
Add custom metrics and telemetry:
defmodule SymphonyElixir . Telemetry do
def handle_event ([ :symphony , :agent , :start ], measurements, metadata, _config ) do
# Send to monitoring system
Metrics . increment ( "symphony.agent.started" ,
tags: [ issue_state: metadata.issue.state])
end
def handle_event ([ :symphony , :agent , :complete ], measurements, metadata, _config ) do
Metrics . timing ( "symphony.agent.duration" ,
measurements.duration,
tags: [ outcome: metadata.outcome])
end
end
State Machine Extensions
Add custom orchestrator states or transitions:
defmodule SymphonyElixir . Orchestrator . States do
# Standard states
@unclaimed :unclaimed
@running :running
@retry_queued :retry_queued
# Custom states
@manual_approval :manual_approval
@quarantined :quarantined
def transition (issue, event) do
case { current_state (issue), event} do
{ :running , :approval_required } -> @manual_approval
{ :running , :suspicious_activity } -> @quarantined
# ... standard transitions
end
end
end
HTTP Server Extensions
The optional HTTP server can be extended with custom endpoints:
defmodule SymphonyElixir . HttpServer . CustomRoutes do
use Plug . Router
plug :match
plug :dispatch
get "/api/v1/custom/metrics" do
metrics = collect_custom_metrics ()
send_json (conn, 200 , metrics)
end
post "/api/v1/custom/trigger/:issue_id" do
issue_id = conn.params[ "issue_id" ]
# Trigger custom action
send_json (conn, 200 , %{ triggered: issue_id})
end
end
Mount in the main router:
plug SymphonyElixir . HttpServer . CustomRoutes
Configuration Extensions
Add custom configuration sections:
---
# Standard config
tracker :
kind : linear
# Custom extension
monitoring :
datadog_api_key : $DATADOG_API_KEY
trace_sampling_rate : 0.1
notifications :
slack_webhook : $SLACK_WEBHOOK
alert_on_failure : true
---
Parse in config module:
defmodule SymphonyElixir . Config do
def monitoring_config do
workflow_config ()
|> Map . get ( "monitoring" , %{})
|> parse_monitoring_config ()
end
defp parse_monitoring_config (section) do
%{
datadog_api_key: resolve_env_var (section[ "datadog_api_key" ]),
trace_sampling_rate: section[ "trace_sampling_rate" ] || 0.1
}
end
end
Best Practices
Maintain SPEC.md Alignment Keep extensions compatible with the language-agnostic specification. Document deviations clearly.
Test in Isolation Unit test extensions independently before integrating with the orchestrator.
Handle Errors Gracefully Return structured errors that the orchestrator can retry or surface appropriately.
Document Configuration Add configuration schema, defaults, and examples for any new settings.
Examples from Production
Complete implementation in lib/symphony_elixir/codex/dynamic_tool.ex:
Handles both string and object input formats
Validates required query field
Normalizes GraphQL errors array to success: false
Returns structured JSON response
Memory Tracker (Testing)
In-memory tracker for testing in lib/symphony_elixir/tracker/memory.ex:
defmodule SymphonyElixir . Tracker . Memory do
@behaviour SymphonyElixir . Tracker
def start_link (initial_issues \\ []) do
Agent . start_link ( fn -> initial_issues end , name: __MODULE__ )
end
@impl true
def fetch_candidate_issues do
issues = Agent . get ( __MODULE__ , & &1 )
{ :ok , issues}
end
end
Additional Resources