From clickup-pack
Guides ClickUp data migration from Jira, Asana, Trello or between workspaces using API v2, with hierarchy mappings and TypeScript script.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin clickup-packThis skill is limited to using the following tools:
Migrate project data to ClickUp from external tools or between ClickUp workspaces using API v2. Covers data mapping, batch creation, custom field migration, and validation.
Guides migration from Jira, Asana, GitHub Issues to Linear using checklists, workflow mappings, TypeScript export scripts, and batch import steps.
Interacts with ClickUp REST API to manage tasks, spaces, lists, assignees. Handles pagination, subtasks for reporting, automation, and workflow queries.
Syncs tasks bidirectionally between Claude Code and PM platforms including GitHub Projects, Linear, Notion, Asana, Trello, ClickUp, Monday.com, Todoist, and local storage. Handles credentials via env vars and resolves sync conflicts with internal state as source of truth.
Share bugs, ideas, or general feedback.
Migrate project data to ClickUp from external tools or between ClickUp workspaces using API v2. Covers data mapping, batch creation, custom field migration, and validation.
| Source | Complexity | Key Challenge |
|---|---|---|
| Trello | Low | Board -> List mapping, labels -> tags |
| Asana | Medium | Sections -> statuses, custom fields |
| Jira | High | Epics/stories/subtasks, custom fields, workflows |
| Another ClickUp workspace | Medium | Custom field UUIDs differ per workspace |
External Concept ClickUp API v2 Target
───────────────── ──────────────────────
Project/Board → Space (POST /team/{team_id}/space)
Epic/Section → Folder (POST /space/{space_id}/folder)
Sprint/Column → List (POST /folder/{folder_id}/list)
Issue/Card/Task → Task (POST /list/{list_id}/task)
Subtask → Task with parent (parent field)
Label/Tag → Tag (POST /task/{task_id}/tag/{tag_name})
Custom Field → Custom Field (POST /task/{task_id}/field/{field_id})
Comment → Comment (POST /task/{task_id}/comment)
Attachment → Attachment (POST /task/{task_id}/attachment)
// src/migrate-to-clickup.ts
interface MigrationItem {
externalId: string;
name: string;
description: string;
status: string;
priority?: 'urgent' | 'high' | 'normal' | 'low';
assigneeEmail?: string;
dueDate?: string;
labels?: string[];
subtasks?: MigrationItem[];
}
const PRIORITY_MAP: Record<string, number> = {
urgent: 1, high: 2, normal: 3, low: 4,
};
const STATUS_MAP: Record<string, string> = {
'To Do': 'to do',
'In Progress': 'in progress',
'Done': 'complete',
'Backlog': 'to do',
// Add your status mappings here
};
async function migrateItems(
items: MigrationItem[],
listId: string,
memberEmails: Map<string, number>, // email -> ClickUp user ID
): Promise<{ migrated: number; errors: Array<{ item: string; error: string }> }> {
let migrated = 0;
const errors: Array<{ item: string; error: string }> = [];
for (const item of items) {
try {
const assignees = item.assigneeEmail && memberEmails.has(item.assigneeEmail)
? [memberEmails.get(item.assigneeEmail)!]
: [];
const task = await clickupRequest(`/list/${listId}/task`, {
method: 'POST',
body: JSON.stringify({
name: item.name,
markdown_description: item.description,
status: STATUS_MAP[item.status] ?? 'to do',
priority: item.priority ? PRIORITY_MAP[item.priority] : null,
assignees,
due_date: item.dueDate ? new Date(item.dueDate).getTime() : undefined,
due_date_time: !!item.dueDate,
tags: item.labels ?? [],
}),
});
// Migrate subtasks
if (item.subtasks?.length) {
for (const subtask of item.subtasks) {
await clickupRequest(`/list/${listId}/task`, {
method: 'POST',
body: JSON.stringify({
name: subtask.name,
markdown_description: subtask.description,
parent: task.id,
status: STATUS_MAP[subtask.status] ?? 'to do',
}),
});
}
}
migrated++;
console.log(`Migrated: ${item.name} -> ${task.id}`);
// Rate limit: stay under 100 req/min
await new Promise(r => setTimeout(r, 700));
} catch (error) {
errors.push({ item: item.name, error: String(error) });
}
}
return { migrated, errors };
}
// Map external emails to ClickUp user IDs
async function buildMemberLookup(teamId: string): Promise<Map<string, number>> {
const data = await clickupRequest(`/team/${teamId}`);
const lookup = new Map<string, number>();
for (const member of data.team.members) {
lookup.set(member.user.email, member.user.id);
}
return lookup;
}
async function cloneListBetweenWorkspaces(
sourceToken: string,
sourceListId: string,
destToken: string,
destListId: string,
) {
// Fetch all tasks from source
const sourceTasks = [];
let page = 0;
let hasMore = true;
while (hasMore) {
const data = await fetch(
`https://api.clickup.com/api/v2/list/${sourceListId}/task?page=${page}&subtasks=true&include_closed=true`,
{ headers: { 'Authorization': sourceToken } }
).then(r => r.json());
sourceTasks.push(...data.tasks);
hasMore = data.tasks.length === 100;
page++;
}
console.log(`Fetched ${sourceTasks.length} tasks from source`);
// Create tasks in destination
for (const task of sourceTasks) {
if (task.parent) continue; // Handle subtasks separately
const created = await fetch(
`https://api.clickup.com/api/v2/list/${destListId}/task`,
{
method: 'POST',
headers: { 'Authorization': destToken, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: task.name,
markdown_description: task.description,
priority: task.priority?.id ? parseInt(task.priority.id) : null,
tags: task.tags.map((t: any) => t.name),
}),
}
).then(r => r.json());
console.log(`Cloned: ${task.name} -> ${created.id}`);
await new Promise(r => setTimeout(r, 700)); // Rate limit
}
}
async function validateMigration(
sourceItems: MigrationItem[],
listId: string,
): Promise<{ match: number; missing: string[] }> {
const tasks = await clickupRequest(`/list/${listId}/task?include_closed=true`);
const taskNames = new Set(tasks.tasks.map((t: any) => t.name));
const missing = sourceItems
.filter(item => !taskNames.has(item.name))
.map(item => item.name);
return {
match: sourceItems.length - missing.length,
missing,
};
}
| Issue | Cause | Solution |
|---|---|---|
| Rate limited during migration | Too many creates | Add 700ms delay between requests |
| Status not found | Status name mismatch | Map source statuses to ClickUp statuses |
| Assignee not found | Email not in workspace | Invite user first or skip assignment |
| Custom field UUID mismatch | Different workspace | Re-fetch field UUIDs via /list/{id}/field |
For advanced troubleshooting during migration, see clickup-debug-bundle.