# Migrate Jira to Linear

> Linear is flat where Jira is nested, so the shape of the work changes on the way across.

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

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

## How the structure changes

- Each Jira project becomes a Linear team. Linear has no boards or folders — issues live directly in a team.
- Each Jira epic becomes a Linear project, which is how Linear groups issues inside a team.
- Jira status values become Linear workflow states, and sprints become cycles — both created before any issue is written.
- Sub-tasks arrive as ordinary Linear issues with parentId set, one wave after their parent exists.
- Issue links become issue relations in the final wave, once every issue on both ends has been created.

## Entity map

| Jira | Linear | Wave | Fields |
|---|---|---|---|
| Project | Team | 0 | 6 |
| User | User | 0 | 4 |
| Component | Label | 1 | 1 |
| Epic | Project | 1 | 15 |
| IssueType | Label | 1 | 1 |
| Label | Label | 1 | 2 |
| Sprint | Cycle | 1 | 6 |
| Status | WorkflowState | 1 | 2 |
| Issue | Issue | 2 | 36 |
| Sub-task | Issue | 3 | 25 |
| Attachment | Attachment | 4 | 6 |
| Comment | Comment | 4 | 5 |
| IssueLink | IssueRelation | 5 | 4 |

## Field coverage

### Project → Team

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID, not directly mapped to Linear Team. |
| `key` | `Key` | Lossy | Linear Team key has max 5 characters, must be uppercase. |
| `name` | `Name` | Full |  |
| `description` | `Description` | Lossy | Jira ADF rich text is converted to Markdown. |
| `lead` | `Lead` | Lossy | Linear Team does not have a dedicated 'Lead' field. |
| `projectTypeKey` | `Project Type` | Lossy | Linear Team does not have a dedicated 'Project Type' field. |

### User → User

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `accountId` | `Source Account ID` | Lossy | Linear users are identified by email/UUID, not source account ID. |
| `displayName` | `Display Name` | Full |  |
| `emailAddress` | `Email` | Full |  |
| `active` | `Active Status` | Lossy | Linear API does not support setting user active status. |

### Component → Label

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `name` | `Name` | Full | Jira components have no native equivalent in Linear; create one Linear label per component so taxonomy survives the migration. |

### Epic → Project

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `key` | `Source Key` | Dropped | Jira Epic Key used for internal lookup. |
| `summary` | `Name` | Full |  |
| `description` | `Description` | Lossy | Jira ADF converted to Markdown. |
| `status.name` | `State` | Full |  |
| `priority.name` | `Priority` | Lossy | Linear Project has no dedicated 'Priority' field. |
| `assignee.accountId` | `Lead ID` | Full |  |
| `reporter` | `Reporter` | Lossy | Reporter prepended to description. |
| `labels` | `Labels` | Lossy | Labels appended to description. |
| `duedate` | `End Date` | Full |  |
| `customfield_10015` | `Start Date` | Full | Assuming customfield_10015 is Epic Start Date. |
| `created` | `Created At` | Dropped | createdAt not settable for Projects. |
| `updated` | `Updated At` | Lossy | updatedAt not settable. |
| `project.key` | `Team ID` | Dropped | Used for Team lookup. |
| `comment` | `Comments` | Dropped | Comments are on Issues, not Projects in Linear. |

### IssueType → Label

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `name` | `Name` | Full | Linear has no first-class issue-type field. Materialise each Jira issue type (Story, Bug, Task, Epic, Sub-task) as a Linear label so the Issue's issuetype.name → labelIds lookup resolves. |

### Label → Label

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `name` | `Name` | Full |  |
| `color` | `Color` | Full | Jira labels have no explicit colors, Linear assigns default. |

### Sprint → Cycle

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `name` | `Name` | Full |  |
| `state` | `State` | Lossy | Linear Cycles derive state from dates, no explicit state field. |
| `startDate` | `Starts At` | Full |  |
| `endDate` | `Ends At` | Full | If endDate is missing, agent should handle via coalesce or default. |
| `goal` | no equivalent | Dropped | Linear Cycles do not have a 'goal' field. Dropped — appending it to name causes max-length issues. |

### Status → WorkflowState

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `name` | `Name` | Full |  |
| `category` | `Type` | Full |  |

