# How the Symphony Linear Tracker Fetches Issues: GraphQL Integration Explained

> Discover how the Symphony Linear tracker fetches issues using GraphQL integration. Learn about API polling, response normalization, and efficient data handling for your workflow.

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

---

**The Symphony Linear tracker fetches issues by implementing the `SymphonyElixir.Tracker` behaviour, which delegates to `SymphonyElixir.Linear.Client` to execute a GraphQL poll query against Linear's API, then normalizes the response into `SymphonyElixir.Linear.Issue` structs for downstream processing.**

The OpenAI Symphony repository orchestrates AI-driven development workflows by connecting to external issue trackers. When configured for Linear, the system uses a modular architecture to retrieve candidate issues through a type-safe Elixir client that abstracts the Linear GraphQL API.

## Tracker Behaviour and Adapter Selection

Symphony defines a generic interface for issue trackers through the `SymphonyElixir.Tracker` behaviour. When the orchestrator needs to fetch work items, it calls `SymphonyElixir.Tracker.fetch_candidate_issues/0`, which dynamically selects the appropriate adapter based on runtime configuration.

In [`elixir/lib/symphony_elixir/tracker.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/tracker.ex), the `adapter/0` function reads the configured tracker kind (typically "linear") and returns the `SymphonyElixir.Linear.Adapter` module. This design allows Symphony to support multiple trackers without modifying the orchestrator logic.

The Linear adapter, defined in [`elixir/lib/symphony_elixir/linear/adapter.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/adapter.ex), implements the required callbacks by delegating to its corresponding client module:

```elixir

# Inside the Linear adapter (adapter.ex)

def fetch_candidate_issues, do: client_module().fetch_candidate_issues()

```

## GraphQL Query Construction

The actual API interaction happens in [`elixir/lib/symphony_elixir/linear/client.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/client.ex). The `fetch_candidate_issues/0` function constructs and executes a GraphQL query named **SymphonyLinearPoll**. This query filters issues by project slug and state name, requesting only items marked as candidates (such as "To Do" or "In Progress").

The query variables include:

- `projectSlug`: Extracted from `Config.settings!().linear.project_slug` (defined in [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md))
- `stateNames`: The list of candidate states from configuration
- `first`: Pagination limit (typically 50)
- `after`: Cursor for pagination (initially `nil`)

```graphql
query SymphonyLinearPoll(
  $projectSlug: String!,
  $stateNames: [String!]!,
  $first: Int!,
  $after: String
) {
  issues(
    filter: {
      project: {slug: {eq: $projectSlug}},
      state: {name: {in: $stateNames}}
    },
    first: $first,
    after: $after
  ) {
    nodes { … }
    pageInfo { … }
  }
}

```

## Authentication and HTTP Transport

Before executing the query, the client retrieves authentication credentials from `Config.settings!().linear.api_key`, which populates from the `LINEAR_API_KEY` environment variable or the [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) configuration file. If the token is missing, the orchestrator logs an error and aborts the fetch operation.

The client then posts the GraphQL payload to Linear's API endpoint using the configured token in the Authorization header.

## Response Normalization

Upon receiving the HTTP response, `SymphonyElixir.Linear.Client` parses the JSON body and extracts the `nodes` array from the `issues` field. Each node is transformed into a `%SymphonyElixir.Linear.Issue{}` struct through the `Linear.Issue.from_graphql/1` function defined in [`elixir/lib/symphony_elixir/linear/issue.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/issue.ex).

This struct normalizes essential fields including the issue ID, title, description, current state, and assignee information. The adapter then returns `{:ok, [issue_structs]}` to the tracker behaviour, or `{:error, reason}` if any step fails, allowing the orchestrator to handle pagination errors or API timeouts gracefully.

## Practical Implementation Example

The following example demonstrates the complete call flow from the orchestrator perspective:

```elixir

# High‑level call used by the orchestrator

case SymphonyElixir.Tracker.fetch_candidate_issues() do
  {:ok, issues} ->
    IO.inspect(issues, label: "Candidate Linear issues")
  {:error, reason} ->
    IO.puts("Failed to fetch Linear issues: #{inspect(reason)}")
end

```

The underlying client implementation handles the GraphQL execution and struct mapping:

```elixir

# Inside the Linear client (client.ex)

def fetch_candidate_issues do
  query = @poll_query   # the SymphonyLinearPoll query shown above

  variables = %{
    "projectSlug" => Config.settings!().linear.project_slug,
    "stateNames"  => Config.settings!().linear.candidate_states,
    "first"       => 50,
    "after"       => nil
  }

  with {:ok, resp} <- http_post(query, variables),
       %{"data" => %{"issues" => %{"nodes" => nodes}}} <- resp do
    {:ok, Enum.map(nodes, &Linear.Issue.from_graphql/1)}
  else
    error -> {:error, error}
  end
end

```

## Summary

- **Adapter Pattern**: `SymphonyElixir.Tracker` uses a configurable adapter pattern to delegate Linear-specific operations to `SymphonyElixir.Linear.Adapter` without coupling the orchestrator to the external API.
- **GraphQL Polling**: The client executes the `SymphonyLinearPoll` query with parameterized filters for project slug and candidate states, enabling precise issue selection.
- **Environment Configuration**: Authentication relies on the `LINEAR_API_KEY` environment variable or [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) settings, with explicit error handling for missing credentials.
- **Struct Normalization**: Raw GraphQL nodes convert to `%SymphonyElixir.Linear.Issue{}` structs in [`elixir/lib/symphony_elixir/linear/issue.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/issue.ex), providing type-safe data for the orchestration engine.

## Frequently Asked Questions

### What GraphQL query does Symphony use to fetch Linear issues?

Symphony uses a query named **SymphonyLinearPoll** defined in [`elixir/lib/symphony_elixir/linear/client.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/client.ex). This query accepts a project slug and list of state names as variables, returning the first N issues matching those criteria with pagination support via cursor-based offsets.

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

The tracker authenticates using a Linear API token retrieved from `Config.settings!().linear.api_key`. This value populates from the `LINEAR_API_KEY` environment variable or the [`WORKFLOW.md`](https://github.com/openai/symphony/blob/main/WORKFLOW.md) configuration file, and is attached to the HTTP Authorization header when executing the GraphQL request.

### Which module converts the GraphQL response into Elixir structs?

The `SymphonyElixir.Linear.Issue` module in [`elixir/lib/symphony_elixir/linear/issue.ex`](https://github.com/openai/symphony/blob/main/elixir/lib/symphony_elixir/linear/issue.ex) handles normalization. It provides a `from_graphql/1` function that maps the raw GraphQL node fields (id, title, description, state, assignee) into a typed `%SymphonyElixir.Linear.Issue{}` struct used throughout the Symphony system.

### What happens if the Linear API request fails?

If the HTTP request fails or returns malformed data, `SymphonyElixir.Linear.Client` returns an `{:error, reason}` tuple instead of the success tuple. The adapter propagates this error back to the `SymphonyElixir.Tracker` behaviour, which surfaces the failure to the orchestrator for appropriate logging and error handling.