Documentation
Linear sync
Two-way sync between a factory project and one Linear team/project: issues, their statuses, and their comments. The factory is the source of truth for which statuses exist.
Turning it on
Credential (factory-wide). Put a Linear API key in
.envand redeploy:LINEAR_API_KEY=lin_api_xxx LINEAR_WEBHOOK_URL=https://factory.example.com # your public/tunnel hostWith
LINEAR_API_KEYunset the feature is completely inert — no Linear calls anywhere, no errors.LINEAR_WEBHOOK_URLis what Linear posts to; without it sync still works but only on the poll interval (LINEAR_POLL_SEC, default 300s), and the server logs a warning at first link.
Per project (opt-in). Add a
linear:block to the project's YAML (or fill in the Linear block on the project's settings → Connection tab):linear: team: ENG # team key, name, or id — required project: "Website" # Linear project name or id — optional manage_states: true # optional, default true; see the warning belowProjects without the block are untouched. Removing it stops all pushes and makes inbound events no-ops; nothing is deleted on either side.
What syncs
| Direction | |
|---|---|
| Issue create | both ways |
| Title / description / status | both ways |
| Comment create | both ways, attributed (**Linear (name)** commented: / **AI Factory** commented:) |
| Workflow states | factory → Linear only |
| Issue delete in Linear | unpairs; the factory issue stays |
Not synced (v1): assignees, labels, priority, attachments, sub-issues, cycles, comment edits/deletes, and per-person authorship — everything the factory pushes appears as the API key's user.
An inbound status change goes through the same code path as a status change made in the dashboard, so it triggers the pipeline exactly the same way.
⚠ State reconciliation rewrites the whole Linear TEAM
Linear workflow states belong to a team, not a project. When the factory's status list changes it creates missing states, renames/reorders existing ones, and archives states that aren't in the factory workflow — after moving any issues on them to the closest remaining state of the same type. That affects every Linear project in the team.
Point the link at a dedicated team, or set manage_states: false to make
reconciliation additive-only (create/rename, never archive).
Loop prevention
Each paired issue stores a shadow of the last value both sides agreed on, per
field. Outbound pushes when local != shadow and then sets shadow = local;
inbound applies when remote != shadow and then sets shadow = remote. So the
webhook echo of our own push finds remote == shadow and does nothing, and the
beat after an inbound apply finds local == shadow and pushes nothing. If both
sides moved since the last agreement, the factory wins (logged), and the next
beat pushes its value once.
Comments use the same idea by existence: an inbound comment gets its
linear_comment_links row in the same transaction as the comment, so the
outbound pass never sees it as unpushed.
Caveats
- Not retroactive. Sync pairs issues from the moment the project links. Issues that already existed in Linear are not adopted; issues that already exist in the factory are pushed to Linear as new issues.
- One team per project, no fan-out.
- Linear errors are logged, surfaced as
last_erroron the project settings page, and retried on the next beat. They never crash the server and never affect projects that aren't linked.
Manual smoke test (not in CI)
The automated suite stubs the Linear API entirely. To verify the real path once:
set the key and a reachable LINEAR_WEBHOOK_URL, link a scratch team, then
check that (a) the factory registered a webhook in Linear → Settings → API →
Webhooks, (b) creating an issue on either side produces its pair, and (c) moving
a status in Linear moves it in the factory within a second or two.