todoist_bulk_move_tasks
Move multiple tasks with their subtasks to a different project, section, or parent task in Todoist using a single operation.
Instructions
Move multiple tasks (and their respective subtasks, if any; e.g., up to 10-20 parent tasks for best performance) to a different project, section, or make them subtasks of another task. Provide an array of taskIds and exactly one destination (projectId, sectionId, or parentId).
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| taskIds | Yes | An array of task IDs to move. | |
| projectId | No | The ID of the destination project. (Optional, use only one of projectId, sectionId, parentId) | |
| sectionId | No | The ID of the destination section. (Optional, use only one of projectId, sectionId, parentId) | |
| parentId | No | The ID of the parent task to move these tasks under. (Optional, use only one of projectId, sectionId, parentId) |
Implementation Reference
- src/index.ts:1648-1717 (handler)The core handler logic for executing the todoist_bulk_move_tasks tool. Validates input with isBulkMoveTasksArgs, extracts destination, loops through each taskId calling todoistClient.moveTasks([taskId], moveArgs), tracks successes/failures with detailed logging and verification, returns summary of results.if (name === "todoist_bulk_move_tasks") { if (!isBulkMoveTasksArgs(args)) { return { content: [{ type: "text", text: "Invalid arguments for bulk_move_tasks. Provide a non-empty array of taskIds and exactly one of: projectId, sectionId, or parentId (must be a non-empty string)." }], isError: true }; } try { const moveArgs: { projectId?: string; sectionId?: string; parentId?: string } = {}; if (args.projectId) moveArgs.projectId = args.projectId; else if (args.sectionId) moveArgs.sectionId = args.sectionId; else if (args.parentId) moveArgs.parentId = args.parentId; console.error(`[DEBUG] todoist_bulk_move_tasks: Attempting to move ${args.taskIds.length} task(s) individually. Destination args: ${JSON.stringify(moveArgs)}`); const results = { succeeded: [] as string[], failed: [] as { id: string, error: string }[], }; for (const taskId of args.taskIds) { try { console.error(`[DEBUG] Moving task ${taskId} to: ${JSON.stringify(moveArgs)}`); const individualMoveResult = await todoistClient.moveTasks([taskId], moveArgs as any); // Check if the API returned the task and if its properties reflect the move // For simplicity, we assume if no error is thrown, it was accepted by the API. // A more robust check would be to fetch the task again and verify its sectionId/projectId. if (individualMoveResult && individualMoveResult.length > 0 && individualMoveResult[0].id === taskId) { console.error(`[DEBUG] Task ${taskId} processed by API. Result: ${JSON.stringify(individualMoveResult[0])}`); // Further check if sectionId or projectId in individualMoveResult[0] matches moveArgs const movedTaskDetails = individualMoveResult[0]; let successfulMove = false; if (moveArgs.sectionId && movedTaskDetails.sectionId === moveArgs.sectionId) successfulMove = true; else if (moveArgs.projectId && movedTaskDetails.projectId === moveArgs.projectId) successfulMove = true; else if (moveArgs.parentId && movedTaskDetails.parentId === moveArgs.parentId) successfulMove = true; // If the API doesn't reflect the change immediately in the returned object, we might still count it as succeeded based on no error. // For now, we count as success if API call didn't throw and returned our task. if (successfulMove) { results.succeeded.push(taskId); } else { // This case means API processed it but didn't reflect the change in the returned object, or it was already there. // Could be a race condition or API behavior. We'll count it as attempted but not fully confirmed by response. console.warn(`[DEBUG] Task ${taskId} processed, but move not immediately confirmed in API response object. Counting as succeeded based on no error.`); results.succeeded.push(taskId); // Tentatively count as success } } else { // API call succeeded but didn't return our task, or returned empty array console.warn(`[DEBUG] Task ${taskId} move API call succeeded but task not found in response or empty response.`); results.succeeded.push(taskId); // Tentatively count as success if API didn't error } } catch (taskError: any) { console.error(`[DEBUG] Failed to move task ${taskId}: ${taskError.message}`); results.failed.push({ id: taskId, error: taskError.message }); } } let summaryMessage = `Bulk move attempt complete for ${args.taskIds.length} task(s). `; summaryMessage += `Succeeded: ${results.succeeded.length}. `; if (results.succeeded.length > 0) summaryMessage += `Moved IDs: ${results.succeeded.join(", ")}. `; summaryMessage += `Failed: ${results.failed.length}.`; if (results.failed.length > 0) { summaryMessage += ` Failed IDs: ${results.failed.map(f => `${f.id} (${f.error})`).join("; ")}`; } return { content: [{ type: "text", text: summaryMessage }], isError: results.failed.length > 0 && results.succeeded.length === 0, // Overall error if all fails }; } catch (error: any) { console.error(`[DEBUG] todoist_bulk_move_tasks: Outer error caught: ${error.message}`, error); return { content: [{ type: "text", text: `Error in bulk moving tasks: ${error.message}` }], isError: true }; } }
- src/index.ts:258-287 (schema)The Tool object definition specifying the name, description, and inputSchema (JSON Schema) for the todoist_bulk_move_tasks tool.const BULK_MOVE_TASKS_TOOL: Tool = { name: "todoist_bulk_move_tasks", description: "Move multiple tasks (and their respective subtasks, if any; e.g., up to 10-20 parent tasks for best performance) to a different project, section, or make them subtasks of another task. Provide an array of taskIds and exactly one destination (projectId, sectionId, or parentId).", inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "An array of task IDs to move.", minItems: 1 // Ensure at least one task ID is provided }, projectId: { type: "string", description: "The ID of the destination project. (Optional, use only one of projectId, sectionId, parentId)" }, sectionId: { type: "string", description: "The ID of the destination section. (Optional, use only one of projectId, sectionId, parentId)" }, parentId: { type: "string", description: "The ID of the parent task to move these tasks under. (Optional, use only one of projectId, sectionId, parentId)" } }, required: ["taskIds"] // Note: Validation for providing exactly one of projectId, sectionId, or parentId // is handled in the isBulkMoveTasksArgs type guard and the tool handler. } };
- src/index.ts:1083-1121 (registration)The server.setRequestHandler for ListToolsRequestSchema where BULK_MOVE_TASKS_TOOL is registered in the tools array (specifically at line 1096).server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Task tools CREATE_TASK_TOOL, QUICK_ADD_TASK_TOOL, GET_TASKS_TOOL, GET_TASK_TOOL, UPDATE_TASK_TOOL, DELETE_TASK_TOOL, COMPLETE_TASK_TOOL, REOPEN_TASK_TOOL, SEARCH_TASKS_TOOL, MOVE_TASK_TOOL, BULK_MOVE_TASKS_TOOL, // Project tools GET_PROJECTS_TOOL, GET_PROJECT_TOOL, CREATE_PROJECT_TOOL, UPDATE_PROJECT_TOOL, DELETE_PROJECT_TOOL, // Section tools GET_SECTIONS_TOOL, CREATE_SECTION_TOOL, UPDATE_SECTION_TOOL, DELETE_SECTION_TOOL, // Label tools CREATE_LABEL_TOOL, GET_LABEL_TOOL, GET_LABELS_TOOL, UPDATE_LABEL_TOOL, DELETE_LABEL_TOOL, // Comment tools CREATE_COMMENT_TOOL, GET_COMMENT_TOOL, GET_COMMENTS_TOOL, UPDATE_COMMENT_TOOL, DELETE_COMMENT_TOOL, ], }));
- src/index.ts:944-966 (helper)Type guard helper function isBulkMoveTasksArgs that validates the tool's input arguments: ensures non-empty taskIds array of strings and exactly one non-empty string destination (projectId, sectionId, or parentId).function isBulkMoveTasksArgs(args: unknown): args is { taskIds: string[]; projectId?: string; sectionId?: string; parentId?: string; } { if ( typeof args !== 'object' || args === null || !('taskIds' in args) || !Array.isArray((args as any).taskIds) || (args as any).taskIds.length === 0 || !(args as any).taskIds.every((id: any) => typeof id === 'string') ) { return false; } const { projectId, sectionId, parentId } = args as any; const destinations = [projectId, sectionId, parentId]; const providedDestinations = destinations.filter(dest => dest !== undefined && dest !== null && String(dest).trim() !== ''); return providedDestinations.length === 1 && providedDestinations.every(dest => typeof dest === 'string'); }