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

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

With no annotations provided, the description carries full burden but only states it's a batch addition operation. It doesn't disclose behavioral traits such as whether it's idempotent, requires specific permissions, handles errors, or returns any output. For a mutation tool with zero annotation coverage, this is a significant gap in transparency.

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 a single, efficient sentence that front-loads key information ('Add multiple tasks or projects') without unnecessary words. It earns its place by clarifying the batch nature and target system, making it appropriately sized and well-structured.

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

Completeness2/5

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

Given no annotations and no output schema, the description is incomplete for a mutation tool. It lacks details on behavioral context, error handling, or return values, which are crucial for an agent to use it correctly. The high schema coverage doesn't compensate for these gaps in operational transparency.

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?

The schema description coverage is 100%, so the schema fully documents the 'items' parameter and its nested properties. The description adds no parameter semantics beyond implying batch processing, which is already covered by the tool name and schema. Baseline 3 is appropriate as the schema does the heavy lifting.

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

Purpose4/5

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

The description clearly states the action ('Add multiple tasks or projects') and target ('to OmniFocus in a single operation'), making the purpose evident. It distinguishes from single-item tools like 'add_omnifocus_task' and 'add_project' by emphasizing batch capability, though it doesn't explicitly name these siblings.

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

Usage Guidelines3/5

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

The description implies usage for adding multiple items at once, suggesting it's an alternative to single-item tools. However, it doesn't explicitly state when to use this over siblings like 'add_omnifocus_task' or 'add_project', nor does it mention prerequisites or exclusions, leaving guidance incomplete.

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

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