Migrate from Jira, Asana, GitHub Issues, or other tools to Linear.
Use when planning a migration to Linear, executing data transfer,
or mapping workflows between tools.
Trigger with phrases like "migrate to linear", "jira to linear",
"asana to linear", "import to linear", "linear migration".
Comprehensive guide for migrating from Jira, Asana, or GitHub Issues to Linear. Covers assessment, workflow mapping, data export, transformation, batch import with hierarchy support, and post-migration validation. Linear also has a built-in importer (Settings > Import) for Jira, Asana, GitHub, and CSV.
import { LinearClient } from "@linear/sdk";
async function importToLinear(
client: LinearClient,
teamId: string,
issues: JiraIssue[],
stateMap: Map<string, string>,
userMap: Map<string, string>,
labelMap: Map<string, string>
): Promise<{ created: number; errors: number; idMap: Map<string, string> }> {
const idMap = new Map<string, string>(); // sourceId -> linearId
let created = 0;
let errors = 0;
// Sort: parents first, then children
const sorted = [...issues].sort((a, b) => {
if (a.subtasks.length > 0 && !a.parent) return -1; // Parents first
if (b.subtasks.length > 0 && !b.parent) return 1;
return 0;
});
for (const jiraIssue of sorted) {
try {
const transformed = await transformJiraIssue(jiraIssue, stateMap, userMap, labelMap);
// Set parent if it was already imported
if (jiraIssue.parent && idMap.has(jiraIssue.parent)) {
transformed.parentId = idMap.get(jiraIssue.parent);
}
const result = await client.createIssue({
teamId,
title: transformed.title,
description: `${transformed.description}\n\n---\n*Migrated from ${jiraIssue.key}*`,
priority: transformed.priority,
stateId: transformed.stateId,
assigneeId: transformed.assigneeId,
labelIds: transformed.labelIds,
estimate: transformed.estimate,
parentId: transformed.parentId,
});
if (result.success) {
const issue = await result.issue;
idMap.set(jiraIssue.key, issue!.id);
created++;
if (created % 25 === 0) console.log(`Imported ${created}/${sorted.length}`);
}
// Rate limit: 100ms between requests
await new Promise(r => setTimeout(r, 100));
} catch (error: any) {
console.error(`Failed to import ${jiraIssue.key}: ${error.message}`);
errors++;
}
}
console.log(`Import complete: ${created} created, ${errors} errors`);
return { created, errors, idMap };
}
Step 6: Post-Migration Validation
async function validateMigration(
client: LinearClient,
teamId: string,
sourceIssues: JiraIssue[],
idMap: Map<string, string>
): Promise<{ valid: boolean; issues: string[] }> {
const problems: string[] = [];
// Check all issues were imported
if (idMap.size < sourceIssues.length) {
problems.push(`Missing: ${sourceIssues.length - idMap.size} issues not imported`);
}
// Sample validation: check 50 random issues
const sample = sourceIssues.slice(0, 50);
for (const source of sample) {
const linearId = idMap.get(source.key);
if (!linearId) {
problems.push(`${source.key}: not imported`);
continue;
}
try {
const issue = await client.issue(linearId);
if (issue.title !== source.summary) {
problems.push(`${source.key}: title mismatch`);
}
} catch {
problems.push(`${source.key}: not found in Linear (${linearId})`);
}
await new Promise(r => setTimeout(r, 50));
}
return { valid: problems.length === 0, issues: problems };
}
Post-Migration Checklist
[ ] All issues imported and validated
[ ] Parent/child relationships correct
[ ] Labels and priorities mapped correctly
[ ] User assignments transferred
[ ] Integrations reconfigured (GitHub, Slack)
[ ] Team workflows customized in Linear
[ ] Team trained on Linear
[ ] Source system set to read-only
[ ] Parallel run period started (2 weeks recommended)
[ ] Archive source system after parallel run