Skip to main content
Glama
windalfin

ClickUp MCP Server

by windalfin

move_bulk_tasks

Move multiple tasks to a different list in ClickUp. Use task IDs or task names with list names. Note that task statuses may reset if the destination list has different status options.

Instructions

Move multiple tasks to a different list efficiently. For each task, you MUST provide either:

  1. taskId alone (preferred)

  2. taskName + listName

WARNING: Task statuses may reset if target list has different status options.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
tasksYesArray of tasks to move
targetListIdNoID of destination list (preferred). Use instead of targetListName if available.
targetListNameNoName of destination list. Only use if you don't have targetListId.

Implementation Reference

  • MCP tool handler for 'move_bulk_tasks'. Validates input, resolves task IDs and target list ID from names if provided, moves each task using taskService.moveTask in a loop, tracks success/failure, returns summary.
    export async function handleMoveBulkTasks(parameters: any) {
      const { tasks, targetListId, targetListName } = parameters;
      
      if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
        throw new Error('You must provide a non-empty array of tasks to move');
      }
    
      let finalTargetListId = targetListId;
          
      // If no targetListId but targetListName is provided, look up the list ID
      if (!finalTargetListId && targetListName) {
        const listInfo = await findListIDByName(workspaceService, targetListName);
        if (!listInfo) {
          throw new Error(`Target list "${targetListName}" not found`);
        }
        finalTargetListId = listInfo.id;
      }
      
      if (!finalTargetListId) {
        throw new Error("Either targetListId or targetListName must be provided");
      }
    
      const results = {
        total: tasks.length,
        successful: 0,
        failed: 0,
        failures: [] as any[]
      };
    
      for (const task of tasks) {
        try {
          let taskId = task.taskId;
          
          if (!taskId && task.taskName) {
            if (!task.listName) {
              throw new Error(`List name is required when using task name for task "${task.taskName}"`);
            }
            
            const listInfo = await findListIDByName(workspaceService, task.listName);
            if (!listInfo) {
              throw new Error(`List "${task.listName}" not found`);
            }
            const taskList = await taskService.getTasks(listInfo.id);
            const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
            
            if (!foundTask) {
              throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
            }
            taskId = foundTask.id;
          }
    
          if (!taskId) {
            throw new Error("Either taskId or taskName must be provided");
          }
    
          await taskService.moveTask(taskId, finalTargetListId);
          results.successful++;
        } catch (error: any) {
          results.failed++;
          results.failures.push({
            task: task.taskId || task.taskName,
            error: error.message
          });
        }
      }
    
      return {
        content: [{
          type: "text",
          text: JSON.stringify(results, null, 2)
        }]
      };
    } 
  • Input schema definition for the move_bulk_tasks tool, specifying parameters for tasks array and target list.
    export const moveBulkTasksTool = {
      name: "move_bulk_tasks",
      description: "Move multiple tasks to a different list efficiently. For each task, you MUST provide either:\n1. taskId alone (preferred)\n2. taskName + listName\n\nWARNING: Task statuses may reset if target list has different status options.",
      inputSchema: {
        type: "object",
        properties: {
          tasks: {
            type: "array",
            description: "Array of tasks to move",
            items: {
              type: "object",
              properties: {
                taskId: {
                  type: "string",
                  description: "Task ID (preferred). Use instead of taskName if available."
                },
                taskName: {
                  type: "string",
                  description: "Task name. Requires listName when used."
                },
                listName: {
                  type: "string",
                  description: "REQUIRED with taskName: List containing the task."
                }
              }
            }
          },
          targetListId: {
            type: "string",
            description: "ID of destination list (preferred). Use instead of targetListName if available."
          },
          targetListName: {
            type: "string",
            description: "Name of destination list. Only use if you don't have targetListId."
          }
        },
        required: ["tasks"]
      }
    };
  • src/server.ts:67-93 (registration)
    Registration of moveBulkTasksTool in the list of available tools returned by ListToolsRequestHandler.
    server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          workspaceHierarchyTool,
          createTaskTool,
          getTaskTool,
          getTasksTool,
          updateTaskTool,
          moveTaskTool,
          duplicateTaskTool,
          deleteTaskTool,
          createBulkTasksTool,
          updateBulkTasksTool,
          moveBulkTasksTool,
          deleteBulkTasksTool,
          createListTool,
          createListInFolderTool,
          getListTool,
          updateListTool,
          deleteListTool,
          createFolderTool,
          getFolderTool,
          updateFolderTool,
          deleteFolderTool
        ]
      };
    });
  • src/server.ts:120-122 (registration)
    Dispatcher case in CallToolRequestHandler that routes 'move_bulk_tasks' calls to handleMoveBulkTasks handler.
    case "move_bulk_tasks":
      return handleMoveBulkTasks(params as { tasks: any[], targetListId?: string, targetListName?: string });
    case "delete_bulk_tasks":
  • Bulk task moving helper method in ClickUpTaskService. Recreates tasks in new list and deletes originals with advanced batching and error handling. (Note: tool handler uses single moveTask loop instead of this bulk method.)
    async moveBulkTasks(
      tasks: string[],
      targetListId: string,
      options?: BulkOperationOptions,
      progressCallback?: (progress: ProgressInfo) => void
    ): Promise<BulkOperationResult<ClickUpTask>> {
      this.logOperation('moveBulkTasks', { 
        taskCount: tasks.length,
        targetListId,
        batchSize: options?.batchSize,
        concurrency: options?.concurrency
      });
      
      try {
        // First verify destination list exists
        const destinationList = await this.listService.getList(targetListId);
        if (!destinationList) {
          throw new ClickUpServiceError(
            `Destination list not found with ID: ${targetListId}`,
            ErrorCode.NOT_FOUND
          );
        }
    
        // Set default options for better performance
        const bulkOptions: BulkOperationOptions = {
          batchSize: options?.batchSize ?? 5, // Smaller batch size for better rate limit handling
          concurrency: options?.concurrency ?? 2, // Lower concurrency to avoid rate limits
          continueOnError: options?.continueOnError ?? true, // Continue on individual task failures
          retryCount: options?.retryCount ?? 3, // Retry failed operations
          ...options
        };
    
        return await this.bulkProcessor.processBulk(
          tasks,
          async (taskId) => {
            try {
              // Get the original task
              const originalTask = await this.getTask(taskId);
              const currentStatus = originalTask.status?.status;
              const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
    
              // Determine the appropriate status for the destination list
              let newStatus = availableStatuses.includes(currentStatus || '')
                ? currentStatus // Keep the same status if available in destination list
                : destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
    
              // Prepare the task data for the new list
              const taskData: CreateTaskData = {
                name: originalTask.name,
                description: originalTask.description,
                status: newStatus,
                priority: originalTask.priority?.priority as any,
                due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
                assignees: originalTask.assignees?.map(a => a.id) || []
              };
    
              // Create new task and delete old one in a single makeRequest call
              return await this.makeRequest(async () => {
                // First create the new task
                const response = await this.client.post<ClickUpTask>(
                  `/list/${targetListId}/task`,
                  taskData
                );
    
                // Then delete the original task
                await this.client.delete(`/task/${taskId}`);
    
                // Add a property to indicate the task was moved
                const newTask = {
                  ...response.data,
                  moved: true,
                  originalId: taskId
                };
    
                return newTask;
              });
            } catch (error) {
              // Enhance error context for better debugging
              if (error instanceof ClickUpServiceError) {
                error.context = {
                  ...error.context,
                  taskId,
                  targetListId,
                  targetListName: destinationList.name
                };
              }
              throw error;
            }
          },
          bulkOptions
        );
      } catch (error: any) {
        const errorMessage = error instanceof ClickUpServiceError ?
          error.message :
          `Failed to move tasks in bulk: ${error.message}`;
    
        throw new ClickUpServiceError(
          errorMessage,
          error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN,
          {
            targetListId,
            taskCount: tasks.length,
            error: error instanceof Error ? error.message : String(error)
          }
        );
      }
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively describes the mutation behavior ('Move multiple tasks'), provides efficiency context ('efficiently'), and includes a critical warning about status resetting. However, it doesn't mention error handling, rate limits, or authentication requirements that would be helpful for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is perfectly sized with three sentences that each earn their place: purpose statement, parameter requirements, and behavioral warning. It's front-loaded with the core functionality and uses clear bullet points for the parameter guidance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no annotations and no output schema, the description provides good context about the operation, parameter requirements, and a critical behavioral warning. However, it doesn't describe what the tool returns or provide guidance on error cases, which would be helpful given the complexity of bulk operations with multiple identification methods.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already fully documents all parameters. The description adds some semantic context about parameter preferences ('taskId alone (preferred)', 'taskName + listName'), but doesn't provide additional meaning beyond what's in the schema descriptions. This meets the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Move multiple tasks'), the resource ('tasks'), and the scope ('to a different list efficiently'). It distinguishes from the sibling 'move_task' by specifying bulk operation and provides explicit parameter requirements that differentiate it from other task manipulation tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit when-to-use guidance by stating 'For each task, you MUST provide either: 1. taskId alone (preferred) 2. taskName + listName'. It also includes a warning about status resetting with different target list options, which helps the agent understand when this tool might have unintended consequences versus alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/windalfin/clickup-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server