# How Symphony Manages Linear Issue State Transitions: A Deep Dive into the GraphQL Adapter

> Discover how Symphony manages Linear issue state transitions using its GraphQL adapter. Learn about resolved target states and atomic issueUpdate mutations for seamless workflow management.

- Repository: [OpenAI/symphony](https://github.com/openai/symphony)
- Tags: deep-dive
- Published: 2026-05-08

---

**Symphony orchestrates Linear issue state transitions through a dedicated GraphQL adapter that resolves target state IDs via team-specific queries and executes atomic `issueUpdate` mutations, returning `:ok` on success or tagged error tuples on failure.**

The openai/symphony repository treats Linear as a first-class issue tracker, enabling automated workflow progression through Elixir's `SymphonyElixir.Tracker` behaviour. When the orchestrator determines that a Linear issue must advance to a different workflow state—such as when a Codex run completes or a worker reports failure—it delegates the transition to the **Linear adapter**, a thin GraphQL wrapper that handles ID resolution and mutation execution.

## The Five-Step Transition Architecture

The transition flow moves through distinct architectural layers, ensuring separation between orchestration logic and external API concerns.

1. **Orchestrator Initiation** – The orchestrator calls `SymphonyElixir.Tracker.update_issue_state/2` with the Linear issue ID and desired **state name** (as defined in [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md)).

2. **Tracker Delegation** – The generic `Tracker` module forwards the call to the active tracker implementation.

3. **State ID Resolution** – For Linear projects, `SymphonyElixir.Linear.Adapter.update_issue_state/2` executes `@state_lookup_query` to search the issue's team for a matching state name, returning the internal `stateId`.

4. **Mutation Execution** – The adapter fires `@update_state_mutation` (the `issueUpdate` GraphQL mutation) with the resolved state ID.

5. **Result Handling** – The function returns `:ok` if the mutation reports `"success": true`, otherwise `{:error, :issue_update_failed}`.

All GraphQL interactions flow through `Linear.Client.graphql/3`, which builds payloads, injects the `Authorization` header from [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) configuration, and executes via `Req.post/2`.

## Resolving State IDs from Workflow Names

Unlike hardcoded GraphQL IDs, Symphony uses human-readable state names configured in [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md). The adapter must map these names to Linear's internal identifiers before mutating the issue.

When `SymphonyElixir.Linear.Adapter.update_issue_state/2` receives a transition request, it first calls an internal `resolve_state_id/2` helper. This function executes the module's `@state_lookup_query`, which searches the specific team's workflow states for a name match:

```elixir

# Internal resolution - performed automatically by the adapter

{:ok, state_id} = SymphonyElixir.Linear.Adapter.resolve_state_id(issue_id, "In Review")

# state_id now contains the GraphQL ID required by Linear's API

```

This lookup is defined in [`elixir/lib/symphony_elixir/linear/adapter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/adapter.ex) (lines 26-38), ensuring that workflow changes in Linear don't require code redeployments—only configuration updates.

## Executing the State Change Mutation

With the resolved `stateId`, the adapter performs the actual transition using the `@update_state_mutation` constant. The mutation follows Linear's GraphQL schema, accepting `issueId` and `stateId` parameters:

```elixir
mutation = """
mutation SymphonyUpdateIssueState($issueId: String!, $stateId: String!) {
  issueUpdate(id: $issueId, input: {stateId: $stateId}) {
    success
  }
}
"""

```

The adapter calls `SymphonyElixir.Linear.Client.graphql/3` to send this mutation, passing the `issueId` and resolved `stateId` as variables. Error handling distinguishes between network failures, GraphQL errors, and logical failures (like invalid state IDs), logging the HTTP status and truncated response body for debugging.

## Practical Implementation Examples

### Updating State from a Worker Process

After a successful Codex run, workers can trigger state transitions through the high-level Tracker API:

```elixir

# Move issue to "Done" after completion

case SymphonyElixir.Tracker.update_issue_state(issue_id, "Done") do
  :ok ->
    IO.puts("Issue #{issue_id} successfully moved to Done")
    
  {:error, reason} ->
    IO.puts("Failed to update issue state: #{inspect(reason)}")
end

```

This call chains through `Tracker` to `Linear.Adapter.update_issue_state/2` as implemented in [`elixir/lib/symphony_elixir/linear/adapter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/adapter.ex) (lines 61-74).

### Direct GraphQL Debugging

For troubleshooting or custom scripts, bypass the adapter and use the client directly:

```elixir
mutation = """
mutation SymphonyUpdateIssueState($issueId: String!, $stateId: String!) {
  issueUpdate(id: $issueId, input: {stateId: $stateId}) {
    success
  }
}
"""

{:ok, resp} = SymphonyElixir.Linear.Client.graphql(mutation, %{
  "issueId" => issue_id,
  "stateId" => state_id
})

IO.inspect(resp)

```

This matches the `@update_state_mutation` used internally by the adapter (lines 18-24 in the adapter source).

## Configuration and Authentication

The Linear adapter relies on credentials and workflow definitions stored in [`elixir/WORKFLOW.md`](https://github.com/openai/symphony/blob/main/elixir/WORKFLOW.md). The `Linear.Client` module extracts the API key from this configuration to populate the `Authorization` header for all requests. This centralized config determines which states are considered active or terminal, driving the orchestrator's transition decisions.

## Summary

- **Symphony manages Linear issue state transitions** through a behavior-based architecture separating orchestration from API implementation.
- **State name resolution** occurs via GraphQL queries against team-specific workflows in [`elixir/lib/symphony_elixir/linear/adapter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/adapter.ex), allowing configuration-driven state management without code changes.
- **The Linear adapter** implements the tracker contract through `update_issue_state/2`, handling both ID lookup and mutation execution before returning explicit `:ok` or `{:error, reason}` tuples.
- **Authentication and logging** are handled centrally in [`elixir/lib/symphony_elixir/linear/client.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/client.ex), which injects API keys from [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) and captures truncated error responses for debugging.

## Frequently Asked Questions

### What happens if the requested state name doesn't exist in Linear?

The `resolve_state_id/2` function fails to find a matching state in the team's workflow, causing `update_issue_state/2` to return `{:error, :issue_update_failed}` before executing any mutation. The orchestrator can catch this error and handle the misconfiguration appropriately without corrupting the Linear issue state.

### How does Symphony authenticate with the Linear API?

Authentication occurs through the `Linear.Client.graphql/3` helper, which retrieves the API key from [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) configuration and injects it into the `Authorization` header of every HTTP request sent via `Req.post/2` to Linear's GraphQL endpoint.

### Can developers use the Linear adapter outside of the orchestrator?

Yes. While the orchestrator calls `SymphonyElixir.Tracker.update_issue_state/2`, developers can interact directly with `SymphonyElixir.Linear.Adapter.update_issue_state/2` or the lower-level `Linear.Client.graphql/3` for custom scripts and debugging, as shown in the direct GraphQL debugging example above.

### What information is logged when a state transition fails?

The system logs the HTTP status code and a truncated version of the response body when GraphQL mutations fail, enabling rapid debugging of authentication issues, invalid state IDs, or Linear API outages without exposing full response payloads in production logs.