From b6afd38c450f07db839621414b8fc3966bce965c Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 13:16:08 +0300 Subject: [PATCH 1/6] feat: add ChangeItemColumnValuesBatchTool with basic success test Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ange-item-column-values-batch-tool.test.ts | 38 +++++++ .../change-item-column-values-batch-tool.ts | 105 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts new file mode 100644 index 00000000..9aa5309c --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts @@ -0,0 +1,38 @@ +import { createMockApiClient } from './test-utils/mock-api-client'; +import { ChangeItemColumnValuesBatchTool } from './change-item-column-values-batch-tool'; + +describe('ChangeItemColumnValuesBatchTool', () => { + let mocks: ReturnType; + + beforeEach(() => { + mocks = createMockApiClient(); + jest.clearAllMocks(); + }); + + describe('successful batch updates', () => { + it('updates multiple items and returns per-item results', async () => { + mocks.setResponses([ + { change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' } }, + { change_multiple_column_values: { id: '102', name: 'Item B', url: 'https://monday.com/102' } }, + { change_multiple_column_values: { id: '103', name: 'Item C', url: 'https://monday.com/103' } }, + ]); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + const result = await tool.execute({ + boardId: 456, + items: [ + { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, + { itemId: 102, columnValues: '{"status":{"label":"Done"}}' }, + { itemId: 103, columnValues: '{"status":{"label":"Done"}}' }, + ], + }); + + const content = result.content as Record; + expect(content.successful).toHaveLength(3); + expect(content.failed).toHaveLength(0); + expect(content.message).toContain('3 of 3'); + expect(mocks.getMockRequest()).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts new file mode 100644 index 00000000..307a629a --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts @@ -0,0 +1,105 @@ +import { z } from 'zod'; +import { + ChangeItemColumnValuesMutation, + ChangeItemColumnValuesMutationVariables, +} from 'src/monday-graphql/generated/graphql/graphql'; +import { changeItemColumnValues } from '../../../monday-graphql/queries.graphql'; +import { ToolInputType, ToolOutputType, ToolType } from '../../tool'; +import { BaseMondayApiTool, createMondayApiAnnotations } from './base-monday-api-tool'; + +const batchItemSchema = z.object({ + itemId: z.number().describe('The ID of the item to update'), + columnValues: z + .string() + .describe( + 'A JSON string of column values to set: {"column_id": "value", ...}. For status use {"label":"Done"}, for date use {"date":"2023-05-25"}.', + ), + createLabelsIfMissing: z + .boolean() + .optional() + .describe('If true, create missing Status/Dropdown labels. Requires board structure permissions.'), +}); + +export const changeItemColumnValuesBatchToolSchema = { + boardId: z.number().describe('The ID of the board containing the items to update'), + items: z + .array(batchItemSchema) + .min(1) + .max(50) + .describe('Array of items to update. Each item needs an itemId and columnValues. Max 50 items per batch.'), +}; + +export type ChangeItemColumnValuesBatchToolInput = typeof changeItemColumnValuesBatchToolSchema; + +export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool { + name = 'change_item_column_values_batch'; + type = ToolType.WRITE; + annotations = createMondayApiAnnotations({ + title: 'Change Item Column Values (Batch)', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + }); + + getDescription(): string { + return ( + 'Update column values for multiple items in a single batch operation. ' + + 'Each item is updated independently — partial failures do not block other items. ' + + 'Returns per-item success/failure results. Max 50 items per batch. ' + + '[REQUIRED PRECONDITION]: Before using this tool, use get_board_info to understand the board structure (column IDs, types, labels).' + ); + } + + getInputSchema(): ChangeItemColumnValuesBatchToolInput { + return changeItemColumnValuesBatchToolSchema; + } + + protected async executeInternal( + input: ToolInputType, + ): Promise> { + const boardId = this.context?.boardId ?? input.boardId; + + const results = await Promise.allSettled( + input.items.map(async (item) => { + const variables: ChangeItemColumnValuesMutationVariables = { + boardId: boardId.toString(), + itemId: item.itemId.toString(), + columnValues: item.columnValues, + ...(item.createLabelsIfMissing !== undefined && { + createLabelsIfMissing: item.createLabelsIfMissing, + }), + }; + + const res = await this.mondayApi.request(changeItemColumnValues, variables); + return { + itemId: item.itemId, + id: res.change_multiple_column_values?.id, + name: res.change_multiple_column_values?.name, + url: res.change_multiple_column_values?.url, + }; + }), + ); + + const successful = results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map((r) => r.value); + + const failed = input.items + .map((item, index) => ({ item, result: results[index] })) + .filter((entry): entry is { item: typeof entry.item; result: PromiseRejectedResult } => + entry.result.status === 'rejected', + ) + .map((entry) => ({ + itemId: entry.item.itemId, + error: entry.result.reason instanceof Error ? entry.result.reason.message : String(entry.result.reason), + })); + + return { + content: { + message: `${successful.length} of ${input.items.length} items updated successfully${failed.length > 0 ? `, ${failed.length} failed` : ''}`, + successful, + failed, + }, + }; + } +} From 4af36497acd93f0b92bcf9f1c0da9a8c2832f549 Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 13:17:43 +0300 Subject: [PATCH 2/6] test: add partial failure, boardId resolution, and createLabelsIfMissing tests for batch tool Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ange-item-column-values-batch-tool.test.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts index 9aa5309c..4c338d62 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts @@ -35,4 +35,117 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(mocks.getMockRequest()).toHaveBeenCalledTimes(3); }); }); + + describe('partial failure handling', () => { + it('reports per-item success and failure when some items fail', async () => { + const error = new Error('invalid value - unable to assign person with id: 3477320'); + mocks.mockRequest + .mockResolvedValueOnce({ + change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + }) + .mockRejectedValueOnce(error) + .mockResolvedValueOnce({ + change_multiple_column_values: { id: '103', name: 'Item C', url: 'https://monday.com/103' }, + }); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + const result = await tool.execute({ + boardId: 456, + items: [ + { itemId: 101, columnValues: '{"person":{"personsAndTeams":[{"id":1}]}}' }, + { itemId: 102, columnValues: '{"person":{"personsAndTeams":[{"id":3477320}]}}' }, + { itemId: 103, columnValues: '{"person":{"personsAndTeams":[{"id":1}]}}' }, + ], + }); + + const content = result.content as Record; + expect(content.successful).toHaveLength(2); + expect(content.failed).toHaveLength(1); + expect(content.failed[0].error).toContain('unable to assign person'); + expect(content.message).toContain('2 of 3'); + }); + + it('handles all items failing', async () => { + mocks.setError('Board not found'); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 999 }); + + const result = await tool.execute({ + boardId: 999, + items: [ + { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, + { itemId: 102, columnValues: '{"status":{"label":"Done"}}' }, + ], + }); + + const content = result.content as Record; + expect(content.successful).toHaveLength(0); + expect(content.failed).toHaveLength(2); + expect(content.message).toContain('0 of 2'); + }); + }); + + describe('boardId resolution', () => { + it('uses boardId from context when available', async () => { + mocks.setResponse({ + change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + }); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + await tool.execute({ + boardId: 789, + items: [{ itemId: 101, columnValues: '{"status":{"label":"Done"}}' }], + }); + + expect(mocks.getMockRequest()).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ boardId: '456' }), + ); + }); + + it('uses boardId from input when no context', async () => { + mocks.setResponse({ + change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + }); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient); + + await tool.execute({ + boardId: 789, + items: [{ itemId: 101, columnValues: '{"status":{"label":"Done"}}' }], + }); + + expect(mocks.getMockRequest()).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ boardId: '789' }), + ); + }); + }); + + describe('createLabelsIfMissing', () => { + it('passes createLabelsIfMissing per-item to the GraphQL mutation', async () => { + mocks.setResponses([ + { change_multiple_column_values: { id: '101', name: 'Item A', url: null } }, + { change_multiple_column_values: { id: '102', name: 'Item B', url: null } }, + ]); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + await tool.execute({ + boardId: 456, + items: [ + { itemId: 101, columnValues: '{"status":{"label":"New Label"}}', createLabelsIfMissing: true }, + { itemId: 102, columnValues: '{"status":{"label":"Existing"}}' }, + ], + }); + + const calls = mocks.getMockRequest().mock.calls; + expect(calls[0][1]).toEqual( + expect.objectContaining({ itemId: '101', createLabelsIfMissing: true }), + ); + expect(calls[1][1]).not.toHaveProperty('createLabelsIfMissing'); + }); + }); }); From d4dc4e07ee9759340ceab555ed395eee30e93ef4 Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 13:18:35 +0300 Subject: [PATCH 3/6] feat: register ChangeItemColumnValuesBatchTool in platform-api-tools index Co-Authored-By: Claude Opus 4.6 (1M context) --- .../agent-toolkit/src/core/tools/platform-api-tools/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts index 7c121fcb..4f4ffb0f 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/index.ts @@ -1,6 +1,7 @@ import { AllMondayApiTool } from './all-monday-api-tool'; import { BaseMondayApiToolConstructor } from './base-monday-api-tool'; import { ChangeItemColumnValuesTool } from './change-item-column-values-tool'; +import { ChangeItemColumnValuesBatchTool } from './change-item-column-values-batch-tool'; import { GetObjectSchemasTool } from './get-object-schemas-tool/get-object-schemas-tool'; import { CreateObjectSchemaTool } from './create-object-schema-tool/create-object-schema-tool'; import { UpdateObjectSchemaTool } from './update-object-schema-tool/update-object-schema-tool'; @@ -82,6 +83,7 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ FullBoardDataTool, ListUsersAndTeamsTool, ChangeItemColumnValuesTool, + ChangeItemColumnValuesBatchTool, MoveItemToGroupTool, CreateBoardTool, CreateFormTool, @@ -153,6 +155,7 @@ export * from './manage-object-schema-board-connection-tool/manage-object-schema export * from './manage-object-schema-columns-tool/manage-object-schema-columns-tool'; export * from './set-object-schema-column-active-state-tool/set-object-schema-column-active-state-tool'; export * from './change-item-column-values-tool'; +export * from './change-item-column-values-batch-tool'; export * from './create-board-tool'; export * from './workforms-tools/create-form-tool'; export * from './workforms-tools/update-form-tool'; From 5c4a53fd47c5bd3c1b2f9fd326a8cf8b5b80650c Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 13:36:32 +0300 Subject: [PATCH 4/6] fix(agent-toolkit): align batch tool with single-item tool standards - Add conditional boardId schema (omit when context provides it) - Use identical columnValues .describe() text with full JSON example - Add both preconditions (get_board_info + link_board_items_workflow) - Extract GraphQL response errors in failed items (same as rethrowWithContext) - Align itemId/createLabelsIfMissing .describe() text verbatim - Add 3 new tests: GraphQL error extraction, schema conditional tests Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ange-item-column-values-batch-tool.test.ts | 43 ++++++++++++++++--- .../change-item-column-values-batch-tool.ts | 40 +++++++++++++---- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts index 4c338d62..89f95f7d 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts @@ -20,7 +20,6 @@ describe('ChangeItemColumnValuesBatchTool', () => { const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); const result = await tool.execute({ - boardId: 456, items: [ { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, { itemId: 102, columnValues: '{"status":{"label":"Done"}}' }, @@ -51,7 +50,6 @@ describe('ChangeItemColumnValuesBatchTool', () => { const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); const result = await tool.execute({ - boardId: 456, items: [ { itemId: 101, columnValues: '{"person":{"personsAndTeams":[{"id":1}]}}' }, { itemId: 102, columnValues: '{"person":{"personsAndTeams":[{"id":3477320}]}}' }, @@ -72,7 +70,6 @@ describe('ChangeItemColumnValuesBatchTool', () => { const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 999 }); const result = await tool.execute({ - boardId: 999, items: [ { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, { itemId: 102, columnValues: '{"status":{"label":"Done"}}' }, @@ -84,6 +81,32 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(content.failed).toHaveLength(2); expect(content.message).toContain('0 of 2'); }); + + it('extracts GraphQL response errors from failed items', async () => { + const graphqlError = new Error('GraphQL Error'); + (graphqlError as any).response = { + errors: [{ message: 'Invalid column value' }, { message: 'Column not found' }], + }; + mocks.mockRequest + .mockResolvedValueOnce({ + change_multiple_column_values: { id: '101', name: 'Item A', url: null }, + }) + .mockRejectedValueOnce(graphqlError); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + const result = await tool.execute({ + items: [ + { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, + { itemId: 102, columnValues: '{"status":{"label":"Bad"}}' }, + ], + }); + + const content = result.content as Record; + expect(content.successful).toHaveLength(1); + expect(content.failed).toHaveLength(1); + expect(content.failed[0].error).toBe('Invalid column value, Column not found'); + }); }); describe('boardId resolution', () => { @@ -95,7 +118,6 @@ describe('ChangeItemColumnValuesBatchTool', () => { const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); await tool.execute({ - boardId: 789, items: [{ itemId: 101, columnValues: '{"status":{"label":"Done"}}' }], }); @@ -122,6 +144,18 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect.objectContaining({ boardId: '789' }), ); }); + + it('omits boardId from schema when context provides it', () => { + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + const schema = tool.getInputSchema(); + expect(schema).not.toHaveProperty('boardId'); + }); + + it('includes boardId in schema when no context', () => { + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient); + const schema = tool.getInputSchema(); + expect(schema).toHaveProperty('boardId'); + }); }); describe('createLabelsIfMissing', () => { @@ -134,7 +168,6 @@ describe('ChangeItemColumnValuesBatchTool', () => { const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); await tool.execute({ - boardId: 456, items: [ { itemId: 101, columnValues: '{"status":{"label":"New Label"}}', createLabelsIfMissing: true }, { itemId: 102, columnValues: '{"status":{"label":"Existing"}}' }, diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts index 307a629a..61285874 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts @@ -8,20 +8,21 @@ import { ToolInputType, ToolOutputType, ToolType } from '../../tool'; import { BaseMondayApiTool, createMondayApiAnnotations } from './base-monday-api-tool'; const batchItemSchema = z.object({ - itemId: z.number().describe('The ID of the item to update'), + itemId: z.number().describe('The ID of the item to be updated'), columnValues: z .string() .describe( - 'A JSON string of column values to set: {"column_id": "value", ...}. For status use {"label":"Done"}, for date use {"date":"2023-05-25"}.', + `A string containing the new column values for the item following this structure: {\\"column_id\\": \\"value\\",... you can change multiple columns at once, note that for status column you must use nested value with 'label' as a key and for date column use 'date' as key} - example: "{\\"text_column_id\\":\\"New text\\", \\"status_column_id\\":{\\"label\\":\\"Done\\"}, \\"date_column_id\\":{\\"date\\":\\"2023-05-25\\"}, \\"phone_id\\":\\"123-456-7890\\", \\"email_id\\":\\"test@example.com\\"}"`, ), createLabelsIfMissing: z .boolean() .optional() - .describe('If true, create missing Status/Dropdown labels. Requires board structure permissions.'), + .describe( + 'If true, create missing Status/Dropdown labels when setting those columns. Requires permission to change board structure. Omit or false to only use existing labels.', + ), }); export const changeItemColumnValuesBatchToolSchema = { - boardId: z.number().describe('The ID of the board containing the items to update'), items: z .array(batchItemSchema) .min(1) @@ -29,7 +30,14 @@ export const changeItemColumnValuesBatchToolSchema = { .describe('Array of items to update. Each item needs an itemId and columnValues. Max 50 items per batch.'), }; -export type ChangeItemColumnValuesBatchToolInput = typeof changeItemColumnValuesBatchToolSchema; +export const changeItemColumnValuesBatchInBoardToolSchema = { + boardId: z.number().describe('The ID of the board containing the items to update'), + ...changeItemColumnValuesBatchToolSchema, +}; + +export type ChangeItemColumnValuesBatchToolInput = + | typeof changeItemColumnValuesBatchToolSchema + | typeof changeItemColumnValuesBatchInBoardToolSchema; export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool { name = 'change_item_column_values_batch'; @@ -46,18 +54,24 @@ export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool, ): Promise> { - const boardId = this.context?.boardId ?? input.boardId; + const boardId = + this.context?.boardId ?? (input as ToolInputType).boardId; const results = await Promise.allSettled( input.items.map(async (item) => { @@ -91,7 +105,7 @@ export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool ({ itemId: entry.item.itemId, - error: entry.result.reason instanceof Error ? entry.result.reason.message : String(entry.result.reason), + error: this.extractErrorMessage(entry.result.reason), })); return { @@ -102,4 +116,12 @@ export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool e.message)?.join(', '); + if (graphQLErrors) { + return graphQLErrors; + } + return error instanceof Error ? error.message : String(error); + } } From 6d2bba035de864bb169d3cf00db5714d915db70e Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 15:05:09 +0300 Subject: [PATCH 5/6] refactor(agent-toolkit): use GraphQL aliased mutations instead of Promise.allSettled Replace N individual HTTP requests with a single GraphQL request containing aliased change_multiple_column_values mutations. This reduces API round-trips from O(n) to O(1) and avoids rate-limit exposure on large batches. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ange-item-column-values-batch-tool.test.ts | 112 +++++++++++------- .../change-item-column-values-batch-tool.ts | 102 ++++++++++------ 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts index 89f95f7d..148d8e99 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts @@ -10,12 +10,12 @@ describe('ChangeItemColumnValuesBatchTool', () => { }); describe('successful batch updates', () => { - it('updates multiple items and returns per-item results', async () => { - mocks.setResponses([ - { change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' } }, - { change_multiple_column_values: { id: '102', name: 'Item B', url: 'https://monday.com/102' } }, - { change_multiple_column_values: { id: '103', name: 'Item C', url: 'https://monday.com/103' } }, - ]); + it('sends a single GraphQL request with aliased mutations and returns per-item results', async () => { + mocks.setResponse({ + item_0: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + item_1: { id: '102', name: 'Item B', url: 'https://monday.com/102' }, + item_2: { id: '103', name: 'Item C', url: 'https://monday.com/103' }, + }); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); @@ -31,21 +31,43 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(content.successful).toHaveLength(3); expect(content.failed).toHaveLength(0); expect(content.message).toContain('3 of 3'); - expect(mocks.getMockRequest()).toHaveBeenCalledTimes(3); + expect(mocks.getMockRequest()).toHaveBeenCalledTimes(1); + }); + + it('builds query with correct aliased mutation structure', async () => { + mocks.setResponse({ + item_0: { id: '101', name: 'Item A', url: null }, + }); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + await tool.execute({ + items: [{ itemId: 101, columnValues: '{"status":{"label":"Done"}}' }], + }); + + const [query, variables] = mocks.getMockRequest().mock.calls[0]; + expect(query).toContain('item_0: change_multiple_column_values'); + expect(query).toContain('board_id: $boardId'); + expect(query).toContain('item_id: $itemId_0'); + expect(query).toContain('column_values: $columnValues_0'); + expect(variables).toEqual( + expect.objectContaining({ boardId: '456', itemId_0: '101', columnValues_0: '{"status":{"label":"Done"}}' }), + ); }); }); describe('partial failure handling', () => { - it('reports per-item success and failure when some items fail', async () => { + it('reports per-item success and failure from GraphQL partial response', async () => { const error = new Error('invalid value - unable to assign person with id: 3477320'); - mocks.mockRequest - .mockResolvedValueOnce({ - change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, - }) - .mockRejectedValueOnce(error) - .mockResolvedValueOnce({ - change_multiple_column_values: { id: '103', name: 'Item C', url: 'https://monday.com/103' }, - }); + (error as any).response = { + data: { + item_0: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + item_1: null, + item_2: { id: '103', name: 'Item C', url: 'https://monday.com/103' }, + }, + errors: [{ message: 'invalid value - unable to assign person with id: 3477320', path: ['item_1'] }], + }; + mocks.mockRequest.mockRejectedValue(error); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); @@ -62,9 +84,10 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(content.failed).toHaveLength(1); expect(content.failed[0].error).toContain('unable to assign person'); expect(content.message).toContain('2 of 3'); + expect(mocks.getMockRequest()).toHaveBeenCalledTimes(1); }); - it('handles all items failing', async () => { + it('handles all items failing when no data returned', async () => { mocks.setError('Board not found'); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 999 }); @@ -82,38 +105,37 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(content.message).toContain('0 of 2'); }); - it('extracts GraphQL response errors from failed items', async () => { - const graphqlError = new Error('GraphQL Error'); - (graphqlError as any).response = { - errors: [{ message: 'Invalid column value' }, { message: 'Column not found' }], + it('handles all items failing with partial data (all null)', async () => { + const error = new Error('GraphQL Error'); + (error as any).response = { + data: { item_0: null, item_1: null }, + errors: [ + { message: 'Invalid column value', path: ['item_0'] }, + { message: 'Column not found', path: ['item_1'] }, + ], }; - mocks.mockRequest - .mockResolvedValueOnce({ - change_multiple_column_values: { id: '101', name: 'Item A', url: null }, - }) - .mockRejectedValueOnce(graphqlError); + mocks.mockRequest.mockRejectedValue(error); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); const result = await tool.execute({ items: [ - { itemId: 101, columnValues: '{"status":{"label":"Done"}}' }, + { itemId: 101, columnValues: '{"status":{"label":"Bad"}}' }, { itemId: 102, columnValues: '{"status":{"label":"Bad"}}' }, ], }); const content = result.content as Record; - expect(content.successful).toHaveLength(1); - expect(content.failed).toHaveLength(1); - expect(content.failed[0].error).toBe('Invalid column value, Column not found'); + expect(content.successful).toHaveLength(0); + expect(content.failed).toHaveLength(2); + expect(content.failed[0].error).toBe('Invalid column value'); + expect(content.failed[1].error).toBe('Column not found'); }); }); describe('boardId resolution', () => { it('uses boardId from context when available', async () => { - mocks.setResponse({ - change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, - }); + mocks.setResponse({ item_0: { id: '101', name: 'Item A', url: 'https://monday.com/101' } }); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); @@ -128,9 +150,7 @@ describe('ChangeItemColumnValuesBatchTool', () => { }); it('uses boardId from input when no context', async () => { - mocks.setResponse({ - change_multiple_column_values: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, - }); + mocks.setResponse({ item_0: { id: '101', name: 'Item A', url: 'https://monday.com/101' } }); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient); @@ -159,11 +179,11 @@ describe('ChangeItemColumnValuesBatchTool', () => { }); describe('createLabelsIfMissing', () => { - it('passes createLabelsIfMissing per-item to the GraphQL mutation', async () => { - mocks.setResponses([ - { change_multiple_column_values: { id: '101', name: 'Item A', url: null } }, - { change_multiple_column_values: { id: '102', name: 'Item B', url: null } }, - ]); + it('includes createLabelsIfMissing variables per-item in the batch query', async () => { + mocks.setResponse({ + item_0: { id: '101', name: 'Item A', url: null }, + item_1: { id: '102', name: 'Item B', url: null }, + }); const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); @@ -174,11 +194,11 @@ describe('ChangeItemColumnValuesBatchTool', () => { ], }); - const calls = mocks.getMockRequest().mock.calls; - expect(calls[0][1]).toEqual( - expect.objectContaining({ itemId: '101', createLabelsIfMissing: true }), - ); - expect(calls[1][1]).not.toHaveProperty('createLabelsIfMissing'); + const [query, variables] = mocks.getMockRequest().mock.calls[0]; + expect(query).toContain('create_labels_if_missing: $createLabelsIfMissing_0'); + expect(query).not.toContain('$createLabelsIfMissing_1'); + expect(variables.createLabelsIfMissing_0).toBe(true); + expect(variables).not.toHaveProperty('createLabelsIfMissing_1'); }); }); }); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts index 61285874..83244110 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts @@ -1,9 +1,4 @@ import { z } from 'zod'; -import { - ChangeItemColumnValuesMutation, - ChangeItemColumnValuesMutationVariables, -} from 'src/monday-graphql/generated/graphql/graphql'; -import { changeItemColumnValues } from '../../../monday-graphql/queries.graphql'; import { ToolInputType, ToolOutputType, ToolType } from '../../tool'; import { BaseMondayApiTool, createMondayApiAnnotations } from './base-monday-api-tool'; @@ -39,6 +34,33 @@ export type ChangeItemColumnValuesBatchToolInput = | typeof changeItemColumnValuesBatchToolSchema | typeof changeItemColumnValuesBatchInBoardToolSchema; +type ItemResult = { id: string; name: string; url: string | null } | null; + +function buildBatchMutation(boardId: string, items: z.infer[]) { + const varDefs: string[] = ['$boardId: ID!']; + const mutations: string[] = []; + const variables: Record = { boardId }; + + items.forEach((item, i) => { + varDefs.push(`$itemId_${i}: ID!`, `$columnValues_${i}: JSON!`); + variables[`itemId_${i}`] = item.itemId.toString(); + variables[`columnValues_${i}`] = item.columnValues; + + const args = ['board_id: $boardId', `item_id: $itemId_${i}`, `column_values: $columnValues_${i}`]; + + if (item.createLabelsIfMissing !== undefined) { + varDefs.push(`$createLabelsIfMissing_${i}: Boolean`); + args.push(`create_labels_if_missing: $createLabelsIfMissing_${i}`); + variables[`createLabelsIfMissing_${i}`] = item.createLabelsIfMissing; + } + + mutations.push(`item_${i}: change_multiple_column_values(${args.join(', ')}) { id name url }`); + }); + + const query = `mutation(${varDefs.join(', ')}) {\n${mutations.join('\n')}\n}`; + return { query, variables }; +} + export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool { name = 'change_item_column_values_batch'; type = ToolType.WRITE; @@ -52,7 +74,7 @@ export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool).boardId; - const results = await Promise.allSettled( - input.items.map(async (item) => { - const variables: ChangeItemColumnValuesMutationVariables = { - boardId: boardId.toString(), - itemId: item.itemId.toString(), - columnValues: item.columnValues, - ...(item.createLabelsIfMissing !== undefined && { - createLabelsIfMissing: item.createLabelsIfMissing, - }), - }; + const { query, variables } = buildBatchMutation(boardId.toString(), input.items); + + let data: Record = {}; + let errors: Array<{ message: string; path?: string[] }> = []; - const res = await this.mondayApi.request(changeItemColumnValues, variables); + try { + data = await this.mondayApi.request>(query, variables); + } catch (error: unknown) { + const partialData = (error as any)?.response?.data; + if (partialData) { + data = partialData; + errors = (error as any).response?.errors ?? []; + } else { return { - itemId: item.itemId, - id: res.change_multiple_column_values?.id, - name: res.change_multiple_column_values?.name, - url: res.change_multiple_column_values?.url, + content: { + message: `0 of ${input.items.length} items updated successfully, ${input.items.length} failed`, + successful: [], + failed: input.items.map((item) => ({ + itemId: item.itemId, + error: this.extractErrorMessage(error), + })), + }, }; - }), - ); + } + } + + const successful: Array<{ itemId: number; id: string; name: string; url: string | null }> = []; + const failed: Array<{ itemId: number; error: string }> = []; - const successful = results - .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') - .map((r) => r.value); - - const failed = input.items - .map((item, index) => ({ item, result: results[index] })) - .filter((entry): entry is { item: typeof entry.item; result: PromiseRejectedResult } => - entry.result.status === 'rejected', - ) - .map((entry) => ({ - itemId: entry.item.itemId, - error: this.extractErrorMessage(entry.result.reason), - })); + input.items.forEach((item, i) => { + const alias = `item_${i}`; + const result = data[alias]; + if (result) { + successful.push({ itemId: item.itemId, id: result.id, name: result.name, url: result.url }); + } else { + const itemErrors = errors + .filter((e) => e.path?.[0] === alias) + .map((e) => e.message) + .join(', '); + failed.push({ itemId: item.itemId, error: itemErrors || 'Unknown error' }); + } + }); return { content: { From 813cd4b007a6850279808a9e545fd64cc2a42692 Mon Sep 17 00:00:00 2001 From: shifrinnitzan Date: Thu, 14 May 2026 15:22:45 +0300 Subject: [PATCH 6/6] fix(agent-toolkit): align batch tool response shape with singular tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-item success entries now use item_id, item_name, item_url and include a "Item {id} successfully updated" message — matching the singular change_item_column_values tool response contract. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ange-item-column-values-batch-tool.test.ts | 26 +++++++++++++++++++ .../change-item-column-values-batch-tool.ts | 16 ++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts index 148d8e99..707318c1 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.test.ts @@ -34,6 +34,28 @@ describe('ChangeItemColumnValuesBatchTool', () => { expect(mocks.getMockRequest()).toHaveBeenCalledTimes(1); }); + it('returns per-item fields matching the singular tool response shape', async () => { + mocks.setResponse({ + item_0: { id: '101', name: 'Item A', url: 'https://monday.com/101' }, + }); + + const tool = new ChangeItemColumnValuesBatchTool(mocks.mockApiClient, { boardId: 456 }); + + const result = await tool.execute({ + items: [{ itemId: 101, columnValues: '{"status":{"label":"Done"}}' }], + }); + + const content = result.content as Record; + const item = content.successful[0]; + expect(item).toEqual({ + itemId: 101, + message: 'Item 101 successfully updated', + item_id: '101', + item_name: 'Item A', + item_url: 'https://monday.com/101', + }); + }); + it('builds query with correct aliased mutation structure', async () => { mocks.setResponse({ item_0: { id: '101', name: 'Item A', url: null }, @@ -81,6 +103,8 @@ describe('ChangeItemColumnValuesBatchTool', () => { const content = result.content as Record; expect(content.successful).toHaveLength(2); + expect(content.successful[0].item_id).toBe('101'); + expect(content.successful[0].message).toBe('Item 101 successfully updated'); expect(content.failed).toHaveLength(1); expect(content.failed[0].error).toContain('unable to assign person'); expect(content.message).toContain('2 of 3'); @@ -102,6 +126,8 @@ describe('ChangeItemColumnValuesBatchTool', () => { const content = result.content as Record; expect(content.successful).toHaveLength(0); expect(content.failed).toHaveLength(2); + expect(content.failed[0].itemId).toBe(101); + expect(content.failed[1].itemId).toBe(102); expect(content.message).toContain('0 of 2'); }); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts index 83244110..632992db 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-batch-tool.ts @@ -121,14 +121,26 @@ export class ChangeItemColumnValuesBatchTool extends BaseMondayApiTool = []; + const successful: Array<{ + itemId: number; + message: string; + item_id: string; + item_name: string; + item_url: string | null; + }> = []; const failed: Array<{ itemId: number; error: string }> = []; input.items.forEach((item, i) => { const alias = `item_${i}`; const result = data[alias]; if (result) { - successful.push({ itemId: item.itemId, id: result.id, name: result.name, url: result.url }); + successful.push({ + itemId: item.itemId, + message: `Item ${result.id} successfully updated`, + item_id: result.id, + item_name: result.name, + item_url: result.url, + }); } else { const itemErrors = errors .filter((e) => e.path?.[0] === alias)