motion_custom_fields
Manage custom fields for tasks and projects in Motion to track additional data beyond standard fields. Create, list, delete fields, and assign them to specific projects or tasks.
Instructions
Manage custom fields for tasks and projects. Required params per operation: list: workspaceId or workspaceName. create: workspaceId/workspaceName + name + field (type); options[] also required for select/multiSelect. delete: workspaceId/workspaceName + fieldId. add_to_project: projectId + fieldId. remove_from_project: projectId + valueId. add_to_task: taskId + fieldId. remove_from_task: taskId + valueId.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| operation | Yes | Operation to perform | |
| fieldId | No | Custom field definition ID. Required for: delete, add_to_project, add_to_task. For remove operations, use valueId instead. | |
| valueId | No | Custom field value assignment ID (not the field definition ID). Required for: remove_from_project, remove_from_task. | |
| workspaceId | No | Workspace ID. Required for: list, create, delete. | |
| workspaceName | No | Workspace name (alternative to workspaceId). Required for: list, create, delete. | |
| name | No | Field name. Required for: create. | |
| field | No | Field type. Required for: create. Also needed for add_to_project/add_to_task when providing a non-null value. | |
| options | No | Option labels. Required for: create when field is select or multiSelect. | |
| required | No | Whether field is required on tasks/projects. | |
| projectId | No | Project ID. Required for: add_to_project, remove_from_project. | |
| taskId | No | Task ID. Required for: add_to_task, remove_from_task. | |
| value | No | Field value to set. Optional for add_to_project/add_to_task. When provided and non-null, the field param (type) is also required. |
Implementation Reference
- src/handlers/CustomFieldHandler.ts:7-135 (handler)Main handler class implementing the motion_custom_fields tool. Supports 7 operations: list, create, delete, add_to_project, remove_from_project, add_to_task, remove_from_task. Each operation has a dedicated private method with validation and error handling.export class CustomFieldHandler extends BaseHandler { async handle(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { try { const { operation } = args; switch(operation) { case 'list': return await this.handleList(args); case 'create': return await this.handleCreate(args); case 'delete': return await this.handleDelete(args); case 'add_to_project': return await this.handleAddToProject(args); case 'remove_from_project': return await this.handleRemoveFromProject(args); case 'add_to_task': return await this.handleAddToTask(args); case 'remove_from_task': return await this.handleRemoveFromTask(args); default: return this.handleUnknownOperation(operation); } } catch (error: unknown) { return this.handleError(error); } } private async handleList(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if (!args.workspaceId && !args.workspaceName) { return this.handleError(new Error('Workspace ID or workspace name is required for list operation')); } const workspace = await this.workspaceResolver.resolveWorkspace({ workspaceId: args.workspaceId, workspaceName: args.workspaceName }); const fields = await this.motionService.getCustomFields(workspace.id); return formatCustomFieldList(fields); } private async handleCreate(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if ((!args.workspaceId && !args.workspaceName) || !args.name || !args.field) { return this.handleError(new Error('Workspace ID/name, name, and field are required for create operation')); } if (args.name.length > LIMITS.CUSTOM_FIELD_NAME_MAX_LENGTH) { return this.handleError(new Error(`Field name exceeds ${LIMITS.CUSTOM_FIELD_NAME_MAX_LENGTH} characters`)); } const isSelectType = ['select', 'multiSelect'].includes(args.field); if (!isSelectType && args.options) { return this.handleError(new Error('Options parameter is only allowed for select/multiSelect field types')); } if (isSelectType && !args.options) { return this.handleError(new Error('Options parameter is required for select/multiSelect field types')); } const workspace = await this.workspaceResolver.resolveWorkspace({ workspaceId: args.workspaceId, workspaceName: args.workspaceName }); const fieldData: CreateCustomFieldData = { name: args.name, field: args.field, ...(args.required !== undefined && { required: args.required }), ...(args.options && { metadata: { options: args.options } }) }; const newField = await this.motionService.createCustomField(workspace.id, fieldData); return formatCustomFieldDetail(newField); } private async handleDelete(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if ((!args.workspaceId && !args.workspaceName) || !args.fieldId) { return this.handleError(new Error('Workspace ID/name and field ID are required for delete operation')); } const workspace = await this.workspaceResolver.resolveWorkspace({ workspaceId: args.workspaceId, workspaceName: args.workspaceName }); await this.motionService.deleteCustomField(workspace.id, args.fieldId); return formatCustomFieldSuccess('deleted'); } private async handleAddToProject(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if (!args.projectId || !args.fieldId) { return this.handleError(new Error('Project ID and field ID are required for add_to_project operation')); } if (args.value !== undefined && args.value !== null && !args.field) { return this.handleError(new Error('Field type (field) is required when providing a value. Use "text", "number", "multiSelect", etc.')); } const result = await this.motionService.addCustomFieldToProject(args.projectId, args.fieldId, args.value, args.field); return formatCustomFieldSuccess('added', 'project', args.projectId, result); } private async handleRemoveFromProject(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if (!args.projectId || !args.valueId) { return this.handleError(new Error('Project ID and valueId (custom field value ID) are required for remove_from_project operation')); } await this.motionService.removeCustomFieldFromProject(args.projectId, args.valueId); return formatCustomFieldSuccess('removed', 'project', args.projectId); } private async handleAddToTask(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if (!args.taskId || !args.fieldId) { return this.handleError(new Error('Task ID and field ID are required for add_to_task operation')); } if (args.value !== undefined && args.value !== null && !args.field) { return this.handleError(new Error('Field type (field) is required when providing a value. Use "text", "number", "multiSelect", etc.')); } const result = await this.motionService.addCustomFieldToTask(args.taskId, args.fieldId, args.value, args.field); return formatCustomFieldSuccess('added', 'task', args.taskId, result); } private async handleRemoveFromTask(args: MotionCustomFieldsArgs): Promise<McpToolResponse> { if (!args.taskId || !args.valueId) { return this.handleError(new Error('Task ID and valueId (custom field value ID) are required for remove_from_task operation')); } await this.motionService.removeCustomFieldFromTask(args.taskId, args.valueId); return formatCustomFieldSuccess('removed', 'task', args.taskId); } }
- src/tools/ToolDefinitions.ts:289-355 (registration)Tool definition for motion_custom_fields with name, description, and complete input schema including all 7 operations (list, create, delete, add_to_project, remove_from_project, add_to_task, remove_to_task) and their required/optional parameters.export const customFieldsToolDefinition: McpToolDefinition = { name: TOOL_NAMES.CUSTOM_FIELDS, description: "Manage custom fields for tasks and projects. Required params per operation: list: workspaceId or workspaceName. create: workspaceId/workspaceName + name + field (type); options[] also required for select/multiSelect. delete: workspaceId/workspaceName + fieldId. add_to_project: projectId + fieldId. remove_from_project: projectId + valueId. add_to_task: taskId + fieldId. remove_from_task: taskId + valueId.", inputSchema: { type: "object", properties: { operation: { type: "string", enum: ["list", "create", "delete", "add_to_project", "remove_from_project", "add_to_task", "remove_from_task"], description: "Operation to perform" }, fieldId: { type: "string", description: "Custom field definition ID. Required for: delete, add_to_project, add_to_task. For remove operations, use valueId instead." }, valueId: { type: "string", description: "Custom field value assignment ID (not the field definition ID). Required for: remove_from_project, remove_from_task." }, workspaceId: { type: "string", description: "Workspace ID. Required for: list, create, delete." }, workspaceName: { type: "string", description: "Workspace name (alternative to workspaceId). Required for: list, create, delete." }, name: { type: "string", description: "Field name. Required for: create." }, field: { type: "string", enum: ["text", "url", "date", "person", "multiPerson", "phone", "select", "multiSelect", "number", "email", "checkbox", "relatedTo"], description: "Field type. Required for: create. Also needed for add_to_project/add_to_task when providing a non-null value." }, options: { type: "array", items: { type: "string" }, description: "Option labels. Required for: create when field is select or multiSelect." }, required: { type: "boolean", description: "Whether field is required on tasks/projects." }, projectId: { type: "string", description: "Project ID. Required for: add_to_project, remove_from_project." }, taskId: { type: "string", description: "Task ID. Required for: add_to_task, remove_from_task." }, value: { oneOf: [ { type: "string" }, { type: "number" }, { type: "boolean" }, { type: "array", items: { type: "string" } }, { type: "null" } ], description: "Field value to set. Optional for add_to_project/add_to_task. When provided and non-null, the field param (type) is also required." } }, required: ["operation"] } };
- src/types/mcp-tool-args.ts:80-95 (schema)TypeScript interface defining MotionCustomFieldsArgs with all possible parameters for the 7 operations, including operation type, fieldId, valueId, workspaceId/workspaceName, name, field type, options, required flag, projectId, taskId, and value.export type CustomFieldsOperation = 'list' | 'create' | 'delete' | 'add_to_project' | 'remove_from_project' | 'add_to_task' | 'remove_from_task'; export interface MotionCustomFieldsArgs { operation: CustomFieldsOperation; fieldId?: string; valueId?: string; workspaceId?: string; workspaceName?: string; name?: string; field?: 'text' | 'url' | 'date' | 'person' | 'multiPerson' | 'phone' | 'select' | 'multiSelect' | 'number' | 'email' | 'checkbox' | 'relatedTo'; options?: string[]; required?: boolean; projectId?: string; taskId?: string; value?: string | number | boolean | string[] | null; }
- src/handlers/HandlerFactory.ts:27-38 (registration)Handler factory registration mapping the TOOL_NAMES.CUSTOM_FIELDS constant to the CustomFieldHandler class, enabling dynamic handler instantiation based on tool name.private registerHandlers(): void { this.handlers.set(TOOL_NAMES.TASKS, TaskHandler); this.handlers.set(TOOL_NAMES.PROJECTS, ProjectHandler); this.handlers.set(TOOL_NAMES.WORKSPACES, WorkspaceHandler); this.handlers.set(TOOL_NAMES.USERS, UserHandler); this.handlers.set(TOOL_NAMES.SEARCH, SearchHandler); this.handlers.set(TOOL_NAMES.COMMENTS, CommentHandler); this.handlers.set(TOOL_NAMES.CUSTOM_FIELDS, CustomFieldHandler); this.handlers.set(TOOL_NAMES.RECURRING_TASKS, RecurringTaskHandler); this.handlers.set(TOOL_NAMES.SCHEDULES, ScheduleHandler); this.handlers.set(TOOL_NAMES.STATUSES, StatusHandler); }
- Helper functions for formatting custom field responses: formatCustomFieldList for listing multiple fields, formatCustomFieldDetail for single field details, and formatCustomFieldSuccess for operation success messages with valueId tracking.export function formatCustomFieldList(fields: MotionCustomField[]): CallToolResult { if (fields.length === 0) { return formatMcpSuccess("No custom fields found."); } const fieldFormatter = (field: MotionCustomField) => { const name = field.name ? `${field.name} ` : ''; return `- ${name}ID: ${field.id} [Type: ${field.field}]`; }; return formatListResponse(fields, `Found ${fields.length} custom field${fields.length === 1 ? '' : 's'}`, fieldFormatter); } /** * Format single custom field response */ export function formatCustomFieldDetail(field: MotionCustomField): CallToolResult { const details = [ `Custom field created successfully:`, field.name ? `- Name: ${field.name}` : null, `- ID: ${field.id}`, `- Type: ${field.field}` ].filter(Boolean).join('\n'); return formatMcpSuccess(details); } /** * Format success message for custom field operations */ export function formatCustomFieldSuccess(operation: string, entityType?: string, entityId?: string, apiResponse?: MotionCustomFieldValue): CallToolResult { let message = `Custom field ${operation} successfully`; if (entityType && entityId) { message += ` for ${entityType} ${entityId}`; } // For add operations, include the API response so users can obtain the valueId for later removal if (apiResponse) { if (apiResponse.id) { message += `\nValue ID: ${apiResponse.id}`; if (entityType) { message += `\nTip: Use this valueId with remove_from_${entityType} to remove this custom field assignment.`; } } else { message += `\nNote: The API did not return a valueId. To find the valueId, use motion_tasks (operation: get) or inspect the task/project's customFieldValues.`; } if (apiResponse.type) { message += `\nType: ${apiResponse.type}`; } if (apiResponse.value !== undefined) { message += `\nValue: ${JSON.stringify(apiResponse.value)}`; } } return formatMcpSuccess(message); }