Skip to main content
Symphony integrates with Linear via GraphQL to poll for issues, track state transitions, and coordinate agent lifecycles. This guide covers authentication, project configuration, and state mapping.

Getting Your Linear API Key

1

Open Linear Settings

Navigate to Settings → Security & access → Personal API keys in Linear.
2

Create a new key

Click Create key, give it a descriptive name like “Symphony Orchestrator”, and copy the generated token.
Linear only shows the token once. Store it securely.
3

Set the environment variable

Export the token in your shell:
export LINEAR_API_KEY="lin_api_..."
Or add it to your shell profile (~/.bashrc, ~/.zshrc):
echo 'export LINEAR_API_KEY="lin_api_..."' >> ~/.bashrc

Configuring the API Key

Symphony reads the Linear API key from the tracker.api_key field in WORKFLOW.md. By default, it references the LINEAR_API_KEY environment variable:
tracker:
  kind: linear
  api_key: $LINEAR_API_KEY  # Default behavior
The $LINEAR_API_KEY syntax tells Symphony to read from the environment variable. You can also hard-code a token (not recommended) or use a different variable name.

Alternative: Direct Token

tracker:
  api_key: lin_api_hardcoded_token_here
Never commit hardcoded API keys to version control.

Alternative: Custom Environment Variable

tracker:
  api_key: $MY_CUSTOM_LINEAR_TOKEN

Finding Your Project Slug

The project_slug identifies which Linear project Symphony should poll for issues.
1

Open your project in Linear

Navigate to the project you want Symphony to monitor.
2

Copy the project URL

Right-click the project name in the sidebar and select Copy link.The URL format is:
https://linear.app/<team>/project/<slug>-<hash>
For example:
https://linear.app/openai/project/symphony-0c79b11b75ea
3

Extract the slug

The slug is everything after /project/:
symphony-0c79b11b75ea
4

Add to WORKFLOW.md

tracker:
  kind: linear
  project_slug: "symphony-0c79b11b75ea"
The slug is stable and won’t change even if you rename the project.

State Management

Symphony maps Linear workflow states to agent lifecycle actions. You define two state lists:

Active States

Issues in active states are candidates for agent work. Symphony polls Linear for issues in these states.
tracker:
  active_states:
    - Todo
    - In Progress
    - Merging
    - Rework
From lib/symphony_elixir/config.ex:9:
@default_active_states ["Todo", "In Progress"]
Default: ["Todo", "In Progress"]

Terminal States

Issues moving to terminal states trigger workspace cleanup and agent shutdown.
tracker:
  terminal_states:
    - Closed
    - Cancelled
    - Duplicate
    - Done
From lib/symphony_elixir/config.ex:10:
@default_terminal_states ["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]
Default: ["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]
Note the inclusion of both "Cancelled" and "Canceled" to handle regional spelling differences.

Custom Linear States

Symphony’s reference WORKFLOW.md uses custom states that don’t exist in default Linear projects:
  • Rework: Agent addresses reviewer feedback
  • Human Review: PR is ready and waiting for human approval
  • Merging: Human approved; agent executes merge
From elixir/README.md:43:
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.

Creating Custom States

1

Open Linear Team Settings

Navigate to Team Settings → Workflow in Linear.
2

Add custom states

Create new states for your workflow:
  • Human Review (between In Progress and Done)
  • Rework (parallel to In Progress)
  • Merging (between Human Review and Done)
3

Update active_states in WORKFLOW.md

tracker:
  active_states:
    - Todo
    - In Progress
    - Human Review
    - Rework
    - Merging

Assignee Filtering

Optionally filter issues by assignee. This is useful when multiple Symphony instances run against the same project.
tracker.assignee
string
Filter to issues assigned to a specific user.Use "me" to match the API token owner:
tracker:
  assignee: me
Use a Linear user ID for a specific person:
tracker:
  assignee: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
From lib/symphony_elixir/linear/client.ex:449-450:
"me" ->
  resolve_viewer_assignee_filter()
When assignee: me is configured, Symphony makes a GraphQL viewer query on startup to resolve the user ID.
Without assignee filtering, Symphony considers all issues in the project that match the configured states.

GraphQL Endpoint

Symphony uses Linear’s public GraphQL API:
tracker:
  endpoint: https://api.linear.app/graphql  # Default
You rarely need to customize this unless using a Linear proxy or enterprise endpoint.

Issue Polling

Symphony polls Linear at regular intervals:
polling:
  interval_ms: 5000  # Poll every 5 seconds
From lib/symphony_elixir/config.ex:25:
@default_poll_interval_ms 30_000  # 30 seconds
Default: 30 seconds (30,000 milliseconds)
Lower intervals mean faster response to new issues but increase API load. 5-30 seconds is typical for active development.

Issue Data Retrieved

From lib/symphony_elixir/linear/client.ex:12-55, Symphony fetches:
query SymphonyLinearPoll($projectSlug: String!, $stateNames: [String!]!) {
  issues(filter: {project: {slugId: {eq: $projectSlug}}, state: {name: {in: $stateNames}}}) {
    nodes {
      id
      identifier
      title
      description
      priority
      state { name }
      branchName
      url
      assignee { id }
      labels { nodes { name } }
      inverseRelations {
        nodes {
          type
          issue {
            id
            identifier
            state { name }
          }
        }
      }
      createdAt
      updatedAt
    }
  }
}

Key Fields

  • inverseRelations: Captures “blocks” relationships for dependency tracking
  • branchName: Git branch associated with the issue
  • assignee: Used for assignee filtering
  • labels: Available in prompt templates for routing logic

Validation

Symphony validates Linear configuration on startup: From lib/symphony_elixir/config.ex:367-370:
:ok <- require_tracker_kind(),
:ok <- require_linear_token(),
:ok <- require_linear_project(),

Required Fields

  • tracker.kind must be "linear"
  • tracker.api_key must resolve to a non-empty string
  • tracker.project_slug must be present
Missing configuration halts startup with explicit error messages:
{:error, :missing_linear_api_token}
{:error, :missing_linear_project_slug}

Testing the Connection

Verify your Linear setup:
mise exec -- iex -S mix
iex> SymphonyElixir.Config.linear_api_token()
"lin_api_..."

iex> SymphonyElixir.Config.linear_project_slug()
"symphony-0c79b11b75ea"

iex> SymphonyElixir.Linear.Client.fetch_candidate_issues()
{:ok, [%SymphonyElixir.Linear.Issue{identifier: "MT-123", ...}]}
If you see {:error, :missing_linear_api_token}, check your environment variable. If you see {:error, :missing_linear_project_slug}, verify your WORKFLOW.md.

Complete Example

tracker:
  kind: linear
  api_key: $LINEAR_API_KEY
  project_slug: "symphony-0c79b11b75ea"
  assignee: me
  active_states:
    - Todo
    - In Progress
    - Human Review
    - Rework
    - Merging
  terminal_states:
    - Closed
    - Cancelled
    - Duplicate
    - Done

polling:
  interval_ms: 5000
This configuration:
  • Authenticates with the LINEAR_API_KEY environment variable
  • Polls the symphony-0c79b11b75ea project
  • Only picks up issues assigned to the API token owner
  • Checks Linear every 5 seconds
  • Manages 5 active states and 4 terminal states

Build docs developers (and LLMs) love