Skip to content

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/BacklogNOT_STARTED; In ProgressIN_PROGRESS; DoneCOMPLETED; CanceledCANCELED; unknown → left unset
Priority (normalized) priority UrgentURGENT, HighHIGH, MediumMEDIUM, LowLOW, No priorityUNKNOWN
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 to AMB-L1 in 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 Estimate column is non-empty, its value is preserved in descriptionJson.estimate but not mapped to TeamBoost duration (which is in hours/days). Reply to AMB-L2 in 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's state.type (backlog/unstarted/started/completed/canceled) drives this; an unrecognized custom state maps to UNKNOWN.
  • 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.