ADE's Linear integration attaches a Linear workspace to the CTO autonomous orchestration layer. It ingests issues from Linear, matches each issue against user-defined workflow definitions, dispatches the matched work as an ADE mission, employee chat session, worker run, or PR resolution, and closes the issue back out (state transition, comment, artifact links) when the run terminates.
This document describes the shape of the integration: who participates, which services own what, which tables store state, and how the desktop app and the headless ADE CLI run the same pipeline.
The full Linear stack — credential service, GraphQL client, issue tracker,
template service, workflow file loader, flow policy, routing, intake,
outbound, dispatcher, sync, ingress, and closeout — runs inside the
runtime daemon that owns the project. The desktop renderer is a viewer
over window.ade.cto.linear* IPC channels, and the headless ADE CLI
hosts the same services through apps/ade-cli/src/headlessLinearServices.ts
so Linear-driven workflows can run in ade serve without the desktop
app open.
Both the desktop main process (for local projects) and the standalone
ade serve daemon load the same service modules out of
apps/desktop/src/main/services/cto/; the path reflects the source
tree, not where execution happens.
The webhook HTTP listener (linearIngressService), the relay poller,
and the reconciliation timer (linearSyncService) all bind on the
runtime host. A remote runtime behind a NAT therefore needs the relay
path even if the desktop machine has a public webhook URL.
The integration is used by four distinct consumers:
- The CTO agent. Linear workflows are authored, saved, and rolled back
through the CTO tab's flow-policy surface, and the CTO agent is the
default supervisor for review gates (
reviewerIdentityKey: "cto"). Linear runs show up in the CTO's history and feed theawaiting_human_reviewandawaiting_delegationqueues that operators resolve from the CTO tab. - Missions. When a Linear workflow's target type is
mission, the dispatcher launches a mission throughmissionService/aiOrchestratorService, links the mission back to theLinearWorkflowRunrow, and waits for mission completion before moving on to PR gates or closeout. - Lanes, commits, PRs, and chat. A user can attach a Linear issue
to a brand-new lane from
CreateLaneDialog(the Linear issue picker in the always-open Advanced section), or to chat context from the composer's Linear attach affordance. Once a lane is connected to an issue, ADE auto-derives the branch name, prefixes commit messages withRefs IDENT: …, seeds the PR title (IDENT: title), and adds aFixes IDENT/Refs IDENTmagic word to the PR body so Linear links the PR back to the issue. There is also a top-barLinearQuickViewButtonthat opens the sameLinearIssueBrowserthe chat composer uses, lets the operator filter / search across their Linear backlog, and turns any selected issue into a new lane in one click. - The headless ADE CLI.
apps/ade-cli/src/headlessLinearServices.tsinstantiates the full Linear service stack (sync, dispatcher, closeout, intake, ingress, routing, outbound, templates) so external callers can trigger and resolve Linear runs without the desktop UI running. The ADE CLI exposes these over JSON-RPC tools such aslistLinearWorkflows,resolveLinearRunAction,routeLinearIssueToCto,routeLinearIssueToMission, andresolveLinearSyncQueueItem.
Linear (webhook / polled issues)
|
v
+--------------------+
| linearIngressService| (webhook HTTP listener + relay poller)
+--------------------+
|
v
+--------------------+ fetchIssueById
| linearSyncService | <---------------------+
+--------------------+ |
| |
v |
+--------------------+ flow policy + |
| linearRoutingService| trigger match |
+--------------------+ |
| |
v |
+-----------------------+ |
| linearDispatcherService| <-------+ |
+-----------------------+ | |
| | |
v | |
+-----------------------+ launches | |
| missionService / |----------+ |
| agentChatService / | |
| workerAgentService / | |
| prService | |
+-----------------------+ |
| |
v |
+-----------------------+ |
| linearCloseoutService |--------------------+
+-----------------------+ (comment / state / artifacts)
The two "inputs" into sync are: a relay/webhook event from
linearIngressService (calls syncService.processIssueUpdate(issueId)), or
the timer-based reconciliation pass inside linearSyncService itself that
polls intake on reconciliationIntervalSec (clamped to a 15s floor, default
30s).
When no Linear token is stored, the entire pipeline sits idle. The sync
service is created with autoStart: false unless credentials are present,
and hasCredentials: () => linearCredentialService.getStatus().tokenStored
is passed in so every cycle short-circuits. No HTTP listener binds, no
reconciliation timer fires, no background CPU is consumed. Enabling the
integration is a deliberate act of storing a token (manual paste or OAuth)
in the CTO tab connection panel.
A LinearWorkflowDefinition has six main parts:
- Triggers —
assignees,labels,projectSlugs,teamKeys,priority,stateTransitions,owner,creator,metadataTags. Values inside a trigger group are OR-ed together; populated groups are AND-ed. Empty groups are ignored. - Routing —
metadataTagsapplied to the run andwatchOnly: truewhich records a match but launches no work. - Target — what to create.
typeis one ofmission,employee_session,worker_run,pr_resolution,review_gate. Other target fields set executor kind (cto/employee/worker), run mode (autopilot/assisted/manual), lane selection (primary/fresh_issue_lane/operator_prompt), session reuse policy, optionaldownstreamTargetfor multi-stage chains, andprStrategyfor targets that create PRs. - Steps — ordered
LinearWorkflowStep[]the dispatcher walks through. Types arecomment_linear,set_linear_state,set_linear_assignee,apply_linear_label,launch_target,wait_for_target_status,wait_for_pr,attach_artifacts,request_human_review,complete_issue,reopen_issue,emit_app_notification. - Closeout — success/failure state keys (
done,in_progress,blocked,in_review,todo, or a raw Linear state), labels,reviewReadyWhen(work_complete/pr_created/pr_ready),artifactMode(linksorattachments). - Retry / concurrency / observability —
maxAttempts,baseDelaySec,maxActiveRuns,perIssue,dedupeByIssue,emitNotifications,captureIssueSnapshot.
Workflow source is either "repo" (YAML files under
.ade/workflows/linear/**) or "generated" (built from the pipeline canvas
in the renderer).
Run statuses walk through:
queued
-> in_progress
-> waiting_for_target
-> waiting_for_pr
-> awaiting_human_review
-> awaiting_delegation
-> awaiting_lane_choice
-> retry_wait
-> completed | failed | cancelled
Core Linear services on desktop
(apps/desktop/src/main/services/cto/):
linearCredentialService.ts— token + OAuth client + auth mode storage and health check. Reads/writes through the per-machineSyncCredentialStore(~/.ade/secrets/) when one is passed in, with a one-time migration from the legacy project-locallinear-token.v1.bin/linear-oauth-client.v1.binfiles; falls back to the legacy project-scoped path when the machine store is unavailable. Environment overrides (ADE_LINEAR_API,LINEAR_API_KEY,ADE_LINEAR_TOKEN,LINEAR_TOKEN) still take precedence.linearOAuthService.ts— OAuth authorization flowlinearClient.ts— GraphQL client wrapperlinearIssueTracker.ts— normalization intoNormalizedLinearIssuelinearTemplateService.ts— mission/session template resolutionlinearWorkflowFileService.ts— YAML workflow files under.ade/workflows/linear/**. Everysave(config)call invokesensureSharedAdeProjectScaffold(projectRoot)first so a project that was previously local-only gets promoted to the shared.ade/scaffold (including the canonical.ade/.gitignoreandcto/identity.yaml) the moment a Linear workflow is persisted.flowPolicyService.ts— versioned policy read/write, rollback, revisionslinearRoutingService.ts— match triggers against an issue, pick workflowlinearIntakeService.ts— issue discovery loop, snapshots, hasheslinearOutboundService.ts— comments, artifact uploads, state transitionslinearCloseoutService.ts— terminal outcome application to LinearlinearDispatcherService.ts— run lifecycle, step walker, retries, concurrency, delegation, stage chaininglinearSyncService.ts— reconciliation loop,processIssueUpdateentry point, dashboard, queue, sync eventslinearIngressService.ts— webhook HTTP listener + relay poller, hands off tosyncService.processIssueUpdate
Shared types and helpers:
apps/desktop/src/shared/types/linearSync.ts— allLinearWorkflow*types, run statuses, event payloads, catalog types, theNormalizedLinearIssueshape (extended withprojectName,teamName,dueDate,estimate,archivedAt,completedAt,canceledAt,startedAt),LinearConnectionStatus(extended withorganizationId/organizationName/organizationUrlKey/organizationLogoUrlso controllers can render the workspace brand), and the legacyLinearSyncConfigkept for migration reads.apps/desktop/src/shared/types/lanes.ts—LaneLinearIssue(the lane-attached subset of a Linear issue that gets persisted with the lane row) plus the optionallinearIssuefield onCreateLaneArgs/CreateChildLaneArgs/LaneSummary.apps/desktop/src/shared/linearIssueBranch.ts— pure helperslinearIssueLaneName(issue)("IDENT title") andlinearIssueBranchName(issue)(slugified, sanitised against git ref rules:IDENT-title-slug).sanitizeLinearIssueBranchNameis the underlying ref-safety pass and is also exported.apps/desktop/src/shared/linearMagicWords.ts— pure helpers for the PR / commit Linear references:linearPrMagicWord(closeOnMerge)picksFixes(closes the issue when the PR merges) orRefs(links without closing);buildLinearPrTitle/buildLinearPrReferencebuild the strings;ensureLinearPrReferenceinjects the magic word into a PR body if one isn't already there (withpreserveExisting: falseto overwrite an existingRefs/Fixes <IDENT>line);ensureLinearCommitReferenceprefixes a commit subject withRefs IDENT: …when missing.apps/desktop/src/shared/chatContextAttachments.ts— pure helpers for the chat composer's Linear context attachment surface:makeLinearIssueContextAttachment(issue, source),mergeChatContextAttachments,removeChatContextAttachment,chatContextAttachmentKey, plus a defensivenormalizeLinearIssuereader used when re-hydrating attachments from disk or wire payloads.apps/desktop/src/shared/linearWorkflowPresets.ts— default workflow presets, visual plan derivation, step rebuilding. Seeworkflow-presets.md.
Renderer wiring:
apps/desktop/src/renderer/components/cto/LinearSyncPanel.tsx— the main CTO-tab management surface (connection, workflow editor, queue, dashboard, ingress status).apps/desktop/src/renderer/components/cto/pipeline/*— the visual pipeline canvas with trigger, stage, closeout cards.apps/desktop/src/renderer/components/lanes/LinearIssuePicker.tsx— shared issue picker mounted insideCreateLaneDialog. Loads filters viaade.cto.getLinearIssuePickerData(projects + states- assignees in one call) and pages issues with
ade.cto.searchLinearIssues. Exports a row component (LinearIssueRow) and pure label helpers reused by the chat composer's Linear attach dialog and the top-bar quick-view.
- assignees in one call) and pages issues with
apps/desktop/src/renderer/components/lanes/LinearIssueBadge.tsx— compact lane-list badge showing the connected issue's identifier / state / priority. Clicking opens chat with the issue pre-attached as context, falling back to the public Linear URL when chat is unavailable.apps/desktop/src/renderer/components/lanes/linearBrand.tsx— shared Linear brand tokens (LINEAR_BRANDpalette) and icon family (LinearMark,LinearStateIcon,LinearPriorityIcon).apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx— top-bar button (rendered inTopBar.tsxwhenLinearConnectionStatus.connected === true). Opens a popover hosting the sharedLinearIssueBrowser; selecting an issue creates a new lane vialanes.createwithlinearIssueset, refreshes the lane store, and selects the new lane.apps/desktop/src/renderer/components/app/LinearIssueBrowser.tsx— full filter/search surface. Readsade.cto.getLinearQuickViewfor the workspace summary andade.cto.searchLinearIssuesfor paginated results. Persists per-project filter state inlocalStorageunderade.linear.quickView.filters.v1:<projectRoot>.apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx— the composer's Linear attach affordance opens aLinearIssueContextDialogthat hosts the sameLinearIssueBrowserand emits anAgentChatLinearIssueContextAttachment(type: "linear_issue") through the chat session'scontextAttachmentsarray.AgentChatPaneautomatically attaches the lane's connected issue when a chat opens on a Linear-connected lane (viainitialLinearIssueContext, source"lane_link"), and the composer pins it to the dialog so the user can see what's already linked.apps/desktop/src/renderer/components/prs/CreatePrModal.tsx— readslane.linearIssue, defaults the PR title tobuildLinearPrTitle, and usesensureLinearPrReferenceagainst the body whenever the user toggles thecloseLinearIssueOnMergecheckbox so the magic word stays in sync withFixes/Refs.apps/desktop/src/renderer/components/settings/LinearSection.tsx— Settings > Integrations panel for connecting Linear. Reads / writes viaade.cto.getLinearConnectionStatus,ade.cto.setLinearToken,ade.cto.startLinearOAuth, andade.cto.clearLinearToken. Surfaces connection state, project list, and a docs-style hint card describing the issue-routing / CTO-workflow value props.
IPC wiring (apps/desktop/src/main/services/ipc/registerIpc.ts):
- Channels are named in
apps/desktop/src/shared/ipc.tsunderctoGetLinearConnectionStatus,ctoSetLinearToken,ctoClearLinearToken,ctoGetLinearSyncDashboard,ctoRunLinearSyncNow,ctoListLinearSyncQueue,ctoResolveLinearSyncQueueItem,ctoGetLinearWorkflowRunDetail,ctoGetLinearIngressStatus,ctoListLinearIngressEvents,ctoEnsureLinearWebhook,ctoLinearWorkflowEvent(renderer notification broadcast),ctoStartLinearOAuth,ctoGetLinearOAuthSession,ctoSetLinearOAuthClient,ctoClearLinearOAuthClient,ctoGetLinearProjects,ctoGetLinearWorkflowCatalog,ctoGetLinearQuickView(workspace summary used by the top-bar quick view),ctoGetLinearIssuePickerData(one-shot projects + states + assignees catalog forLinearIssuePicker), andctoSearchLinearIssues(paginated issue search consumed by bothLinearIssuePickerandLinearIssueBrowser). IssueTracker(apps/desktop/src/main/services/cto/issueTracker.ts) grew matchinggetQuickView(connection)andsearchIssues(query)methods, both forwarded tolinearClientbylinearIssueTracker.ts.IssueTrackerIssueSearchQuerycovers project / state-types / assignee / priority / free-text / cursor pagination filters; the result is{ issues, pageInfo }.
Headless ADE CLI mode:
apps/ade-cli/src/headlessLinearServices.ts—createHeadlessLinearServices()builds the full service stack (linearClient,linearIssueTracker,linearTemplateService,linearWorkflowFileService,flowPolicyService,linearRoutingService,linearIntakeService,linearOutboundService,linearCloseoutService,linearDispatcherService,linearSyncService,linearIngressService) plus a headlessagentChatServiceandworkerHeartbeatServicethat fail fast when agent execution is requested.apps/ade-cli/src/adeRpcServer.tsregisters the Linear JSON-RPC tools atlistLinearWorkflows,getLinearRunStatus,resolveLinearRunAction,cancelLinearRun,routeLinearIssueToCto,routeLinearIssueToMission,routeLinearIssueToWorker,rerouteLinearRun,getLinearSyncDashboard,runLinearSyncNow,listLinearSyncQueue,resolveLinearSyncQueueItem,getLinearWorkflowRunDetail.
Deeper reading:
dispatch-and-sync.md— issue fetch, routing, dispatcher lifecycle, closeout, reconciliation, relay/webhook ingressworkflow-presets.md— how presets produce and round-trip to the visual plan in the pipeline builder
The Linear pipeline above is fully autonomous: it runs missions / chats / workers without the human ever opening a lane manually. Most day-to-day developer work, though, starts the other way around — the human picks a Linear ticket and creates a lane to work on it. ADE exposes that path in three places that all share the same primitives:
- Create a lane from a Linear issue.
CreateLaneDialog's Advanced section hosts a "Connect Linear issue" affordance backed byLinearIssuePicker. Selecting an issue auto-derives the lane name (linearIssueLaneName→IDENT title) and the branch name (linearIssueBranchName→ident-title-slug, sanitised against git ref rules), pre-fills the create form, and locks the "Import existing branch" tab while an issue is connected. The same picker is launched from the top-barLinearQuickViewButtonand from the chat composer's Linear attach dialog so all three entry points produce identical lane shapes. lane_linear_issuestable.laneService.create/createChildacceptlinearIssue?: LaneLinearIssue; when set, the issue payload (id,identifier,title, project / team / state / priority / labels / assignee / creator / due / estimate / branch name / timestamps) is upserted intolane_linear_issueskeyed by(project_id, lane_id).LaneSummary.linearIssueis hydrated on everylist/get. The service also enforces a collision check: if the resolved branch already exists locally or asorigin/<branch>, lane creation throwsBranch "…" already exists. Detach the Linear issue or choose a different issue..- Commit message prefix. When a lane has a connected issue,
gitOperationsService.commitChanges(and the commit-message generator) auto-prefixes the subject withRefs IDENT: …viaensureLinearCommitReference. Subjects that already mention the identifier are left alone. - PR title + body magic word.
prService.draftPrMetadata/createFromLaneand the rendererCreatePrModalusebuildLinearPrTitle(issue)(IDENT: title) as the default PR title andensureLinearPrReference(body, issue, closeOnMerge)to injectFixes IDENT(closes the Linear issue when the PR merges) orRefs IDENT(links without closing) into the PR description. The user togglescloseLinearIssueOnMergefrom a checkbox inCreatePrModal; the same flag is forwarded bysyncRemoteCommandServiceso phones drive the same behaviour. - Chat context attachment. Chats opened on a lane with a
connected issue automatically receive an
AgentChatLinearIssueContextAttachment(type: "linear_issue",source: "lane_link") viaAgentChatPane'sinitialLinearIssueContext. The composer also supports manual attachment throughLinearIssueContextDialog, which reusesLinearIssueBrowser. Helpers live inshared/chatContextAttachments.ts. - Top-bar quick view.
TopBarmountsLinearQuickViewButtonwheneverLinearConnectionStatus.connectedis true. The popover showsCtoLinearQuickView(workspace + active project counters) plus the sharedLinearIssueBrowser; clicking an issue creates a fresh lane vialanes.create, refreshes the lane store, and selects the new lane.
All state is kept in .ade/ade.db and replicated through cr-sqlite like any
other ADE table. Key tables the Linear stack writes:
linear_workflow_runs— one row perLinearWorkflowRunlinear_workflow_run_steps— per-step status for a runlinear_workflow_run_events— step events, milestones, errorslinear_issue_snapshots— last-seen payload hash per issue for change detection inprocessIssueUpdatelinear_sync_events—issue_closed,watch_only_match,workflow_capacity_wait,issue_dedupedobservability recordslane_linear_issues— issue payload attached to a lane at create time, keyed by(project_id, lane_id). Used by lane hydration,LinearIssueBadge, commit-message prefixing, and PR defaults.linear_issue_claims— active-claim ledger (one active row per(project_id, issue_id)) so two lanes don't try to drive the same issue simultaneously.
Workflow definitions themselves live either inline in the flow policy
(stored in the project config row, versioned via flowPolicyService
revisions) or on disk under .ade/workflows/linear/** when a YAML file
exists for the workflow id.
The sync service appends LinearSyncEventRecord entries for every major
lifecycle moment. The dashboard exposes watchOnlyHits, recentEvents,
queue counters (queued, dispatched, retrying, escalated,
awaitingDelegation, failed), and per-queue-item route metadata
(routeReason, matchedSignals, routeTags, stalledReason,
waitingFor, employeeOverride, activeTargetType). Drill-down to a run
exposes step history, sync events alongside ingress events, linked PR
status, and supervisor notes.
The CTO agent is the supervisory layer. Linear workflows run autonomously once configured, but:
request_human_reviewsteps defaultreviewerIdentityKey: "cto".- Runs in
awaiting_delegationexpose a dropdown inLinearSyncPanelthat setsemployeeOverride, rerouting a queued run without restarting. - The flow-policy versioning (save/rollback/revision list) governs which workflows are active at any given time.
- Linear integration does not require the CTO process to be running. A workflow run and its dispatcher progress independently of CTO heartbeats; CTO just provides the review surface.
- Dormant-until-configured. Until a token is stored, nothing fires.
The ingress HTTP server does not bind. Tests should stub
hasCredentialsaccordingly. - Webhook signing secrets are stored via
automationSecretServiceunder references likelinearRelay.accessToken. Missing/invalid secrets disable the relay path andLinearIngressStatus.relay.statusbecomeserror. - Headless ADE CLI worker targets fail fast. In
createHeadlessWorkerHeartbeatServicethe wakeup always returnsstatus: "failed"with the message "Headless ADE CLI mode does not support worker-backed Linear targets yet." Workflows targetingworker_runare not a supported headless path; useemployee_session,mission, orpr_resolutioninstead. - OAuth client config is per-app, not per-project. Token storage is
storageScope: "app"inLinearConnectionStatus. Switching projects does not change which Linear workspace is attached unless the token is rotated. - Issue closure cancels runs. When an issue reaches a state whose
type is in
intake.terminalStateTypes(default:completed,canceled),linearSyncServiceemits anissue_closedsync event and cancels any active run for that issue. This is how "I fixed it manually in Linear" propagates into ADE. - Reconciliation interval is clamped.
reconciliationIntervalSechas a minimum of 15 seconds inlinearSyncServiceregardless of configured value. - Review wait has a 48-hour timeout.
request_human_reviewsteps time out with areview_timeoutreason rather than blocking the run indefinitely. A stalled supervisor does not stall the dispatcher globally. - Non-PK uniqueness is stripped by CRR retrofit. Linear tables do
not rely on secondary UNIQUE constraints for upserts; dispatcher
merges use explicit select-then-update instead of
ON CONFLICT(some_unique_col).