Skip to main content
Glama

batch_add_items

Add multiple tasks or projects to OmniFocus in one operation. Specify item details like name, notes, due date, tags, and more with a structured input for efficient task management.

Instructions

Add multiple tasks or projects to OmniFocus in a single operation

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
itemsYesArray of items (tasks or projects) to add

Implementation Reference

  • src/server.ts:60-65 (registration)
    Registration of the 'batch_add_items' tool, specifying name, description, schema, and handler.
    server.tool(
      "batch_add_items",
      "Add multiple tasks or projects to OmniFocus in a single operation",
      batchAddItemsTool.schema.shape,
      batchAddItemsTool.handler
    );
  • Zod schema defining the input parameters for the batch_add_items tool, including items array with task/project properties.
    export const schema = z.object({
      items: z.array(z.object({
        type: z.enum(['task', 'project']).describe("Type of item to add ('task' or 'project')"),
        name: z.string().describe("The name of the item"),
        note: z.string().optional().describe("Additional notes for the item"),
        dueDate: z.string().optional().describe("The due date in ISO format (YYYY-MM-DD or full ISO date)"),
        deferDate: z.string().optional().describe("The defer date in ISO format (YYYY-MM-DD or full ISO date)"),
        flagged: z.boolean().optional().describe("Whether the item is flagged or not"),
        estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
        tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
        
        // Task-specific properties
        projectName: z.string().optional().describe("For tasks: The name of the project to add the task to"),
        parentTaskId: z.string().optional().describe("For tasks: ID of the parent task"),
        parentTaskName: z.string().optional().describe("For tasks: Name of the parent task (scoped to project when provided)"),
        tempId: z.string().optional().describe("For tasks: Temporary ID for within-batch references"),
        parentTempId: z.string().optional().describe("For tasks: Reference to parent's tempId within the batch"),
        hierarchyLevel: z.number().int().min(0).optional().describe("Optional ordering hint (0=root, 1=child, ...)"),
        
        // Project-specific properties
        folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
        sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
      })).describe("Array of items (tasks or projects) to add")
      ,
      createSequentially: z.boolean().optional().describe("Process parents before children; when false, best-effort order will still try to resolve parents first")
    });
  • The MCP tool handler for batch_add_items, which calls the primitive batchAddItems function and formats the response with success/failure details.
    export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
      try {
        // Call the batchAddItems function
        const result = await batchAddItems(args.items as BatchAddItemsParams[]);
        
        if (result.success) {
          const successCount = result.results.filter(r => r.success).length;
          const failureCount = result.results.filter(r => !r.success).length;
          
          let message = `✅ Successfully added ${successCount} items.`;
          
          if (failureCount > 0) {
            message += ` ⚠️ Failed to add ${failureCount} items.`;
          }
          
          // Include details about added items
          const details = result.results.map((item, index) => {
            if (item.success) {
              const itemType = args.items[index].type;
              const itemName = args.items[index].name;
              return `- ✅ ${itemType}: "${itemName}"`;
            } else {
              const itemType = args.items[index].type;
              const itemName = args.items[index].name;
              return `- ❌ ${itemType}: "${itemName}" - Error: ${item.error}`;
            }
          }).join('\n');
          
          return {
            content: [{
              type: "text" as const,
              text: `${message}\n\n${details}`
            }]
          };
        } else {
          console.error('[batch_add_items] failure result:', JSON.stringify(result));
          // Batch operation failed completely or no items succeeded.
          const failureDetails = (result.results && result.results.length > 0)
            ? result.results.map((r, index) => {
                const itemType = args.items[index].type;
                const itemName = args.items[index].name;
                return r.success
                  ? `- ✅ ${itemType}: \"${itemName}\"`
                  : `- ❌ ${itemType}: \"${itemName}\" - Error: ${r?.error || 'Unknown error'}`;
              }).join('\\n')
            : `No items processed. ${result.error || ''}`;
    
          return {
            content: [{
              type: "text" as const,
              text: `Failed to process batch operation.\\n\\n${failureDetails}`
            }],
            isError: true
          };
        }
      } catch (err: unknown) {
        const error = err as Error;
        console.error(`Tool execution error: ${error.message}`);
        return {
          content: [{
            type: "text" as const,
            text: `Error processing batch operation: ${error.message}`
          }],
          isError: true
        };
      }
    } 
  • Core batchAddItems primitive function implementing the batch logic: cycle detection, topological processing of hierarchical items, calling addProject/addOmniFocusTask primitives.
    export async function batchAddItems(items: BatchAddItemsParams[]): Promise<BatchResult> {
      try {
        const results: ItemResult[] = new Array(items.length);
        const processed: boolean[] = new Array(items.length).fill(false);
        const tempToRealId = new Map<string, string>();
    
        // Pre-validate cycles in tempId -> parentTempId references
        const tempIndex = new Map<string, number>();
        items.forEach((it, idx) => { if (it.tempId) tempIndex.set(it.tempId, idx); });
    
        // Detect cycles using DFS and capture cycle paths
        const visiting = new Set<string>();
        const visited = new Set<string>();
        const inCycle = new Set<string>();
        const cycleMessageByTempId = new Map<string, string>();
        const stack: string[] = [];
    
        function dfs(tempId: string) {
          if (visited.has(tempId) || inCycle.has(tempId)) return;
          if (visiting.has(tempId)) return; // already on stack, handled by caller
          visiting.add(tempId);
          stack.push(tempId);
          const idx = tempIndex.get(tempId)!;
          const parentTemp = items[idx].parentTempId;
          if (parentTemp && tempIndex.has(parentTemp)) {
            if (visiting.has(parentTemp)) {
              // Found a cycle; construct path
              const startIdx = stack.indexOf(parentTemp);
              const cycleIds = stack.slice(startIdx).concat(parentTemp);
              const cycleNames = cycleIds.map(tid => {
                const i = tempIndex.get(tid)!;
                return items[i].name || tid;
              });
              const pathText = `${cycleNames.join(' -> ')}`;
              for (const tid of cycleIds) {
                inCycle.add(tid);
                cycleMessageByTempId.set(tid, `Cycle detected: ${pathText}`);
              }
            } else {
              dfs(parentTemp);
            }
          }
          stack.pop();
          visiting.delete(tempId);
          visited.add(tempId);
        }
    
        for (const tid of tempIndex.keys()) dfs(tid);
    
        // Mark items that participate in cycles as failed early
        for (const tid of inCycle) {
          const idx = tempIndex.get(tid)!;
          const msg = cycleMessageByTempId.get(tid) || `Cycle detected involving tempId: ${tid}`;
          results[idx] = { success: false, error: msg };
          processed[idx] = true;
        }
    
        // Mark items with unknown parentTempId (and no explicit parentTaskId) as invalid early
        items.forEach((it, idx) => {
          if (processed[idx]) return;
          if (it.parentTempId && !tempIndex.has(it.parentTempId) && !it.parentTaskId) {
            results[idx] = { success: false, error: `Unknown parentTempId: ${it.parentTempId}` };
            processed[idx] = true;
          }
        });
    
        // Stable order: sort by hierarchyLevel (undefined -> 0), then original index
        const indexed = items.map((it, idx) => ({ ...it, __index: idx }));
        indexed.sort((a, b) => (a.hierarchyLevel ?? 0) - (b.hierarchyLevel ?? 0) || a.__index - b.__index);
    
        let madeProgress = true;
        while (processed.some(p => !p) && madeProgress) {
          madeProgress = false;
          for (const item of indexed) {
            const i = item.__index;
            if (processed[i]) continue;
            try {
              if (item.type === 'project') {
                const projectParams: AddProjectParams = {
                  name: item.name,
                  note: item.note,
                  dueDate: item.dueDate,
                  deferDate: item.deferDate,
                  flagged: item.flagged,
                  estimatedMinutes: item.estimatedMinutes,
                  tags: item.tags,
                  folderName: item.folderName,
                  sequential: item.sequential
                };
    
                const projectResult = await addProject(projectParams);
                results[i] = {
                  success: projectResult.success,
                  id: projectResult.projectId,
                  error: projectResult.error
                };
                processed[i] = true;
                madeProgress = true;
                continue;
              }
    
              // task
              let parentTaskId = item.parentTaskId;
              if (!parentTaskId && item.parentTempId) {
                parentTaskId = tempToRealId.get(item.parentTempId);
                if (!parentTaskId) {
                  // Parent not created yet; skip this round
                  continue;
                }
              }
    
              const taskParams: AddOmniFocusTaskParams = {
                name: item.name,
                note: item.note,
                dueDate: item.dueDate,
                deferDate: item.deferDate,
                flagged: item.flagged,
                estimatedMinutes: item.estimatedMinutes,
                tags: item.tags,
                projectName: item.projectName,
                parentTaskId,
                parentTaskName: item.parentTaskName,
                hierarchyLevel: item.hierarchyLevel
              };
    
              const taskResult = await addOmniFocusTask(taskParams);
              results[i] = {
                success: taskResult.success,
                id: taskResult.taskId,
                error: taskResult.error
              };
              if (item.tempId && taskResult.taskId && taskResult.success) {
                tempToRealId.set(item.tempId, taskResult.taskId);
              }
              processed[i] = true;
              madeProgress = true;
            } catch (itemError: any) {
              results[i] = {
                success: false,
                error: itemError?.message || 'Unknown error processing item'
              };
              processed[i] = true; // avoid infinite loop on thrown errors
              madeProgress = true;
            }
          }
        }
    
        // Any unprocessed due to dependencies/cycles -> fail with message
        for (const item of indexed) {
          const i = item.__index;
          if (!processed[i]) {
            const reason = item.parentTempId && !tempToRealId.has(item.parentTempId)
              ? `Unresolved parentTempId: ${item.parentTempId}`
              : 'Unresolved dependency or cycle';
            results[i] = { success: false, error: reason };
            processed[i] = true;
          }
        }
    
        const overallSuccess = results.some(r => r?.success);
        return { success: overallSuccess, results };
      } catch (error: any) {
        console.error('Error in batchAddItems:', error);
        return { success: false, results: [], error: error?.message || 'Unknown error in batchAddItems' };
      }
    }
  • TypeScript type definition for BatchAddItemsParams used by the primitive function, matching the schema structure.
    export type BatchAddItemsParams = {
      type: 'task' | 'project';
      name: string;
      note?: string;
      dueDate?: string;
      deferDate?: string;
      flagged?: boolean;
      estimatedMinutes?: number;
      tags?: string[];
      projectName?: string; // For tasks
      // Hierarchy for tasks
      parentTaskId?: string;
      parentTaskName?: string;
      tempId?: string;
      parentTempId?: string;
      hierarchyLevel?: number;
      folderName?: string; // For projects
      sequential?: boolean; // For projects
    };
Install Server

Other Tools

Related 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/themotionmachine/OmniFocus-MCP'

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