### Issue → Issue

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID, not directly mapped. |
| `key` | `Jira Key` | Full | Jira Key is prepended to the description for traceability. |
| `summary` | `Title` | Full |  |
| `description` | `Description` | Lossy | Jira ADF rich text is converted to Markdown. |
| `issuetype.name` | `Labels` | Full | Jira Issue Type is mapped to a Linear Label. |
| `status.name` | `Workflow State ID` | Full |  |
| `priority.name` | `Priority` | Full |  |
| `assignee.accountId` | `Assignee ID` | Full |  |
| `reporter` | `Reporter` | Lossy | Linear Issue does not have a dedicated 'Reporter' field. Reporter's display name is prepended to the description. |
| `labels` | `Labels` | Full |  |
| `created` | `Created At` | Full |  |
| `updated` | `Updated At` | Lossy | Linear API does not support setting 'updatedAt'. |
| `duedate` | `Due Date` | Full |  |
| `project.key` | `Team ID` | Dropped | Used for internal lookup to determine the parent Linear Team. |
| `parent.key` | `Parent ID` | Dropped | Handled in the Sub-task entity mapping. |
| `customfield_10016` | `Estimate` | Full | Assuming customfield_10016 is Story Points. |
| `customfield_10020` | `Cycle ID` | Full | Assuming customfield_10020 is Sprint. Maps to Linear Cycle. |
| `components` | `Labels` | Full | Jira Components are mapped to Linear Labels. |
| `fixVersions` | `Labels` | Full | Jira Fix Versions are mapped to Linear Labels. |
| `resolution.name` | `Resolution` | Lossy | Linear Issue does not have a dedicated 'Resolution' field. |
| `timeoriginalestimate` | `Original Time Estimate` | Lossy | Linear does not have 'Original Time Estimate'. Appended to description. |
| `timespent` | `Time Spent` | Lossy | Linear does not have 'Time Spent'. Appended to description. |
| `votes.votes` | `Votes` | Lossy | Linear does not have a 'Votes' field. |
| `issuelinks` | `Issue Links` | Dropped | Issue links handled by IssueLink entity. |
| `customfield_10014` | `Project ID` | Full | Links issue to its parent Epic (Linear Project). |
| `versions` | no equivalent | Dropped | Linear has no Affects Versions concept; deliberately dropped. |
| `resolutiondate` | no equivalent | Dropped | Linear has no resolution date field; completion date is implicit in state transitions. |
| `watches.watchCount` | no equivalent | Dropped | Watch counts not meaningful post-migration. |
| `workratio` | no equivalent | Dropped | Derived field; not meaningful post-migration. |
| `progress` | no equivalent | Dropped | Time-tracking aggregate; covered elsewhere if needed. |
| `aggregateprogress` | no equivalent | Dropped | Sub-issue roll-up; recomputed in target. |
| `lastViewed` | no equivalent | Dropped | Per-user metadata; not migrated. |
| `statuscategorychangedate` | no equivalent | Dropped | Derived field; not meaningful post-migration. |
| `statusCategory` | no equivalent | Dropped | Derived from status.name; already covered by status mapping. |
| `creator.accountId` | `Original Creator` | Workaround | Linear cannot set issue creator; stored as text description prefix or label. |
| `reporter.accountId` | `Original Reporter` | Workaround | Linear cannot set issue reporter; stored as text description prefix. |

### Sub-task → Issue

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `key` | `Jira Key` | Full | Jira Key prepended for traceability. |
| `summary` | `Title` | Full |  |
| `description` | `Description` | Lossy | Jira ADF converted to Markdown. |
| `issuetype.name` | `Labels` | Full | Issue Type mapped to Linear Label. |
| `status.name` | `Workflow State ID` | Full |  |
| `priority.name` | `Priority` | Full |  |
| `assignee.accountId` | `Assignee ID` | Full |  |
| `reporter` | `Reporter` | Lossy | Reporter prepended to description. |
| `labels` | `Labels` | Full |  |
| `created` | `Created At` | Full |  |
| `updated` | `Updated At` | Lossy | Linear API does not support setting 'updatedAt'. |
| `duedate` | `Due Date` | Full |  |
| `project.key` | `Team ID` | Dropped | Used for internal Team lookup. |
| `parent.key` | `Parent ID` | Full |  |
| `customfield_10016` | `Estimate` | Full | Assuming customfield_10016 is Story Points. |
| `customfield_10020` | `Cycle ID` | Full | Sprint maps to Linear Cycle. |
| `components` | `Labels` | Full | Components mapped to Labels. |
| `fixVersions` | `Labels` | Full | Fix Versions mapped to Labels. |
| `resolution.name` | `Resolution` | Lossy | Resolution appended to description. |
| `timeoriginalestimate` | `Original Time Estimate` | Lossy | Time estimate appended to description. |
| `timespent` | `Time Spent` | Lossy | Time spent appended to description. |
| `votes.votes` | `Votes` | Lossy | Votes appended to description. |
| `issuelinks` | `Issue Links` | Dropped | Issue links handled by IssueLink entity. |
| `customfield_10014` | `Project ID` | Full | Links sub-task to parent Epic (Linear Project). |

### Attachment → Attachment

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `issueId` | `Issue ID` | Full |  |
| `filename` | `Title` | Full |  |
| `content` | `URL` | Full | Jira attachment URLs may require auth. |
| `mimeType` | `Subtitle` | Full |  |
| `size` | `Size` | Lossy | Linear Attachment does not have a 'size' field. |

### Comment → Comment

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `issueId` | `Issue ID` | Full |  |
| `body` | `Body` | Lossy | ADF converted to Markdown. Author prepended. |
| `author` | `Author` | Lossy | Author display name prepended to comment body. |
| `created` | `Created At` | Full |  |

### IssueLink → IssueRelation

| Jira field | Linear field | Coverage | Notes |
|---|---|---|---|
| `id` | `Source ID` | Dropped | Internal Jira ID. |
| `type.name` | `Type` | Lossy | Jira link types mapped to limited Linear relation types. |
| `inwardIssue.key` | `Issue ID` | Full |  |
| `outwardIssue.key` | `Related Issue ID` | Full |  |

## FAQ

**Does a Jira to Linear migration keep sprints?**

Yes. Jira sprints map to Linear cycles. Cycles require a start and end date, so any sprint missing one is flagged before the run rather than dropped during it.

**What happens to Jira epics in Linear?**

A Jira epic becomes a Linear project inside the team that the Jira project mapped to. Issues that belonged to the epic get that project id. Issues with no epic keep their team but carry no project.

**Are Jira custom fields lost when moving to Linear?**

Linear has a much smaller fixed field set than Jira, so most custom fields have no native slot. Migratly reports each one before the run and offers the remaining routes — description append, label, or a comment — rather than discarding it silently.

---

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