# Migrate Linear to Jira

> Jira is project-centric and unusually restrictive over REST, so several natural mappings run as deliberate workarounds.

Source: https://migratly.dev/migrate/linear-to-jira/

Migratly migrates Linear to Jira by reading both APIs live, deriving the mapping between them, and
presenting it for approval before anything is written. This pair covers **11 entity types**
and **45 field mappings**: 28 carry across intact, 16 are flagged as lossy,
partial, dropped or needing another route, and 10 need a decision from you. The Linear side is
read-only for the whole run.

## How the structure changes

- Each Linear team becomes a Jira project — one project per team, no shared collections.
- Each Linear project becomes a Jira epic: a real issue with issuetype=Epic, with child issues attached through the Epic Link field.
- Workflow states become Jira statuses, cycles become sprints, and labels are matched or created — all before the first issue is written.
- Sub-tasks reference fields.parent.key and run a wave later than their parent.
- Issue relations become Jira issue links in the last wave.

## Entity map

| Linear | Jira | Wave | Fields |
|---|---|---|---|
| Team | Project | 0 | 3 |
| User | User | 0 | 4 |
| Cycle | Sprint | 1 | 5 |
| Label | Label | 1 | 1 |
| LinearProject | Epic | 1 | 3 |
| WorkflowState | Status | 1 | 2 |
| Issue | Issue | 2 | 12 |
| Sub-task | Issue | 3 | 7 |
| Attachment | Attachment | 4 | 1 |
| Comment | Comment | 4 | 4 |
| IssueRelation | IssueLink | 5 | 3 |

## Field coverage

### Team → Project

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `name` | `name` | Full | Reuse/pin an existing Jira project is the supported path. Fresh creation via REST needs admin + projectTypeKey + leadAccountId. |
| `key` | `key` | Partial | Linear team keys may exceed Jira's 10-char limit and must be uppercase A-Z/0-9. alnum + upper + hard-clip to 10 produces a valid candidate; uniqueness is a human decision. |
| `description` | `description` | Full |  |

