Linear#
Linear is the first provider adapter. It supports importing Linear issues and the Linear user directory into TeamBoost, from either a CSV export or the Linear GraphQL API.
| Capability | Method | Status |
|---|---|---|
CSV_IMPORT |
Upload a Linear CSV export | ✅ Built |
SYNC |
Pull issues from the Linear API | ✅ Built |
USER_IMPORT |
Pull the Linear user directory (rides the same sync) | ✅ Built |
WEBHOOK |
— | Not built (see Roadmap) |
The endpoints below live under /api/v1/providers/linear/.... They are generic —
the same routes serve any registered provider by substituting the {provider}
path segment — but the request/response shapes shown here are exactly what Linear
returns today. All routes require authentication; the import and credential routes
require an organization-scoped user token (see Authentication).
Every response uses the standard envelope:
{ "status": "success", "message": "…", "data": { } }
Capabilities#
GET /api/v1/providers/linear
{
"status": "success",
"data": { "provider": "linear", "capabilities": ["CSV_IMPORT", "SYNC", "USER_IMPORT"] }
}
Credentials (for API sync)#
SYNC needs a Linear API key. Each organization stores its own; it is encrypted
at rest and never returned by any endpoint. CSV import does not need a credential.
A Linear personal API key starts with lin_api_ (create one under
Linear → Settings → Security & access → Personal API keys).
Store the key#
PUT /api/v1/providers/linear/credentials?organization_id=<id>
curl -X PUT "https://<host>/api/v1/providers/linear/credentials?organization_id=590789" \
-H "Authorization: Bearer <user-token>" \
-H "Content-Type: application/json" \
-d '{ "secret": "lin_api_xxx" }'
{
"status": "success",
"message": "Credential stored.",
"data": { "provider": "linear", "configured": true, "credentialType": "API_KEY",
"updatedAt": "2026-06-27T09:55:00Z" }
}
Check / verify / delete#
| Route | Purpose |
|---|---|
GET /api/v1/providers/linear/credentials |
Whether a credential is configured (+ metadata). Never returns the secret. |
POST /api/v1/providers/linear/credentials/verify |
Live-test the stored key: the adapter checks the lin_api_ format and makes a cheap probe call to Linear. |
DELETE /api/v1/providers/linear/credentials |
Remove the stored credential (idempotent). |
verify responds { "provider": "linear", "valid": true } on success, 404
credential_not_found if none is stored, or 401 invalid_api_key if Linear
rejects it.
Importing#
Both entry points produce the same reviewable import session: parsed tasks, a
proposed user mapping, per-row errors, and a summary, persisted in PENDING_REVIEW
when projection is clean or PROJECTION_ERRORS when a blocking mapping issue remains.
Nothing is written to TeamBoost yet — the session is for review and mapping confirmation.
From a CSV export#
POST /api/v1/providers/linear/imports/csv?organization_id=<id>
Upload the file produced by Linear's Export issues → CSV. The upload is capped at 4 MB by the shared CSV import endpoint.
curl -X POST "https://<host>/api/v1/providers/linear/imports/csv?organization_id=590789" \
-H "Authorization: Bearer <user-token>" \
-F "file=@linear-export.csv"
The parser reads these columns: ID, Title, Description, Status,
Priority, Assignee, Creator, Labels (comma-space delimited), Created,
Updated, and the issue UUID. Malformed rows are reported in rowErrors rather
than failing the whole import.
From the Linear API#
POST /api/v1/providers/linear/imports/sync?organization_id=<id>
Uses the stored, encrypted API key to pull issues and the user directory from the
Linear GraphQL API (cursor-paginated, with rate-limit retry). Requires a stored
credential — 404 credential_not_found otherwise.
Linear's USER_IMPORT capability rides this same sync route: it enriches the
import session's proposed user mapping with the Linear user directory. There is
no separate standalone user-import endpoint yet.
curl -X POST "https://<host>/api/v1/providers/linear/imports/sync?organization_id=590789" \
-H "Authorization: Bearer <user-token>"
The import session#
{
"status": "success",
"message": "Synced from provider; review the proposed user mapping.",
"data": {
"id": "a1b2c3…",
"provider": "linear",
"capability": "SYNC",
"filename": null,
"status": "PENDING_REVIEW",
"summary": { "taskCount": 8, "userCount": 4, "errorCount": 0,
"projectionErrorCount": 0, "mappingNoteCount": 8 },
"taskEntries": [ {
"index": 0,
"source": { "id": "uuid-1", "identifier": "ENG-1", "title": "…",
"priority": 2,
"state": { "type": "started", "name": "In Progress" },
"assignee": { "email": "person@linear-workspace.com", "name": "…" } },
"projected": {
"title": "…", "type": "OTHER", "description": "…",
"status": "IN_PROGRESS", "priority": "HIGH",
"ownerId": 906, "requestedById": null,
"labels": ["Backend"], "projectTitle": null,
"source": "linear", "scheduleStage": "PENDING",
"descriptionJson": { "provider": "linear", "identifier": "ENG-1",
"issueId": "uuid-1", "providerStatus": "In Progress",
"providerPriority": "2" }
},
"projectionErrors": [],
"mappingNotes": [
"Linear issues have no issue type; defaulted task type to OTHER. Reply to AMB-L1 in the ExecPlan to lock a different default."
],
"metadata": { "provider": "linear", "providerId": "uuid-1", "identifier": "ENG-1",
"parentIdentifier": null }
} ],
"userMappings": [ { "sourceEmail": "person@linear-workspace.com", "sourceName": "…",
"matchedUserId": 906, "matchedEmail": "person@teamboost-org.com",
"method": "EXACT_EMAIL", "confidence": 1.0 } ],
"rowErrors": []
}
}
Each parsed task becomes a task entry (taskEntries): the normalized source, a
projected TeamBoost task-create draft, blocking projectionErrors, and non-blocking
mappingNotes for review. A session can only be confirmed once no projectionErrors remain.
Linear projection field coverage#
The Linear projector maps every field it can from the normalized task into the
projected draft:
| Linear source | TeamBoost field | Notes |
|---|---|---|
Title |
title |
Error if shorter than 2 characters |
Description |
description |
|
Status (normalized) |
status |
Todo/Backlog → NOT_STARTED; In Progress → IN_PROGRESS; Done → COMPLETED; Canceled → CANCELED; unknown → left unset |
Priority (normalized) |
priority |
Urgent→URGENT, High→HIGH, Medium→MEDIUM, Low→LOW, No priority→UNKNOWN |
Assignee (email) |
ownerId |
Resolved via user mapping; present-but-unmapped assignee is a blocking projectionError |
Creator (email) |
requestedById |
Resolved via user mapping; unmapped creator is a non-blocking mappingNote |
Labels |
labels |
Pass-through; TeamBoost creates/maps labels server-side |
Project |
projectTitle |
|
Due Date |
plannedEndDate |
Also preserved in descriptionJson.dueDate |
Parent issue |
descriptionJson.parentKey |
Not a dependency — context only |
Team |
descriptionJson.team |
CSV exports only; the current API-sync shape does not expose team |
Cycle Number/Name |
descriptionJson.cycleNumber/cycleName |
Best-effort from lossless source record |
Estimate (story points) |
descriptionJson.estimate |
Not mapped to duration — see AMB-L2 below |
| — | type |
Always OTHER; see AMB-L1 below |
| — | source |
"linear" |
| — | scheduleStage |
"PENDING" |
Mapping notes (non-blocking operator review items)#
Every Linear entry carries at least one mappingNote:
- AMB-L1 — Issue type: Linear has no issue-type field. Every entry defaults to
type: "OTHER". Reply toAMB-L1in the import ExecPlan to lock a different default (for example, use label heuristics to map label"Bug"→BUG). - AMB-L2 — Story-point estimate: When the
Estimatecolumn is non-empty, its value is preserved indescriptionJson.estimatebut not mapped to TeamBoostduration(which is in hours/days). Reply toAMB-L2in the ExecPlan to set a conversion factor.
Mapping notes flag provisional choices for operator review but never block confirm.
Normalized vocabularies#
Linear values are mapped onto TeamBoost-neutral enums:
- Task status (
status):BACKLOG,TODO,IN_PROGRESS,DONE,CANCELED,UNKNOWN. Linear'sstate.type(backlog/unstarted/started/completed/canceled) drives this; an unrecognized custom state maps toUNKNOWN. - Priority (
priority):NONE,URGENT,HIGH,MEDIUM,LOW,UNKNOWN.
User mapping#
Each imported user is matched against your organization's full TeamBoost member directory (fetched server-side) using this ladder, best match wins:
method |
Meaning |
|---|---|
EXACT_EMAIL |
Same email address. |
NAME_SIMILARITY |
The imported name fuzzy-matched a member's name. |
EMAIL_LOCAL_SIMILARITY |
The email local part (before @) fuzzy-matched a member — catches the same person across different workspace domains. |
MANUAL |
Set by you via the mappings endpoint (overrides the proposal). |
NONE |
No confident match — resolve it yourself before confirming. |
Review and confirm the mapping#
GET /api/v1/providers/linear/imports/{session_id}?organization_id=<id>
PATCH /api/v1/providers/linear/imports/{session_id}/mappings?organization_id=<id>
PATCH replaces the mapping with your edited list (send the complete list, not
a partial patch) and optionally confirms the session:
curl -X PATCH "https://<host>/api/v1/providers/linear/imports/<session_id>/mappings?organization_id=590789" \
-H "Authorization: Bearer <user-token>" \
-H "Content-Type: application/json" \
-d '{ "userMappings": [ { "sourceEmail": "person@x.com", "matchedUserId": 906, "method": "MANUAL" } ],
"confirm": true }'
A confirmed session moves to CONFIRMED. Reads and writes are scoped to your
organization — another organization's session id returns 404.
Not built yet
There is no execute step that pushes a confirmed session into TeamBoost — TeamBoost does not yet expose a write API. Webhooks and OAuth app-install are also planned but not built. See the Roadmap.