Skip to content

Migrate from Linear to Ekso in a weekend

Two commands. Projects become containers, issues become items, cycles stay cycles. What carries, what doesn't, and the parts the docs don't cover.

Linear is a great tool. It made the rest of the category look slow, ugly, and bloated by comparison, and it earned every team that picked it years ago. This post isn’t here to argue otherwise.

It’s here for the team that started on Linear when they were ten people and now have eighty, and who’ve started to feel a particular kind of strain — the strain of trying to run support work, finance ops, vendor management, and a dozen other things that aren’t really issues through a tool that was designed around the issue primitive.

If you’re at that point, this is a practical walk-through of how to move what’s in Linear into Ekso, what carries cleanly, what doesn’t, and the parts of the migration the docs don’t quite cover.

Why now

The Linear conversation has shifted in the last year. The version that used to play out on r/ProductManagement was “Linear or Jira?” — a UX argument with a clear winner. The newer thread is quieter: “We picked Linear at ten people. We’re at eighty. Now what?”

That’s a different question, and it has a different answer.

Linear was built around the issue. It’s a beautiful primitive when the work is code changes — a single team, a single backlog, a single weekly cycle. The pull starts when the work expands past that. Workflow states that don’t share definitions across teams. Triage queues running as side inboxes. Customer requests living in their own surface. Roadmap initiatives in another.

These aren’t bugs in Linear. They’re the soft signals that the work has outgrown the primitive Linear was built around. Linear-leavers aren’t fleeing. They’re graduating.

What carries cleanly

The migrator (ekso migrate linear) maps Linear’s primitives onto Ekso’s data model directly, which means most of what you have moves over without translation losses:

Linear conceptEkso shape
Project (ENG/dotcom)Container
Issue (ENG-123)Item
CommentAnnotation
File attachmentFile (multipart upload)
Subtask, Relates, Blocks, DuplicateLink
CycleCycle
LabelLabel
Cross-team ProjectLabel (prefixed project:)
UserUser (matched on email or minted)
EstimateItem field (configurable via --field-map)

Linear stores everything — issue bodies, comments — as native markdown. Ekso preserves that. No HTML round-trip means no lost formatting, no <br> tags creeping into otherwise clean text.

What doesn’t carry — and what to do about it

Five things don’t make the trip:

  • Workflow states. Linear’s per-team workflow is custom; only the current state of each issue is preserved, not the workflow definition itself. You’ll define the destination workflow in Ekso first.
  • URL-bookmark attachments. Linear treats links as if they were files; the migrator logs these to Meta.linear_attachments[] rather than uploading anything. They’re not lost, just shaped differently.
  • Triage queues. Triage is a Linear concept; the items in a triage queue migrate, but the triage state itself doesn’t. Reconstruct using a label or a saved query in Ekso.
  • Customer requests. Linear’s customer-feedback feature isn’t modelled directly. The cleanest workaround is to migrate the underlying issues (which already happens) and reconstruct customer-attribution with either a source:customer label or a dedicated container. Both are cheap to set up and let saved queries answer “what came from customers this quarter?”. The bit you don’t get back is the upvote/duplicate-merging UX Linear ships natively — that’s a feature build, not a migration concern.
  • Roadmap initiatives. Out of scope for v1.

This is the part where being honest pays off. Most “migrate from X” posts hand-wave on the gaps. Telling you exactly what doesn’t carry — before you’ve spent a weekend running it — is the difference between a useful migration and one that ends with someone shouting at the CLI on Monday morning.

The four-step CLI flow

The migrator runs in four steps, deliberately decoupled so each one is recoverable on its own.

Step 1 — list projects

Verify your Linear credentials work and see what the migrator can see:

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 in the next step.

Step 2 — collect

Pull each project’s issues, comments, and attachments into a local SQLite cache. Repeat --project for every project you want to bring across. ENG_Unassigned is the pseudo-project that catches issues that live in a team but aren’t attached to any project — easy to forget, expensive to discover later.

ekso migrate linear collect \
    --project ENG --project ENG_Unassigned \
    --config migration.config.json \
    --url https://ekso.acme.com

Sample run:

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

This step is read-only against your Ekso install and idempotent against Linear. The cache lives at ~/.ekso/migrate/linear-<timestamp>.sqlite — every later step reads from that file, never from Linear again. --no-attachments and --no-comments slim the cache; --resume picks up where a killed collect left off.

Step 3 — dry-run apply

The dry-run is the load-bearing step. The migrator walks the cache, builds every container, item, annotation, link, file upload, and cycle in memory, and reports what would be written to your install — without committing a single byte.

ekso migrate linear apply \
    --process "Engineering" \
    --board <BOARD_ID> \
    --config migration.config.json \
    --url https://ekso.acme.com \
    --dry-run

--process is the destination Ekso process (name or id) — every migrated item attaches to it. --board is required for cycle creation, but auto-discovered when your install has exactly one board, so single-board installs can omit the flag.

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)

This is where you catch field-mapping mistakes, missing destination fields, and identity-resolution surprises before they hit your install. Read the output, fix the config or --field-map, re-run.

Step 4 — apply for real

Once the dry-run reads clean, drop the flag:

