> ## Documentation Index
> Fetch the complete documentation index at: https://ekso.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Linear

> Move Linear projects, issues, comments, attachments, cycles, and labels into Ekso — in two commands.

`ekso migrate linear` pulls projects, issues, comments, attachments, cycles, and labels from Linear (GraphQL API) into your Ekso tenant. Two commands — `collect` and `apply`. Read [Migrate overview](/cli/migrate/overview) and [Before you start](/cli/migrate/before-you-start) first.

## What gets imported

| Linear concept                      | Ekso shape                                                                                 |
| ----------------------------------- | ------------------------------------------------------------------------------------------ |
| Project                             | [`DataContainer`](/data-model/data-container)                                              |
| Issue (`ENG-123`)                   | [`DataItem`](/data-model/data-item)                                                        |
| Comment                             | [`DataAnnotation`](/data-model/data-annotation)                                            |
| File attachment                     | [`DataFile`](/data-model/data-file) (multipart upload)                                     |
| Subtask, Relates, Blocks, Duplicate | [`DataLink`](/data-model/data-link)                                                        |
| Cycle                               | [`DataCycle`](/data-model/data-cycle)                                                      |
| Label (issue-level)                 | Pipe-delimited string in a `Label` custom field on the [`DataItem`](/data-model/data-item) |
| Cross-team Project                  | [`ConfigLabel`](/data-model/config-label) (prefix `project:`)                              |
| User                                | [`DataUser`](/data-model/data-user) (matched or minted — Linear users always have email)   |
| Estimate (decimal)                  | [`DataItem.Field[]`](/data-model/data-item-field-value) (via `--field-map`)                |

Linear stores descriptions and comments as **markdown directly** — no HTML rendering step. The migrator preserves markdown bodies through to Ekso.

## What does NOT get imported

* **Workflow states.** Linear's per-team workflow is custom; only the current state of each issue is preserved.
* **URL-bookmark "attachments".** Linear lets you attach a URL as if it were a file; these get logged in `Meta.linear_attachments[]` rather than uploaded.
* **Triage queues.** Triage is a Linear-specific workflow concept; the items themselves migrate but the triage state doesn't.
* **Customer requests.** Linear's customer-feedback feature isn't modelled in Ekso v1.
* **Roadmap initiatives.** Out of scope for v1.

## Before you start

Pre-flight checklist on top of [the general one](/cli/migrate/before-you-start):

