brainstorming
Structured design dialogue that validates ideas before implementation begins.
linear-migration-deep-dive
Migrate from Jira, Asana, GitHub Issues, or other tools to Linear.
Full skill instructions
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.
Data Volume
[ ] Total issues/tasks: ___
[ ] Projects/boards: ___
[ ] Users to map: ___
[ ] Attachments: ___
[ ] Custom fields: ___
[ ] Comments: ___
Workflow Analysis
[ ] Source statuses documented
[ ] Status-to-state mapping defined
[ ] Priority mapping defined
[ ] Issue type-to-label mapping defined
[ ] Automations to recreate: ___
Timeline
[ ] Migration window: ___
[ ] Parallel run period: ___
[ ] Cutover date: ___
[ ] Rollback deadline: ___
Jira -> Linear:
| Jira Status | Linear State (type) |
|---|---|
| To Do | Todo (unstarted) |
| In Progress | In Progress (started) |
| In Review | In Review (started) |
| Blocked | In Progress (started) + "Blocked" label |
| Done | Done (completed) |
| Won't Do | Canceled (canceled) |
| Jira Priority | Linear Priority |
|---|---|
| Highest/Blocker | 1 (Urgent) |
| High | 2 (High) |
| Medium | 3 (Medium) |
| Low/Lowest | 4 (Low) |
| Jira Issue Type | Linear Label |
|---|---|
| Bug | Bug |
| Story | Feature |
| Task | Task |
| Epic | (becomes Project or parent issue) |
Asana -> Linear:
| Asana Section | Linear State |
|---|---|
| Backlog | Backlog (backlog) |
| To Do | Todo (unstarted) |
| In Progress | In Progress (started) |
| Review | In Review (started) |
| Done | Done (completed) |
Jira Export:
// src/migration/jira-exporter.ts
interface JiraIssue {
key: string;
summary: string;
description: string;
status: string;
priority: string;
issuetype: string;
assignee?: string;
labels: string[];
storyPoints?: number;
parent?: string;
subtasks: string[];
}
async function exportJiraProject(
baseUrl: string,
projectKey: string,
authToken: string
): Promise<JiraIssue[]> {
const issues: JiraIssue[] = [];
let startAt = 0;
const maxResults = 100;
while (true) {
const jql = `project = ${projectKey} ORDER BY created ASC`;
const response = await fetch(
`${baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=summary,description,status,priority,issuetype,assignee,labels,customfield_10016,parent,subtasks`,
{ headers: { Authorization: `Basic ${authToken}`, Accept: "application/json" } }
);
const data = await response.json();
for (const issue of data.issues) {
issues.push({
key: issue.key,
summary: issue.fields.summary,
description: issue.fields.description?.content
? convertAtlassianDocToMarkdown(issue.fields.description)
: issue.fields.description ?? "",
status: issue.fields.status.name,
priority: issue.fields.priority?.name ?? "Medium",
issuetype: issue.fields.issuetype.name,
assignee: issue.fields.assignee?.emailAddress,
labels: issue.fields.labels ?? [],
storyPoints: issue.fields.customfield_10016,
parent: issue.fields.parent?.key,
subtasks: issue.fields.subtasks?.map((s: any) => s.key) ?? [],
});
}
startAt += maxResults;
if (startAt >= data.total) break;
}
console.log(`Exported ${issues.length} issues from Jira ${projectKey}`);
return issues;
}
Jira Markup -> Markdown Converter:
function convertJiraToMarkdown(text: string): string {
if (!text) return "";
return text
.replace(/h([1-6])\.\s/g, (_, level) => "#".repeat(parseInt(level)) + " ")
.replace(/\*([^*]+)\*/g, "**$1**")
.replace(/_([^_]+)_/g, "*$1*")
.replace(/\{code(?::([^}]*))?\}([\s\S]*?)\{code\}/g, "```$1\n$2\n```")
.replace(/\{noformat\}([\s\S]*?)\{noformat\}/g, "```\n$1\n```")
.replace(/^\*\s/gm, "- ")
.replace(/^#\s/gm, "1. ")
.replace(/\[([^|]+)\|([^\]]+)\]/g, "$1");
}
interface LinearImportIssue {
title: string;
description: string;
priority: number;
stateId: string;
assigneeId?: string;
labelIds: string[];
estimate?: number;
parentId?: string;
sourceId: string; // Original ID for tracking
}
async function transformJiraIssue(
jiraIssue: JiraIssue,
stateMap: Map<string, string>,
userMap: Map<string, string>,
labelMap: Map<string, string>
): Promise<LinearImportIssue> {
// Priority mapping
const priorityMap: Record<string, number> = {
Highest: 1, Blocker: 1,
High: 2,
Medium: 3,
Low: 4, Lowest: 4,
};
// Map labels
const labelIds: string[] = [];
// Issue type becomes a label
const typeLabel = labelMap.get(jiraIssue.issuetype);
if (typeLabel) labelIds.push(typeLabel);
// Original Jira labels
for (const label of jiraIssue.labels) {
const mapped = labelMap.get(label);
if (mapped) labelIds.push(mapped);
}
return {
title: jiraIssue.summary,
description: convertJiraToMarkdown(jiraIssue.description),
priority: priorityMap[jiraIssue.priority] ?? 3,
stateId: stateMap.get(jiraIssue.status) ?? stateMap.get("Todo")!,
assigneeId: jiraIssue.assignee ? userMap.get(jiraIssue.assignee) : undefined,
labelIds,
estimate: jiraIssue.storyPoints ?? undefined,
sourceId: jiraIssue.key,
};
}
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 };
}
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 };
}
[ ] 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
| Issue | Cause | Solution |
|---|---|---|
| User not found | Unmapped email | Add to userMap |
| Rate limited | Too fast import | Increase delay to 200ms |
| State not found | Unmapped status | Update stateMap |
| Parent not found | Import order wrong | Sort parents before children |
| Markup broken | Incomplete conversion | Improve markdown converter |
Structured design dialogue that validates ideas before implementation begins.
Comprehensive design intelligence for web and mobile UI/UX across 10 technology stacks.
Comprehensive implementation plans for multi-step tasks, breaking down specs into bite-sized, testable steps.
Introduction to the obra skills system with mandatory skill invocation rules and best practices.
Execute a written implementation plan with critical review and task checkpoints.
Delegate independent tasks to specialized agents working concurrently with isolated context.
Isolated git worktrees with smart directory selection and safety verification.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Plan searchable and shareable content that drives traffic, builds authority, and generates leads.
README-first repository scanner that extracts commands and classifies reproduction candidates without executing them.
Brainstorm and prioritize marketing strategies tailored to your SaaS stage, budget, and goals.
Plan and optimize your website's page hierarchy, navigation, URL structure, and internal linking.
The world's fastest calendar for remote work
Capture, organize, and utilize your knowledge effortlessly.
Instant Summaries of Audio & Video Interviews with AnySummary
Transform PDFs into engaging mind maps.
Chat with any PDF instantly
Create an optimal daily plan using your voice
A productivity platform to centralize organizational knowledge and workflows with contextual AI assistance.
AskYourPDF Pricing Plans: Tailored to Your Needs