How the Symphony Linear Tracker Fetches Issues: GraphQL Integration Explained

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, 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, implements the required callbacks by delegating to its corresponding client module:


# 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. 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)
  • stateNames: The list of candidate states from configuration
  • first: Pagination limit (typically 50)
  • after: Cursor for pagination (initially nil)
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 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.

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:


# 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:


# 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 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, 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. 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →