The real problem
Why this is harder than it looks
Linear's API is GraphQL, the developer experience is excellent, and the documentation is thorough. Most developers estimate half a day to wire up an integration. Then they run into what the docs don't put in the headline.
Linear's OAuth token system recently underwent a significant migration. Apps created before October 2025 issued non-expiring tokens — convenient for testing, a liability in production. Apps created after that date use expiring access tokens paired with refresh tokens, and all apps are on this new model going forward. That transition sounds mechanical until you're debugging a 401 in production because your refresh logic wasn't handling the new token exchange correctly. Scalekit handles this transparently — it knows which model applies and refreshes tokens proactively, before they expire, regardless of when the underlying app was created.
Then there's actor mode. By default, mutations made via OAuth are attributed to the authorizing user — issues are created under their name, comments appear from their account. Linear also supports an actor=app parameter at authorization time, which attributes all mutations to the OAuth application itself rather than the user. For agents acting as a service across a workspace, app actor is the right model. For personal assistants acting on behalf of a specific user, user actor is correct. The critical detail: this decision is made once, at the authorization URL step — you can't change it per-call. Getting it wrong means re-authorizing all affected users.
Scalekit handles the token refresh lifecycle, actor mode configuration, per-user token isolation, and revocation detection. Your agent names a tool and passes parameters. The auth plumbing is not your problem.
Capabilities
What your agent can do with Linear
Once connected, your agent has pre-built tools for the most common Linear workflows, plus direct GraphQL access for everything else:
- Create issues with full context: title, description, team, priority, assignee, labels, story point estimate, and project association in one call
- Update issues mid-workflow: change state, reassign, reprioritize, or update any field as the work evolves
- List and filter issues: by state, assignee, priority, labels, or project, with cursor-based pagination for large result sets
- Run any GraphQL operation:
linear_graphql_query exposes Linear's full API — teams, projects, cycles, workflow states, comments, labels, and custom mutations
Setup context
What we're building
This guide connects an engineering assistant agent to Linear — helping developers and PMs create issues, triage backlogs, and track work without leaving your product.
🤖
Example agent
Engineering assistant creating and triaging Linear issues on behalf of each team member
🔐
Auth model
B2B SaaS — each user connects their own workspace. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Linear secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="linear",
identifier="user_lin_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="linear",
identifier="user_lin_456"
)
# Redirect user → Linear consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If a user revokes access from Linear's app settings, the account moves to REVOKED
— no silent failures. Check account.status before critical operations.
Bring Your Own Credentials — required
Unlike some connectors, Linear does not provide a managed OAuth app. You must register your own OAuth
application in Linear Settings → API → OAuth applications, paste the Scalekit redirect URI as the callback
URL, then enter your Client ID and Secret in the Scalekit dashboard. Token management is fully handled
after that one-time setup.
Actor mode: who does Linear attribute mutations to?
By default, issues and comments are attributed to the authorizing user. For agents acting as a service
account, Linear supports actor=app mode — all mutations are attributed to the application. Configure this
in the Scalekit dashboard when setting up the connection. It cannot be changed per-call; re-authorization
is required to switch modes.
Already have credentials?
Calling Linear
4 Calling Linear: What your agent writes
With the connected account active, your agent calls actions via execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval and request construction.
Create an issue
Only teamId and title are required. Use linear_graphql_query first to resolve your team's UUID — it differs per workspace.
result = actions.execute_tool(
identifier="user_lin_456",
tool_name="linear_issue_create",
tool_input={
"teamId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Auth token refresh fails silently on 401",
"description": "Reproduction steps: ...",
"priority": "1", # 1=Urgent, 2=High, 3=Medium, 4=Low
"assigneeId": "USER_UUID",
"estimate": "3" # story points
}
)
# Returns issue id, identifier (e.g. ENG-142), title, and URL
List issues with filtering
Fetch issues filtered by state, assignee email, priority, or project name. Results are cursor-paginated — pass after to retrieve subsequent pages.
result = actions.execute_tool(
identifier="user_lin_456",
tool_name="linear_issues_list",
tool_input={
"state": "In Progress",
"assignee": "alice@company.com",
"priority": "2", # High only
"first": 20
}
)
# { "issues": [ { "id": "...", "title": "...", "state": { "name": "In Progress" }, ... } ] }
Update an issue
Move issues through stages, reassign, or reprioritize. Only the fields you supply are changed — all others remain untouched.
result = actions.execute_tool(
identifier="user_lin_456",
tool_name="linear_issue_update",
tool_input={
"issueId": "ISSUE_UUID",
"stateId": "STATE_UUID_FOR_IN_REVIEW",
"assigneeId": "NEW_ASSIGNEE_UUID",
"priority": "1" # escalate to Urgent
}
)
Custom GraphQL query
Use linear_graphql_query for anything the named tools don't cover. Also the right way to resolve team and state UUIDs before creating issues.
result = actions.execute_tool(
identifier="user_lin_456",
tool_name="linear_graphql_query",
tool_input={
"query": """
query {
teams {
nodes {
id
name
key
states {
nodes { id name type }
}
}
}
}
"""
}
)
# Get the authenticated user — useful for self-assignment flows
viewer = actions.execute_tool(
identifier="user_lin_456",
tool_name="linear_graphql_query",
tool_input={
"query": "{ viewer { id name email } }"
}
)
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
linear_tools = get_tools(
connection_name="linear",
identifier="user_lin_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are an engineering assistant. Use the available tools to help manage Linear issues and sprints."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), linear_tools, prompt)
result = AgentExecutor(agent=agent, tools=linear_tools).invoke({
"input": "Show me all high-priority open issues assigned to Alice and move the oldest one to In Review"
})
Other frameworks supported
Tool reference
All 4 Linear tools
Grouped by function. Use linear_graphql_query to reach Linear's full API for anything the named tools don't cover.
Create a new issue. Requires teamId and title. Optional: description, assignee, priority (1–4), workflow state, label IDs, story point estimate, and project association
Update an existing issue by ID. Accepts any combination of title, description, state, assignee, and priority — only the fields supplied are changed
List issues with filtering by state name, assignee email, priority level, label names, or project name. Supports cursor-based pagination via first, after, and before
Execute any GraphQL query or mutation against the Linear API with optional variables support. Use for teams, projects, cycles, comments, workflow states, labels, and any operation not covered by the named tools
Connector notes
Linear-specific behavior
IDs are UUIDs — resolve them before creating records
Linear uses UUIDs for teams, workflow states, labels, projects, and users — not slugs. Before creating or
updating issues, use linear_graphql_query to fetch teams and states at runtime. An agent that hardcodes IDs
will silently break across different customer workspaces.
GraphQL errors return HTTP 200
Linear follows the GraphQL spec: field-level errors are returned in the errors array alongside a 200 HTTP
status. A successful HTTP response from linear_graphql_query does not mean the operation succeeded —
always check the errors field in the response before continuing with downstream logic.
No built-in sandbox environment
Linear doesn't offer a dedicated sandbox. For safe development, create a separate Linear workspace and
register a separate OAuth application for it. Use one Scalekit connection for development and another for
production — tokens issued against a dev workspace will not work in production.
Infrastructure decision
Why not build this yourself
Linear's OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Linear's token migration from non-expiring to expiring tokens — handling both models correctly across apps created before and after the migration cutoff
PROBLEM 02
Proactive refresh logic that runs before token expiry — not in response to a 401 mid-workflow, after your agent has already failed halfway through an operation
PROBLEM 03
Actor mode selection baked into the authorization URL — get it wrong and every mutation is attributed to the wrong identity, requiring re-authorization of all users to fix
PROBLEM 04
Revocation detection and graceful handling when users disconnect your app from Linear's settings — without silent failures continuing to attempt API calls on their behalf
That's one connector. Your agent product will eventually need GitHub, Jira, Slack, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.