Skip to main content
Glama

todoist_tasks_bulk_update

Update multiple Todoist tasks simultaneously using search filters to apply consistent changes across selected items.

Instructions

Update multiple tasks at once based on search criteria. Very efficient for updating many tasks with the same changes.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
search_criteriaYesCriteria to find tasks to update
updatesYesUpdates to apply to matching tasks

Implementation Reference

  • The main handler function that implements the bulk update logic: fetches all tasks, filters by search criteria, applies updates to content/description/due/priority/labels, moves to new project/section if specified, handles errors per task, and returns summary of updated tasks.
    export async function handleBulkUpdateTasks( todoistClient: TodoistApi, args: BulkUpdateTasksArgs ): Promise<string> { try { // Clear cache since we're updating taskCache.clear(); validateBulkSearchCriteria(args.search_criteria); const result = await todoistClient.getTasks(); const allTasks = extractArrayFromResponse<TodoistTask>(result); const matchingTasks = filterTasksByCriteria(allTasks, args.search_criteria); if (matchingTasks.length === 0) { // Provide more helpful information about why no tasks were found let debugInfo = "No tasks found matching the search criteria.\n"; debugInfo += "Search criteria used:\n"; if (args.search_criteria.project_id) { debugInfo += ` - Project ID: ${args.search_criteria.project_id}\n`; } if (args.search_criteria.content_contains) { debugInfo += ` - Content contains: "${args.search_criteria.content_contains}"\n`; } if (args.search_criteria.priority) { debugInfo += ` - Priority: ${args.search_criteria.priority}\n`; } if (args.search_criteria.due_before) { debugInfo += ` - Due before: ${args.search_criteria.due_before}\n`; } if (args.search_criteria.due_after) { debugInfo += ` - Due after: ${args.search_criteria.due_after}\n`; } debugInfo += `\nTotal tasks searched: ${allTasks.length}`; return debugInfo; } const updatedTasks: TodoistTask[] = []; const errors: string[] = []; validateLabels(args.updates.labels); const updateData: Partial<TodoistTaskData> = {}; if (args.updates.content) updateData.content = args.updates.content; if (args.updates.description) updateData.description = args.updates.description; if (args.updates.due_string) updateData.dueString = args.updates.due_string; const apiPriority = toApiPriority(args.updates.priority); if (apiPriority !== undefined) updateData.priority = apiPriority; const bulkLabelsProvided = Object.prototype.hasOwnProperty.call( args.updates, "labels" ); if (bulkLabelsProvided) { updateData.labels = Array.isArray(args.updates.labels) ? args.updates.labels : []; } let moveProjectId: string | undefined; if (args.updates.project_id) { try { moveProjectId = await resolveProjectIdentifier( todoistClient, args.updates.project_id ); } catch (error) { return `Failed to resolve project: ${(error as Error).message}`; } } const moveSectionId = args.updates.section_id; const hasUpdateFields = Object.keys(updateData).length > 0; for (const task of matchingTasks) { try { let latestTask = task; if (hasUpdateFields) { latestTask = await todoistClient.updateTask(task.id, updateData); } if (moveProjectId && moveProjectId !== latestTask.projectId) { const movedTasks = await todoistClient.moveTasks([task.id], { projectId: moveProjectId, }); if (movedTasks.length > 0) { latestTask = movedTasks[0]; } } if (moveSectionId && moveSectionId !== latestTask.sectionId) { const movedTasks = await todoistClient.moveTasks([task.id], { sectionId: moveSectionId, }); if (movedTasks.length > 0) { latestTask = movedTasks[0]; } } updatedTasks.push(latestTask); } catch (error) { errors.push( `Failed to update task "${task.content}": ${(error as Error).message}` ); } } const successCount = updatedTasks.length; const errorCount = errors.length; // Check if we're in dry-run mode const isDryRun = process.env.DRYRUN === "true"; const prefix = isDryRun ? "[DRY-RUN] " : ""; let response = `${prefix}Bulk update completed: ${successCount} updated, ${errorCount} failed.\n\n`; if (successCount > 0) { response += "Updated tasks:\n"; response += updatedTasks .map((task) => `- ${task.content} (ID: ${task.id})`) .join("\n"); response += "\n\n"; } if (errorCount > 0) { response += "Errors:\n"; response += errors.join("\n"); } return response.trim(); } catch (error) { ErrorHandler.handleAPIError("bulk update tasks", error); } }
  • The MCP Tool definition including input schema for search_criteria (project_id, priority, dates, content_contains) and updates (content, description, due_string, priority, labels, project_id, section_id).
    export const BULK_UPDATE_TASKS_TOOL: Tool = { name: "todoist_tasks_bulk_update", description: "Update multiple tasks at once based on search criteria. Very efficient for updating many tasks with the same changes.", inputSchema: { type: "object", properties: { search_criteria: { type: "object", properties: { project_id: { type: "string", description: "Filter tasks by project ID (optional, does not support names)", }, priority: { type: "number", description: "Filter tasks by priority level 1 (highest) to 4 (lowest) (optional)", enum: [1, 2, 3, 4], }, due_before: { type: "string", description: "Filter tasks due before this date (YYYY-MM-DD) (optional)", }, due_after: { type: "string", description: "Filter tasks due after this date (YYYY-MM-DD) (optional)", }, content_contains: { type: "string", description: "Filter tasks containing this text in content (optional)", }, }, description: "Criteria to find tasks to update", }, updates: { type: "object", properties: { content: { type: "string", description: "New content/title for matching tasks (optional)", }, description: { type: "string", description: "New description for matching tasks (optional)", }, due_string: { type: "string", description: "New due date in natural language (optional)", }, priority: { type: "number", description: "New priority from 1 (highest) to 4 (lowest) (optional)", enum: [1, 2, 3, 4], }, labels: { type: "array", items: { type: "string", }, description: "Array of label names to assign (optional)", }, project_id: { type: "string", description: "Move matching tasks to this project ID or name (optional)", }, section_id: { type: "string", description: "Move matching tasks to this section (optional)", }, }, description: "Updates to apply to matching tasks", }, }, required: ["search_criteria", "updates"], }, };
  • src/index.ts:219-224 (registration)
    The tool dispatch/registration in the main server request handler switch statement, which validates args using isBulkUpdateTasksArgs and calls the handleBulkUpdateTasks function.
    case "todoist_tasks_bulk_update": if (!isBulkUpdateTasksArgs(args)) { throw new Error("Invalid arguments for todoist_tasks_bulk_update"); } result = await handleBulkUpdateTasks(apiClient, args); break;
  • Type guard function used for runtime validation of input arguments in the registration/dispatch.
    export function isBulkUpdateTasksArgs( args: unknown ): args is BulkUpdateTasksArgs { if (typeof args !== "object" || args === null) return false; const obj = args as Record<string, unknown>; return ( "search_criteria" in obj && "updates" in obj && typeof obj.search_criteria === "object" && obj.search_criteria !== null && typeof obj.updates === "object" && obj.updates !== null ); }
  • Helper function to filter tasks based on bulk update search criteria (project, priority, content_contains, due dates). Used within the handler.
    function filterTasksByCriteria( tasks: TodoistTask[], criteria: BulkTaskFilterArgs["search_criteria"] ): TodoistTask[] { return tasks.filter((task) => { if (criteria.project_id && task.projectId !== criteria.project_id) return false; const apiPriorityFilter = toApiPriority(criteria.priority); if (apiPriorityFilter !== undefined && task.priority !== apiPriorityFilter) return false; // Fix for issue #34: Handle empty string in content_contains if (criteria.content_contains !== undefined) { // Treat empty or whitespace-only strings as "no match" const searchTerm = criteria.content_contains.trim(); if (searchTerm === "") { // Empty search should match nothing, not everything return false; } if (!task.content.toLowerCase().includes(searchTerm.toLowerCase())) { return false; } } if (criteria.due_before || criteria.due_after) { const taskDate = getDueDateOnly(task.due); if (!taskDate) { return false; } const isBeforeThreshold = !criteria.due_before || taskDate < criteria.due_before; const isAfterThreshold = !criteria.due_after || taskDate > criteria.due_after; if (!isBeforeThreshold || !isAfterThreshold) { return false; } } return true; }); }

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/greirson/mcp-todoist'

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