* A Linear workspace where you have admin or read-everything access.
* A [Linear personal API key](https://linear.app/settings/api) (free tier supports it).

Add a `linear` block to your `migration.config.json`:

```json theme={null}
{
  "ekso": { "url": "https://ekso.acme.com", "apiKey": "ek_live_..." },

  "source": "linear",

  "linear": {
    "apiKey": "lin_api_..."
  }
}
```

Or use the env-var override `EKSO_MIGRATE_LINEAR_KEY` for CI use cases.

## Step 1 — list projects

Verify your credentials work and see what Linear shows you:

```bash theme={null}
ekso migrate linear list-projects \
    --config migration.config.json \
    --url https://ekso.acme.com
```

Sample output:

```
NAME                  KEY            ID                                       ISSUES
Dotcom Rebuild        ENG/dotcom     01a3b9c8-1f44-4eaa-9c6e-7d5b2c1d3f4e     312
Helpdesk              OPS/helpdesk   02b4ca99-2055-5fbb-ad7f-8e6c3d2e4a5f     128
```

The `KEY` column is what you pass to `--project` on `collect`.

## Step 2 — collect

Pull a single project's issues, comments, and attachments into a local SQLite cache:

```bash theme={null}
ekso migrate linear collect \
    --config migration.config.json \
    --project ENG/dotcom \
    --url https://ekso.acme.com
```

Repeat `--project` for multiple. Sample run:

```
fetching projects... 1 fetched
fetching dotcom issues... 312/312  ok
fetching comments...      842/842  ok
fetching attachments...    47/47   ok
saved cache: ~/.ekso/migrate/linear-2026-04-29T143205.sqlite
```

Useful flags:

* `--no-attachments` — skip downloads.
* `--no-comments` — skip comments.
* `--resume` — pick up where a killed collect left off.

## Step 3 — dry-run apply

```bash theme={null}
ekso migrate linear apply \
    --config migration.config.json \
    --process proc_engineering \
    --url https://ekso.acme.com \
    --dry-run
```

Sample output:

```
DRY-RUN — no writes.
would create:  1 container, 28 users, 843 items, 2204 annotations,
               156 files, 47 links, 6 cycles
field-map check: ok (1 process field would be auto-created — Estimate)
```

## Step 4 — apply for real

```bash theme={null}
ekso migrate linear apply \
    --config migration.config.json \
    --process proc_engineering \
    --url https://ekso.acme.com
```

If the run is interrupted, re-run with `--resume`:

```bash theme={null}
ekso migrate linear apply \
    --config migration.config.json \
    --process proc_engineering \
    --resume
```

## Identity resolution for Linear

Linear users always have an email — the default `--user-strategy match-or-create` is reliable here. No special handling needed.

Read [Identity resolution](/cli/migrate/identity-resolution).

## Custom fields for Linear

Linear's main custom field is the first-class `estimate` (a decimal). Most teams need only this one mapping:

```yaml theme={null}
linear:
  estimate: { ekso: Estimate, kind: decimal }
```

Pass it to apply:

```bash theme={null}
ekso migrate linear apply \
    --config migration.config.json \
    --process proc_engineering \
    --field-map migration.fields.yaml
```

If your destination process doesn't yet have an `Estimate` field, the Apply layer auto-creates it before any item write. See [Field mapping](/cli/migrate/field-mapping).

Without `--field-map`, the estimate falls through to `DataItem.Meta.linear_estimate` losslessly.

## Cycles, Labels, Cross-team Projects

| Linear             | Ekso                                                           |
| ------------------ | -------------------------------------------------------------- |
| Cycle              | `DataCycle` (always — no flag)                                 |
| Issue-level label  | Pipe-delimited string on the `DataItem`'s `Label` custom field |
| Cross-team project | `ConfigLabel` (prefix `project:`)                              |

Linear labels are imported at the issue level only — workspace-wide and team-wide label *definitions* don't migrate as standalone records. Each issue's label set is concatenated into a pipe-delimited string and written to a custom field named `Label` on the resulting `DataItem` (e.g. `Bug|Customer-impact|Needs-design`). The `Label` field is auto-created on the destination process if missing.

Linear "projects" that span multiple teams stay as `project:`-prefixed `ConfigLabel`s — single-team projects become containers (per the table above), so the cross-team variety is the only one that needs the label fallback.

Cycles attach to a board on the Ekso side. Pass `--board <BOARD_ID>` if the tenant has more than one board.

## Markdown bodies

Linear stores issue descriptions and comments as markdown natively. The migrator passes the markdown through unchanged — Ekso renders it the same way Linear did. You shouldn't see any visual drift on body content.

## Iron rule — cursor pagination terminator

Linear's GraphQL API paginates with cursors. The pagination loop terminates only when the response returns **both** `HasNextPage=false` *and* a non-null `EndCursor`. An earlier version of the upstream adapter terminated only on `HasNextPage`, which produced infinite loops on certain edge-case responses (Linear sometimes returns `HasNextPage=true` with a null `EndCursor`).

The current CLI handles this correctly with a regression test. If you ever see `Inconsistent pagination response from Linear`, the CLI is detecting the edge case and exiting cleanly rather than looping. Re-run `collect` to retry.

## Troubleshooting Linear-specific issues

| Symptom                                        | Fix                                                                                                                |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `exit 3 — Linear API key invalid`              | Mint a new key at linear.app → Settings → API → Personal API keys.                                                 |
| `URL-bookmark attachment skipped`              | Logged in `Meta.linear_attachments[]`. Documented limitation — Linear "attachments" can be URLs rather than files. |
| `Inconsistent pagination response from Linear` | The pagination terminator detected an edge-case response. Re-run `collect`.                                        |
| `exit 7 — Linear rate-limit exceeded`          | Linear's GraphQL complexity budget got exhausted. Wait, then re-run with `--resume`.                               |

See [Troubleshooting](/cli/migrate/troubleshooting) for the full per-source error table.

## Why migrate to Ekso

Linear is excellent at issue tracking. Ekso adds financial intelligence, multi-tenant SaaS or self-host, and AI-native primitives that Linear doesn't model. See [Ekso vs Linear](https://ekso.app/compare/ekso-vs-linear) for the broader comparison.

## Where to next

* **[Command reference](/cli/migrate/command-reference)** — full flag surface.
* **[Identity resolution](/cli/migrate/identity-resolution)** — `--user-strategy` deep-dive.
* **[Field mapping](/cli/migrate/field-mapping)** — write your `migration.fields.yaml`.
* **[Troubleshooting](/cli/migrate/troubleshooting)** — exit codes and recovery.
