This guide is the practical onboarding path for using LinearSDK against a
real Linear workspace.
It is based on:
- the upstream
linear/linearrepository checked locally - the current open-source Symphony Linear integration under
~/p/g/n/symphony - Linear's current public docs for API keys and SDK authentication
From the linear/linear monorepo:
- the top-level SDK README mostly defers users to
developers.linear.app - the concrete first-party example in the repo is
examples/nextjs-file-upload, which expectsLINEAR_API_KEY - the TypeScript SDK docs describe two auth modes:
- personal API key
- OAuth access token
- the first-party SDK still expects you to bring the token, but Linear's official docs also define OAuth authorization, refresh, actor mode, and client-credentials flows for apps
That is the model this Elixir SDK follows too.
Symphony's Linear adapter is much narrower than the full schema. The live surfaces it exercises are:
viewer- used to resolve
"me"when assignee routing is enabled
- used to resolve
issues(...)- project-scoped polling by project
slugIdand workflow state names
- project-scoped polling by project
issue(id: ...)- used to inspect an issue and resolve its team workflow states
commentCreate- used to leave progress or handoff comments
issueUpdate- used to move issues between workflow states
The examples in examples/ cover those same surfaces.
LinearSDK does:
- default to the production Linear GraphQL endpoint
- expose a provider-local
api_key:shortcut for personal API keys - expose a provider-local
access_token:shortcut for OAuth access tokens - expose
LinearSDK.OAuthfor authorization URLs, token exchange, refresh, and client-credentials flows - support runtime-managed OAuth token sources through
oauth2: - let you execute raw GraphQL documents directly
LinearSDK does not:
- create API keys for you
- host your OAuth callback endpoint for you
- own durable install records or secret authority for you
- alter your Linear workspace setup for you
The example suite does auto-discover a project slug and issue when your workspace already has accessible data. That is an example-layer convenience, not hidden SDK state.
In Linear:
- Open
Settings. - Go to
Security & access. - Find
Personal API keys. - Create a new key and copy it immediately.
Export it locally:
export LINEAR_API_KEY=lin_api_...Then verify basic connectivity:
mix run examples/viewer.exsIf you prefer OAuth, you can use:
LinearSDK.Client.new!(access_token: System.fetch_env!("LINEAR_OAUTH_ACCESS_TOKEN"))If you want the SDK to help with the provider-edge OAuth mechanics, use
LinearSDK.OAuth.
Keep the model straight:
- the OAuth app is the Linear-side app configuration
- the app gives you
client_id,client_secret, and redirect URIs - the OAuth access token is returned after authorization and is the credential you actually send to GraphQL
- for the generated GraphQL side of the app record itself, use
mix run examples/oauth_application_info.exs
For most human-operated setups, prefer the example helper:
examples/run_all.sh --setup-oauth
examples/run_all.sh --oauth
examples/run_all.shThat helper expands to:
export LINEAR_OAUTH_CLIENT_ID="..."
export LINEAR_OAUTH_CLIENT_SECRET="..."
export LINEAR_OAUTH_REDIRECT_URI="http://127.0.0.1:40071/callback"
mix linear.oauth --save --manual --no-browserIf you have a literal loopback redirect URI and the optional callback-listener
dependencies are installed, mix linear.oauth can also capture the callback
directly. Otherwise it falls back to the same manual paste-back flow.
For mutation examples, use:
examples/run_all.sh --setup-oauth-write
examples/run_all.sh --oauth-write
examples/run_all.sh --with-writeIf you want direct OAuth examples instead of the helper wrapper, use:
mix run examples/oauth_authorize_url.exs
mix run examples/oauth_exchange_code.exs
mix run examples/oauth_saved_token_viewer.exs
mix run examples/oauth_refresh_and_viewer.exsLinear's current OAuth docs describe three app-facing patterns:
- authorization-code flow for user or app installation approval
- refresh-token flow for renewable user tokens
- client-credentials flow for app-to-app automation when enabled on the Linear app
Linear also supports OAuth actor authorization. Passing actor=app during the
authorization flow makes later mutations appear as the app instead of the
authorizing user. That is the relevant mode for many agentic and service-style
workloads.
Build an authorization request:
{:ok, request} =
LinearSDK.OAuth.authorization_request(
client_id: System.fetch_env!("LINEAR_OAUTH_CLIENT_ID"),
redirect_uri: System.fetch_env!("LINEAR_OAUTH_REDIRECT_URI"),
scopes: ["read", "write"],
actor: :app,
generate_state: true,
pkce: true
)Exchange the callback code:
{:ok, token} =
LinearSDK.OAuth.exchange_code(
System.fetch_env!("LINEAR_OAUTH_AUTH_CODE"),
client_id: System.fetch_env!("LINEAR_OAUTH_CLIENT_ID"),
client_secret: System.get_env("LINEAR_OAUTH_CLIENT_SECRET"),
redirect_uri: System.fetch_env!("LINEAR_OAUTH_REDIRECT_URI"),
pkce_verifier: System.get_env("LINEAR_OAUTH_PKCE_VERIFIER")
)Persist the token in a runtime-managed file:
:ok =
Prismatic.Adapters.TokenSource.File.put(
token,
path: LinearSDK.OAuthTokenFile.default_path(),
create_dirs?: true
)Then create a client from that saved token:
client =
LinearSDK.Client.new!(
oauth2: [
token_source:
{Prismatic.Adapters.TokenSource.File,
path: LinearSDK.OAuthTokenFile.default_path()}
]
)The live examples can now use that same saved token file directly if
LINEAR_API_KEY is unset.
For the SDK examples:
LINEAR_API_KEYis the only always-required variableLINEAR_PROJECT_SLUGis optional; the examples auto-discover one when possible- if no project slug is available, the candidate-issues example falls back to a workspace-scoped query so the read-only suite still works
LINEAR_ISSUE_REFis optional; the examples auto-discover one when possibleLINEAR_TARGET_STATEis optional; state lookup defaults to the current state, while the transition example auto-picks a different workflow state when possible
For Symphony itself, the requirement is stricter:
tracker.project_slugis required in Symphony's runtime config because its candidate issue polling is intentionally project-scoped
Open the Linear project in your browser and copy its URL. The final path segment is the project slug Symphony uses.
Example:
https://linear.app/acme/project/customer-portal-4f2a8c1d9e6b
In that URL, the project slug is:
customer-portal-4f2a8c1d9e6b
Export it:
export LINEAR_PROJECT_SLUG=customer-portal-4f2a8c1d9e6bFor issue-focused examples, use a value such as ENG-123 or a Linear issue
UUID. This guide calls that value LINEAR_ISSUE_REF.
export LINEAR_ISSUE_REF=ENG-123The write examples first resolve that reference to the canonical internal issue ID before mutating anything.
Use the exact workflow state name from Linear, for example:
TodoIn ProgressDoneHuman Review
Export the target value when needed:
export LINEAR_TARGET_STATE="In Progress"Symphony also supports assignee-based routing. The live poll example mirrors that behavior with:
- unset
LINEAR_ASSIGNEE- return all matching issues
LINEAR_ASSIGNEE=me- resolve the current
viewerand keep only issues assigned to that user
- resolve the current
LINEAR_ASSIGNEE=<linear-user-id>- keep only issues assigned to that user ID
Full read-only suite:
examples/run_all.shCurrent user:
mix run examples/viewer.exsSymphony-style candidate polling:
export LINEAR_ACTIVE_STATES="Todo,In Progress"
mix run examples/symphony_candidate_issues.exsIssue lookup and target-state resolution:
export LINEAR_ISSUE_REF=ENG-123
export LINEAR_TARGET_STATE="In Progress"
mix run examples/symphony_state_lookup.exsThese examples are safe and read-only.
For the full example suite with writes enabled, prefer:
examples/run_all.sh --with-writeLow-level equivalent:
export LINEAR_CONFIRM_WRITE=1Comment on an issue:
export LINEAR_ISSUE_REF=ENG-123
export LINEAR_COMMENT_BODY="Live test comment from LinearSDK examples"
export LINEAR_CONFIRM_WRITE=1
mix run examples/symphony_comment.exsTransition an issue:
export LINEAR_ISSUE_REF=ENG-123
export LINEAR_CONFIRM_WRITE=1
mix run examples/symphony_transition_issue.exsOptionally pin the transition target explicitly:
export LINEAR_ISSUE_REF=ENG-123
export LINEAR_TARGET_STATE="In Progress"
export LINEAR_CONFIRM_WRITE=1
mix run examples/symphony_transition_issue.exsThose scripts perform real mutations against Linear.
- Create a personal API key in Linear and export
LINEAR_API_KEY. - Run
examples/run_all.shor start withmix run examples/viewer.exs. - If you want to pin the polling example to a specific project, export
LINEAR_PROJECT_SLUG. - If you want to pin the issue-focused examples to a specific issue, export
LINEAR_ISSUE_REF. - Inspect the resolved issue and state with
mix run examples/symphony_state_lookup.exs. - Only after that, try the comment and transition examples with
examples/run_all.sh --with-writeor the low-levelLINEAR_CONFIRM_WRITE=1.
Personal API key:
client =
LinearSDK.Client.new!(
api_key: System.fetch_env!("LINEAR_API_KEY")
)OAuth access token:
client =
LinearSDK.Client.new!(
access_token: System.fetch_env!("LINEAR_OAUTH_ACCESS_TOKEN")
)OAuth token source:
client =
LinearSDK.Client.new!(
oauth2: [
token_source:
{Prismatic.Adapters.TokenSource.File,
path: LinearSDK.OAuthTokenFile.default_path()}
]
)Raw header escape hatch:
client =
LinearSDK.Client.new!(
auth: {:header, "Authorization", System.fetch_env!("LINEAR_API_KEY")}
)