Linear Integration
Pilot monitors your Linear workspace for issues with a specific label and automatically implements them, creating pull requests and posting status updates back to Linear.
Setup
1. Create a Linear API Key
- Go to Linear → Settings → API → Personal API keys
- Click Create key
- Copy the key — you won’t see it again
Use a dedicated service account for production. Personal API keys are scoped to the user who created them.
2. Find Your Team ID
Your team ID is the short prefix on your issues (e.g., APP for APP-42).
3. Configure Pilot
Add the Linear section to your config file:
# ~/.pilot/config.yaml
linear:
enabled: true
workspaces:
- name: my-team
api_key: ${LINEAR_API_KEY}
team_id: APP
pilot_label: pilot
auto_assign: true4. Create the pilot Label in Linear
In your Linear workspace, create a label called pilot (or whatever you set in pilot_label). Pilot also uses these status labels automatically:
| Label | Purpose |
|---|---|
pilot | Triggers Pilot to pick up the issue |
pilot-in-progress | Applied while Pilot is working |
pilot-done | Applied after successful completion |
pilot-failed | Applied if execution fails |
Configuration
Multi-Workspace Setup
Pilot can monitor multiple Linear workspaces simultaneously. Each workspace has its own API key, team, and label configuration:
linear:
enabled: true
polling:
enabled: true
interval: 30s
workspaces:
- name: frontend
api_key: ${LINEAR_API_KEY_FRONTEND}
team_id: FE
pilot_label: pilot
project_ids:
- "project-uuid-1"
projects:
- web-app
auto_assign: true
- name: backend
api_key: ${LINEAR_API_KEY_BACKEND}
team_id: BE
pilot_label: pilot
projects:
- api-server
auto_assign: false
polling:
enabled: true
interval: 60sConfiguration Reference
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the Linear adapter |
workspaces | array | — | List of workspace configurations |
polling.enabled | bool | true | Enable polling for new issues |
polling.interval | duration | 30s | How often to check for new issues |
Workspace Fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Identifier for this workspace |
api_key | string | required | Linear API key |
team_id | string | required | Linear team key (e.g., APP) |
pilot_label | string | "pilot" | Label that triggers Pilot |
project_ids | string[] | — | Filter by Linear project IDs |
projects | string[] | — | Pilot project names to map to |
auto_assign | bool | false | Auto-assign issues to the API key owner |
polling.enabled | bool | inherited | Override top-level polling |
polling.interval | duration | inherited | Override top-level interval |
Legacy Single-Workspace Config
For backward compatibility, you can use the flat configuration format:
linear:
enabled: true
api_key: ${LINEAR_API_KEY}
team_id: APP
pilot_label: pilot
project_ids:
- "project-uuid-1"
auto_assign: true
polling:
enabled: true
interval: 30sThis is internally converted to a single workspace named "default".
If both workspaces and top-level api_key are set, the workspaces array takes precedence.
Polling
When polling is enabled, Pilot periodically queries Linear for issues matching:
- Team: matches the configured
team_id - Label: has the
pilot_label(default:pilot) - State: is in
backlog,unstarted, orstarted
Issues are processed oldest-first (by creation date). Each workspace can override the polling interval.
Project Filtering
If project_ids is set, only issues belonging to those Linear projects are picked up. Issues without a project are skipped.
Webhooks
Pilot also supports real-time issue detection via Linear webhooks.
Setting Up Webhooks
- Configure your webhook URL in Linear: Settings → API → Webhooks
- Set the URL to your Pilot instance:
https://your-pilot.example.com/webhooks/linear - Select Issues as the resource type
Supported Events
| Event | Behavior |
|---|---|
Issue.create | If the new issue has the pilot label, Pilot processes it |
Issue.update | Ignored |
Issue.delete | Ignored |
Multi-Workspace Webhook Routing
When using multiple workspaces, Pilot routes incoming webhooks to the correct workspace handler based on the team.id field in the payload. If the team ID is missing, Pilot falls back to trying each workspace handler in sequence.
Priority mapping
Linear’s numeric issue priority maps directly to Pilot’s priority system:
| Value | Linear Priority | Pilot Priority |
|---|---|---|
| 0 | No Priority | None |
| 1 | Urgent | Urgent |
| 2 | High | High |
| 3 | Medium | Medium |
| 4 | Low | Low |
Issue States and Priority
State Types
Pilot filters issues by state type during polling:
| State Type | Description | Polled |
|---|---|---|
backlog | In the backlog | Yes |
unstarted | Not yet started | Yes |
started | In progress | Yes |
completed | Done | No |
canceled | Canceled | No |
Status Notifications
Pilot posts these comments on a Linear issue (polling mode):
| When | Emoji | Comment |
|---|---|---|
| Task dispatched | 🤖 | “Pilot started working on this issue”. Posted when the issue is picked up, before the admission gates run, so a gated decline can still leave this one comment |
PR / MR opened (completed) | ✅ | “Pilot completed this task!” with the pull request or merge request URL |
No changes needed (no_op) | ℹ️ | ”Pilot finished this task without opening a pull request” — no changes were needed. Never reported as a failure |
Branch held for review (needs_human) | ⏸️ | ”Pilot held this task for human review” — names the branch (pilot/<identifier>) and the reason from the execution. When the branch push failed, the comment says the branch is not on the remote instead of calling it pushed |
| Canceled / superseded | ℹ️ | ”Pilot stopped this task: it was canceled” (operator cancel) or “…it was superseded” (issue closed or replaced during the run) |
| Completed without a PR / MR | ℹ️ | ”Pilot finished this task without opening a pull request” — the run completed but produced no pull request; names the branch when there is one |
Any other outcome (failed, rate_limited, stalled, declined, skipped, …) | ❌ | “Pilot could not complete this task” with a short reason: the execution status and the first line of the error (for example rate_limited: …), with absolute paths replaced by <path> and capped at 200 characters. The full error chain stays in the Pilot logs |
An admission-gate decline (the task is already queued or running, is in repick backoff, or already has a terminal completion) gets no further comment and no state move. The 🤖 “started” comment is posted before the gates run, so a declined pickup can still leave that one comment; an issue with no project mapping gets neither. The issue’s state is left exactly as it was, so a Done issue that is re-offered on every poll tick is never flipped back to In Progress. Phase-by-phase progress comments are not posted.
Workflow state transitions
Besides comments, Pilot moves the issue’s native workflow state as the task progresses, so the board reflects reality. This needs no configuration: the defaults use Linear’s state types, which are stable across teams (state names are not).
| Moment | Config key | Default when unset |
|---|---|---|
| Task dispatched | in_progress | The team’s first started-type state (lowest position) |
| PR / MR opened | in_review | No move (comment only) |
| PR merged | done | The team’s first completed-type state |
failed, rate_limited, stalled and other non-success outcomes | failed | No move (comment only) |
no_op, canceled, or completed without a PR / MR | failed if set | Back to the state the issue had before the dispatch move |
superseded (issue closed or replaced during the run) | none | No move: comment only, so a close made by a person is never undone |
needs_human (branch held for review) | in_review if set | No move: the issue stays in the started state |
The comment and the move follow the execution’s classified outcome, not just “succeeded or not” and whether a PR exists. The dispatch move happens only once the task has passed every admission gate and won its claim — after the “started working” comment, and never for a gated decline. A run that ends without a PR or MR (no changes needed, operator cancel) would otherwise stay in the started state forever; Pilot posts the matching comment above and moves the issue to the configured failed state when one is set, otherwise back to the state it had before the dispatch move. That prior state is remembered in memory, so a daemon restart mid-run loses it and the issue gets the comment only. A run superseded because the issue was closed or replaced never moves the issue: whoever closed it owns its state.
Each value is a workflow-state name (for example In Review) or a state UUID; an empty value uses the default. State names are plain strings — do not put a $ in them, because config values are environment-expanded when loaded.
adapters:
linear:
enabled: true
api_key: "${LINEAR_API_KEY}"
team_id: "APP"
transitions:
in_progress: "In Progress"
in_review: "In Review"
# done: defaults to the team's completed-type state
# failed: no move unless set
workspaces:
- name: appbooster
team_id: APP
transitions: # overrides the top level, per key
done: "Shipped"transitions can be set at the top level (applies to the legacy single workspace and is the default for every workspaces[] entry) and per workspace (a non-empty key overrides the top level). A configured name that matches no state of the team is logged as a WARN at startup.
Every attempted move logs Linear state transition at INFO with issue, to and leg (start, pr, merge, failed or restore); a failure logs a WARN. A failed move never stops a task or blocks a merge. If no INFO line follows a dispatch, the move did not run.
The done move happens when autopilot merges (or observes the merge of) a pilot/LIN-* pull request. Pull requests autopilot does not observe — today, all GitLab merge requests — never get the merge move; for those, in_review is the last state Pilot can set.
The pilot-in-progress, pilot-done and pilot-failed labels are unchanged; the native state is additive.
Project Mapping
There are two ways to map Linear projects to Pilot projects: project-level (recommended) and workspace-level (legacy).
Project-Level Mapping (Recommended)
Map each Pilot project directly to its Linear project using linear.project_id in the project config. This gives deterministic 1-to-1 mapping:
projects:
- name: aso-generator
path: ~/Projects/aso-generator
github:
owner: my-org
repo: aso-generator
linear:
project_id: "linear-project-uuid-abc"
- name: pilot
path: ~/Projects/pilot
github:
owner: my-org
repo: pilot
linear:
project_id: "linear-project-uuid-def"When a Linear issue belongs to linear-project-uuid-abc, Pilot resolves it to the aso-generator project directly.
Workspace-Level Mapping (Legacy)
You can also map at the workspace level using project_ids and projects arrays:
workspaces:
- name: appbooster
team_id: APP
project_ids:
- "linear-project-uuid-abc"
- "linear-project-uuid-def"
projects:
- aso-generator
- pilotIf only one Pilot project is mapped, all issues from that workspace use it.
Autopilot Integration (v2.10.0)
When Pilot creates a PR from a Linear issue, the autopilot controller automatically monitors CI status and can auto-merge the PR — the same pipeline that GitHub issues get.
This is powered by the OnPRCreated callback on the Linear poller. After a successful execution produces a PR, the callback fires and hands off to autopilot for CI monitoring, status checks, and optional auto-merge. The callback only fires on successful execution — failed tasks do not trigger it.
Previously, autopilot CI monitoring and auto-merge were GitHub-only. As of v2.10.0, Linear tasks get the full autopilot pipeline automatically with no extra configuration.
Processed Issue Persistence (v2.10.0)
The Linear poller uses a ProcessedStore to persist which issues have already been handled. This prevents re-dispatching issues after a restart or hot upgrade.
The store is backed by SQLite (the same memory backend used by the rest of Pilot) and is enabled automatically — no configuration required.
Resolution Precedence
When an issue arrives, Pilot resolves the target project in this order:
- Project-level
linear.project_id— exact match on issue’s Linear project ID - Workspace-level
project_ids/projects— workspace-scoped mapping - Generic matching — match by Linear project name or team key
- Default project — first configured project as fallback
Project-level mapping takes priority over workspace-level mapping. If both are configured, the project-level linear.project_id wins.