ekso migrate linear apply \
    --process "Engineering" \
    --board <BOARD_ID> \
    --config migration.config.json \
    --url https://ekso.acme.com

Apply is transactional per item and resumable via the IdMap — if it dies partway through, re-run with --resume and the migrator picks up at the next un-applied item rather than re-importing:

ekso migrate linear apply \
    --process "Engineering" \
    --config migration.config.json \
    --url https://ekso.acme.com \
    --resume

Two things worth watching for during the live apply: rate-limit warnings from Linear (handled gracefully — wait, then --resume), and field-mapping mismatches that slipped past the dry-run (rare, but the audit log will name them).

Identity resolution — the part nobody warns you about

Every Linear user has an email address (Linear’s identity model is clean), so user matching mostly Just Works. But there are edge cases:

The default --user-strategy match-or-create does the right thing in the common case: a Linear email that matches an existing Ekso user attributes to that user; an unmatched email mints a new Ekso user — no login credentials, marked as imported, audit attribution preserved.

The cases worth knowing about:

  • Deactivated Linear users. Former employees whose comments and issues you still want preserved. The migrator mints a placeholder. Audit trail intact, placeholder can’t log in. This is the right default; if you’d rather scrub them entirely, do it in Linear before collect runs.
  • Mismatched email across systems. Jane has jane@personal.dev in Linear and jane@company.com in Ekso. The default strategy mints a duplicate Jane. The cleanest fix is to pre-load Ekso with the canonical Linear email before running apply; if you’re already past that point, merge accounts after the fact.
  • Bot and integration accounts. Linear’s GitHub, Slack, and Figma integrations post comments under synthetic identities. These get minted as Ekso ghost users — harmless but noisy. Worth scanning the user list post-apply and disabling the ones you don’t need.

This matters for the audit trail. Every annotation, every item, every state transition attributes to a user. Get the mapping wrong and the trail still runs — it just runs through ghosts and duplicates you’ll spend the next three months reconciling. The two-step collect → dry-run flow exists to catch this before writes commit. Use it.

Custom fields — --field-map and what it solves

Linear’s estimate is a decimal number. Ekso’s estimate is a custom item field. The migrator bridges this with a --field-map flag:

Most Linear teams need exactly one field mapping — the estimate decimal:

# migration.fields.yaml
linear:
  estimate: { ekso: Estimate, kind: decimal }

Pass it to apply:

ekso migrate linear apply \
    --process "Engineering" \
    --config migration.config.json \
    --url https://ekso.acme.com \
    --field-map migration.fields.yaml

Two things worth knowing about field-mapping that don’t make the docs:

  • It’s a one-time decision. The field IDs auto-created on first apply are what every future MCP call, API call, and saved query references. Renaming the Ekso field after the fact means rewriting every query that touches it. Get the destination name right the first time.
  • No-mapping is also a choice. Without --field-map, the estimate falls through to DataItem.Meta.linear_estimate losslessly. That’s the right call if you don’t yet know whether you want the field as a first-class typed primitive or just preserved metadata — you can promote later by writing a one-off script that copies Meta → Field.

What to do after the migrator finishes

The migration is a one-shot, but the work that comes after isn’t. You’ll want to:

  1. Define your workflow states in Ekso. Linear’s per-team workflows didn’t migrate — only the current state of each issue. Open Ekso’s process editor and lay out the states you actually want, not the seven-state hairball your previous Linear admin built. This is the once-in-a-tool-lifetime moment to reset.

  2. Recreate the automations you cared about. Linear’s automation surface is intentionally small, so this list is short for most teams: auto-assignment, SLA escalations, label-on-state-change. Each one becomes an Ekso rule. The rule MCP tool is read-only, so agents won’t be writing your rules — but they will read them, and that’s where deterministic and non-deterministic systems start working as one.

  3. Wire up the MCP server. If your team is moving to agentic ops — and most teams reading a Linear→Ekso migration post probably are — the migration is the moment. The MCP-native architecture is the whole point of the move; start using it.

  4. Decide cross-team projects. The migrator mapped Linear cross-team projects to project:-prefixed labels, which preserves the data and works fine for queries. But if a cross-team project functions like its own container in your team’s mental model — own permissions, own roll-up reporting, own lifecycle — promote it. Labels are easy to migrate to containers; containers are not easy to demote to labels.

When NOT to migrate

Plain honesty: Linear is the right tool for some teams. If you’re a 10-person dev shop, every issue is a code change, and you don’t have support, finance, or non-engineering ops to track, Ekso is overkill. Stay on Linear.

The signal that it’s time to move isn’t “Linear is bad” — it isn’t. The signal is when more than 30% of the work your team tracks isn’t really an issue, and you’re tired of bending Linear into something it wasn’t built to be.

If that’s where you are, the migrator is two commands away.


Full migration docs: ekso.dev/cli/migrate/linear. The CLI overview is at ekso.dev/cli/overview. Background on why Ekso is built MCP-first: Operations is going AI-native. Most tools aren’t.

02

Try Ekso.

One workspace for tasks, tickets, time, and money. Free for unlimited users. Cloud, on-premise, or your own infrastructure.