### User → User

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source Linear ID` | Lossy | Jira users are keyed by accountId; the Linear UUID is kept only for audit. user_map is built source_id -> target accountId via email join. |
| `email` | `emailAddress` | Full | Email is the join key. The Linear member must already exist in the target Jira site with the same email. |
| `name` | `displayName` | Full |  |
| `active` | `active` | Lossy | Jira user active status is read-only via REST; a Jira admin must provision/deactivate accounts. |

### Cycle → Sprint

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `number` | `name` | Full | Linear cycle name -> Jira sprint name (required by Jira). source_field is 'number' (always present) so the transform always runs; it returns the cycle name, or falls back to 'Cycle ' for Linear's auto-cycles that have no name. |
| `startsAt` | `startDate` | Full |  |
| `endsAt` | `endDate` | Full |  |
| `team.key` | `originBoardId` | Full | Sprint must be created on the project's Scrum board. The board id was resolved into id_map (Board:{collection_key}) by the sprint pre_populate; this looks it up. No value transform needed — collection_key drives the key. |
| `` | `state` | Partial | Jira creates sprints in 'future' state. A generic post-create follow-up (declared in the connector spec) activates the sprint whose cycle is currently in progress: now within startsAt..endsAt and not completed. Jira allows one active sprint per board, so this cond yields 'active' for at most one; everything else stays 'future'. The follow-up request itself is spec-driven; only the value is computed here. |

### Label → Label

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `name` | `name` | Full | Jira labels are free-form strings set directly on issues; no creation API. Informational only. Labels are applied at issue level from the issue's embedded labels. |

### LinearProject → Epic

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `name` | `fields.summary` | Full | Linear Project -> Jira Epic (an issue with issuetype Epic). Created at level 1 so issues can link to it via fields.parent. Requires the target project's scheme to include the 'Epic' type; otherwise creation is skipped (non-fatal). |
| `description` | `fields.description` | Partial | Linear project description (Markdown) -> Jira ADF. |
| `state` | `transition` | Workaround | Linear project state -> Jira epic status via a follow-up transition (best-effort; only applies if a same-named status exists in the epic workflow). |

### WorkflowState → Status

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `name` | `name` | Workaround | Jira statuses are read-only via REST. This entity has no create route; it pre-populates the name->status id map (SchemaByName:status) from the target project so the Issue status transition can resolve. Unmatched states keep the issue in its default status. |
| `type` | `statusCategory` | Full | Linear state type suggests the Jira statusCategory for the human-decision UI. |

### Issue → Issue

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `title` | `fields.summary` | Full |  |
| `description` | `fields.description` | Partial | Linear Markdown -> Jira ADF via the markdown_to_adf formatter. Headings/lists/links survive; Linear inline embeds and reactions are lost. |
| `fields.issuetype.name` | `fields.issuetype.name` | Full | Jira requires an issuetype on create. Default Linear issues to 'Task'; the human-decision UI can override. |
| `priority` | `fields.priority.name` | Full | Linear priority int (0=None 1=Urgent 2=High 3=Medium 4=Low) -> Jira priority name. None -> omitted. |
| `assignee.id` | `fields.assignee.accountId` | Full |  |
| `labels.nodes` | `fields.labels` | Full | Linear labels embedded on the issue -> Jira free-form label strings. Spaces are collapsed to hyphens (Jira rejects spaces in labels). |
| `state.name` | `transition` | Full | Jira does not accept status on create. property_type 'transition' keeps this OUT of the create body; after creation the executor runs a follow-up POST /transitions that matches the Linear state name to the target Jira status. Supply a value_map on the route if the target workflow uses different status names. |
| `dueDate` | `fields.duedate` | Full |  |
| `estimate` | `fields.customfield_10016` | Partial | Jira story points use a per-instance custom field (customfield_10016 by convention). |
| `cycle.name` | `fields.customfield_10020` | Partial | Linear cycle -> Jira sprint membership via the Sprint custom field (customfield_10020 by convention). Resolves the sprint id created from the cycle. |
| `project.id` | `fields.parent.key` | Full | Linear Project -> Jira Epic link. Modern Jira links a standard issue to its epic via fields.parent (the epic created from the Linear Project at level 1). Resolves the epic key from Schema:epic; omitted when the issue has no project or the epic wasn't created (e.g. target lacks the Epic type). |
| `createdAt` | `fields.created` | Dropped | Jira's created timestamp is read-only and set to now on creation. |

### Sub-task → Issue

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `title` | `fields.summary` | Full |  |
| `description` | `fields.description` | Partial | Linear Markdown -> Jira ADF. |
| `parent.id` | `fields.parent.key` | Full | Linear sub-issue parent -> Jira parent key. The parent issue is created first (dependency ordering). |
| `fields.issuetype.name` | `fields.issuetype.name` | Full | Sub-tasks require a sub-task-type issue type in Jira. |
| `assignee.id` | `fields.assignee.accountId` | Full |  |
| `priority` | `fields.priority.name` | Full |  |
| `state.name` | `transition` | Full | Status applied via follow-up POST /transitions after create (see Issue); kept out of the create body. |

### Attachment → Attachment

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `url` | `file` | Full | Linear file attachment -> Jira attachment. The executor downloads the asset from Linear (with auth) and multipart-uploads it to the issue (content.attachment.create.multipart). url/filename/content-type fields are spec-declared. |

### Comment → Comment

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `issueId` | `issueId` | Internal | The executor resolves the parent issue's Jira key from id_map and puts it in the URL path; no body field needed. |
| `body` | `body` | Partial | Linear Markdown -> Jira ADF. Jira sets comment author=API user and created=now; original author/timestamp are not preserved in Phase 2 (Phase 3 will prepend them). |
| `user` | `Original Author` | Dropped | Jira sets comment author = the API user; preserving the original author requires prepending to the ADF body (Phase 3). |
| `createdAt` | `Original Timestamp` | Dropped | Jira sets comment created = now; original timestamp preservation requires prepending to the body (Phase 3). |

### IssueRelation → IssueLink

| Linear field | Jira field | Coverage | Notes |
|---|---|---|---|
| `issueId` | `inwardIssue.key` | Full |  |
| `relatedIssueId` | `outwardIssue.key` | Full |  |
| `type` | `type.name` | Partial | Linear relation types (blocks/duplicate/related) -> Jira link types. Custom Jira link types may need pre-creation. |

## FAQ

**Can you migrate Linear issues into an existing Jira project?**

Yes. Existing statuses, labels and sprints are matched by name before anything is created, so an import into a live project reuses what is already there instead of duplicating it.

**How are Linear cycles represented in Jira?**

As sprints on the target board. Jira does not expose sprint creation as a plain REST create, so this mapping runs through the Agile API as a declared workaround rather than a direct field write.

**Does Linear to Jira preserve comment authors and timestamps?**

Comment bodies and original timestamps carry across. Authorship depends on whether the Linear user has a matching Jira account — matched by email during the user wave. Unmatched authors are named in the comment body instead of being dropped.

---

Start a migration: https://app.migratly.dev/
