技能 效率工具 ClickUp项目数据迁移工具

ClickUp项目数据迁移工具

v20260423
clickup-migration-deep-dive
该脚本提供了一个深入的项目数据迁移解决方案,旨在帮助用户将来自Jira、Asana、Trello等外部项目管理工具的数据导入ClickUp。它不仅支持跨工作区的数据同步,还处理了复杂的层级映射(如看板到空间,任务到任务),确保了项目结构和数据的完整性。
获取技能
345 次下载
概览

ClickUp Migration Deep Dive

Overview

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.

Migration Types

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

ClickUp Hierarchy Mapping

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)

Migration Script

// 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 };
}

Build Member Lookup

// 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;
}

Workspace-to-Workspace Migration

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
  }
}

Validation

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,
  };
}

Error Handling

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

Resources

Next Steps

For advanced troubleshooting during migration, see clickup-debug-bundle.

信息
Category 效率工具
Name clickup-migration-deep-dive
版本 v20260423
大小 7.32KB
更新时间 2026-04-28
语言