From 78645882c366dd8235a269c0abc4418b958ca3a2 Mon Sep 17 00:00:00 2001 From: rami-monday Date: Sun, 24 May 2026 11:07:37 +0300 Subject: [PATCH 1/4] feat: add create_bulk_items tool for bulk item ingestion Adds a new MCP tool that bulk creates or updates up to 10,000 items on a board using the ingest_items mutation (API-Version: 2026-07). Handles CSV generation, S3 upload, and job status polling. Co-Authored-By: Claude Opus 4.6 --- .../create-bulk-items-tool.ts | 164 ++++++++++++++++++ .../create-bulk-items.graphql.ts | 33 ++++ .../core/tools/platform-api-tools/index.ts | 5 + 3 files changed, 202 insertions(+) create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts new file mode 100644 index 000000000..2f3dd81fc --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts @@ -0,0 +1,164 @@ +import { z } from 'zod'; +import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; +import { BaseMondayApiTool, createMondayApiAnnotations } from '../base-monday-api-tool'; +import { ingestItemsMutation, fetchJobStatusQuery } from './create-bulk-items.graphql'; + +const onMatchSchema = z + .object({ + match_column_id: z.string().describe('The column ID to match existing items against for upsert'), + behaviour: z.enum(['UPSERT', 'SKIP']).describe('UPSERT to update matched items, SKIP to ignore them'), + }) + .optional() + .describe('If provided, enables upsert mode. When omitted, all rows are created as new items.'); + +export const createBulkItemsSchema = { + board_id: z.number().describe('The ID of the target board to create items on'), + items: z + .array( + z.record(z.string(), z.string()).describe('A row object where keys are column IDs and values are strings'), + ) + .min(1) + .max(10000) + .describe( + 'Array of item objects (up to 10,000). Each object represents a row — keys are column IDs (e.g. name, status, date4), values are strings. The "name" key is required in every row.', + ), + on_match: onMatchSchema, +}; + +interface IngestItemsResponse { + ingest_items: { + job_id: string; + status: string; + s3_url: string; + }; +} + +interface JobStatusResponse { + fetch_job_status: { + status: string; + progress_percentage: number; + fully_imported: boolean; + counts: { + submitted: number; + invalid: number; + skipped: number; + created: number; + updated: number; + failed: number; + }; + failure_reason: string | null; + failure_message: string | null; + }; +} + +const POLL_INTERVAL_MS = 2000; +const POLL_TIMEOUT_MS = 60000; + +function buildCsv(items: Record[]): string { + const columnIds = [...new Set(items.flatMap((item) => Object.keys(item)))]; + const header = columnIds.join(','); + const rows = items.map((item) => + columnIds.map((col) => escapeCsvField(item[col] ?? '')).join(','), + ); + return [header, ...rows].join('\n'); +} + +function escapeCsvField(value: string): string { + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +async function pollJobStatus( + request: (query: string, variables?: Record, options?: any) => Promise, + jobId: string, +): Promise { + const start = Date.now(); + while (Date.now() - start < POLL_TIMEOUT_MS) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + const res = await request(fetchJobStatusQuery, { jobId }, { versionOverride: '2026-07' }); + const status = res.fetch_job_status; + if (status.fully_imported) { + return status; + } + if (status.failure_reason) { + return status; + } + } + throw new Error('Polling timed out after 60 seconds'); +} + +export class CreateBulkItemsTool extends BaseMondayApiTool { + name = 'create_bulk_items'; + type = ToolType.WRITE; + annotations = createMondayApiAnnotations({ + title: 'Create Bulk Items', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + }); + + getDescription(): string { + return ( + 'Bulk create or update up to 10,000 items on a monday.com board using the ingest_items mutation. ' + + 'Provide an array of row objects where keys are column IDs (e.g. name, status, date4) and values are strings. ' + + 'Optionally provide on_match to enable upsert mode — matched items are updated or skipped instead of duplicated. ' + + 'Returns the job ID and final counts (created, updated, skipped, invalid, failed).' + ); + } + + getInputSchema(): typeof createBulkItemsSchema { + return createBulkItemsSchema; + } + + protected async executeInternal(input: ToolInputType): Promise> { + const { board_id, items, on_match } = input; + + const variables: Record = { boardId: String(board_id) }; + if (on_match) { + variables.onMatch = { match_column_id: on_match.match_column_id, behaviour: on_match.behaviour }; + } + + const ingestRes = await this.mondayApi.request( + ingestItemsMutation, + variables, + { versionOverride: '2026-07' }, + ); + + const { job_id, s3_url } = ingestRes.ingest_items; + + const csv = buildCsv(items); + const uploadResponse = await fetch(s3_url, { + method: 'PUT', + headers: { 'Content-Type': 'text/csv' }, + body: csv, + }); + + if (!uploadResponse.ok) { + throw new Error(`S3 upload failed: HTTP ${uploadResponse.status}`); + } + + const jobStatus = await pollJobStatus( + this.mondayApi.request.bind(this.mondayApi), + job_id, + ); + + if (jobStatus.failure_reason) { + return { + content: { + job_id, + error: jobStatus.failure_reason, + message: jobStatus.failure_message, + }, + }; + } + + return { + content: { + job_id, + counts: jobStatus.counts, + }, + }; + } +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts new file mode 100644 index 000000000..15ab32b40 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts @@ -0,0 +1,33 @@ +import { gql } from 'graphql-request'; + +export const ingestItemsMutation = gql` + mutation IngestItems($boardId: ID!, $onMatch: OnMatchInput) { + ingest_items(board_id: $boardId, on_match: $onMatch) { + job_id + status + s3_url + } + } +`; + +export const fetchJobStatusQuery = gql` + query PollJob($jobId: ID!) { + fetch_job_status(job_id: $jobId) { + ... on ItemsJobStatus { + status + progress_percentage + fully_imported + counts { + submitted + invalid + skipped + created + updated + failed + } + failure_reason + failure_message + } + } + } +`; 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 3ac3142d5..bfa9fd8d9 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 @@ -72,6 +72,7 @@ import { CreateAgentTool } from './agents-tools/create-agent/create-agent-tool'; import { DeleteAgentTool } from './agents-tools/delete-agent/delete-agent-tool'; import { ListWorkflowsTool } from './workflows-tools/list-workflows/list-workflows-tool'; import { ManageWorkflowsTool } from './workflows-tools/manage-workflows/manage-workflows-tool'; +import { CreateBulkItemsTool } from './create-bulk-items-tool/create-bulk-items-tool'; export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ DeleteItemTool, @@ -150,6 +151,8 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ // Workflows (subgraph still on dev API version) ListWorkflowsTool, ManageWorkflowsTool, + // Bulk operations + CreateBulkItemsTool, ]; export * from './all-monday-api-tool'; @@ -224,5 +227,7 @@ export * from './agents-tools'; export * from './workflows-tools'; // Dashboard Tools export * from './dashboard-tools'; +// Bulk operations +export * from './create-bulk-items-tool/create-bulk-items-tool'; // Monday Dev Tools export * from '../monday-dev-tools'; From 47d712f29197db4417eaf54d7edcb152775a9ca4 Mon Sep 17 00:00:00 2001 From: rami-monday Date: Sun, 24 May 2026 15:50:12 +0300 Subject: [PATCH 2/4] fix: correct ingest_items mutation fields and add group_id - Return type is UploadJobInit with fields: job_id, upload_url (not s3_url/status) - Add group_id parameter (required by API, defaults to "topics") Co-Authored-By: Claude Opus 4.6 --- .../create-bulk-items-tool/create-bulk-items-tool.ts | 12 ++++++------ .../create-bulk-items.graphql.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts index 2f3dd81fc..589cbd092 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts @@ -13,6 +13,7 @@ const onMatchSchema = z export const createBulkItemsSchema = { board_id: z.number().describe('The ID of the target board to create items on'), + group_id: z.string().optional().default('topics').describe('The ID of the group to create items in. Defaults to "topics".'), items: z .array( z.record(z.string(), z.string()).describe('A row object where keys are column IDs and values are strings'), @@ -28,8 +29,7 @@ export const createBulkItemsSchema = { interface IngestItemsResponse { ingest_items: { job_id: string; - status: string; - s3_url: string; + upload_url: string; }; } @@ -113,9 +113,9 @@ export class CreateBulkItemsTool extends BaseMondayApiTool): Promise> { - const { board_id, items, on_match } = input; + const { board_id, group_id, items, on_match } = input; - const variables: Record = { boardId: String(board_id) }; + const variables: Record = { boardId: String(board_id), groupId: group_id }; if (on_match) { variables.onMatch = { match_column_id: on_match.match_column_id, behaviour: on_match.behaviour }; } @@ -126,10 +126,10 @@ export class CreateBulkItemsTool extends BaseMondayApiTool Date: Mon, 25 May 2026 11:27:47 +0300 Subject: [PATCH 3/4] feat: split create_bulk_items into create and update tools Separates the single bulk tool into two distinct tools so LLMs can better recognize update capabilities: - create_bulk_items: creates new items (group_id optional, defaults to "topics") - update_bulk_items: updates existing items via on_match (group_id required) Shared logic extracted to bulk-items.utils.ts. Co-Authored-By: Claude Opus 4.6 --- .../bulk-items.graphql.ts} | 0 .../bulk-items-tools/bulk-items.utils.ts | 127 ++++++++++++++ .../create-bulk-items-tool.ts | 43 +++++ .../bulk-items-tools/index.ts | 2 + .../update-bulk-items-tool.ts | 52 ++++++ .../create-bulk-items-tool.ts | 164 ------------------ .../core/tools/platform-api-tools/index.ts | 5 +- 7 files changed, 227 insertions(+), 166 deletions(-) rename packages/agent-toolkit/src/core/tools/platform-api-tools/{create-bulk-items-tool/create-bulk-items.graphql.ts => bulk-items-tools/bulk-items.graphql.ts} (100%) create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.utils.ts create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/create-bulk-items-tool.ts create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/index.ts create mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/update-bulk-items-tool.ts delete mode 100644 packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.graphql.ts similarity index 100% rename from packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items.graphql.ts rename to packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.graphql.ts diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.utils.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.utils.ts new file mode 100644 index 000000000..e3fea0872 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.utils.ts @@ -0,0 +1,127 @@ +import { z } from 'zod'; +import { ingestItemsMutation, fetchJobStatusQuery } from './bulk-items.graphql'; + +export interface IngestItemsResponse { + ingest_items: { + job_id: string; + upload_url: string; + }; +} + +export interface JobStatusResponse { + fetch_job_status: { + status: string; + progress_percentage: number; + fully_imported: boolean; + counts: { + submitted: number; + invalid: number; + skipped: number; + created: number; + updated: number; + failed: number; + }; + failure_reason: string | null; + failure_message: string | null; + }; +} + +const POLL_INTERVAL_MS = 2000; +const POLL_TIMEOUT_MS = 60000; + +export const itemsSchema = z + .array( + z.record(z.string(), z.string()).describe('A row object where keys are column IDs and values are strings'), + ) + .min(1) + .max(10000) + .describe( + 'Array of item objects (up to 10,000). Each object represents a row — keys are column IDs (e.g. name, status, date4), values are strings. The "name" key is required in every row.', + ); + +export const boardIdSchema = z.number().describe('The ID of the target board'); + +export const groupIdSchema = z.string().describe('The ID of the group to target. The operation only affects items in this specific group. To update items across multiple groups, call this tool once per group.'); + +function escapeCsvField(value: string): string { + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +export function buildCsv(items: Record[]): string { + const columnIds = [...new Set(items.flatMap((item) => Object.keys(item)))]; + const header = columnIds.join(','); + const rows = items.map((item) => + columnIds.map((col) => escapeCsvField(item[col] ?? '')).join(','), + ); + return [header, ...rows].join('\n'); +} + +export async function pollJobStatus( + request: (query: string, variables?: Record, options?: any) => Promise, + jobId: string, +): Promise { + const start = Date.now(); + while (Date.now() - start < POLL_TIMEOUT_MS) { + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + const res = await request(fetchJobStatusQuery, { jobId }, { versionOverride: '2026-07' }); + const status = res.fetch_job_status; + if (status.fully_imported) { + return status; + } + if (status.failure_reason) { + return status; + } + } + throw new Error('Polling timed out after 60 seconds'); +} + +export async function executeIngestItems( + mondayApi: { request: (query: string, variables?: Record, options?: any) => Promise }, + params: { boardId: string; groupId: string; items: Record[]; onMatch?: { match_column_id: string; behaviour: string } }, +) { + const variables: Record = { boardId: params.boardId, groupId: params.groupId }; + if (params.onMatch) { + variables.onMatch = params.onMatch; + } + + const ingestRes = await mondayApi.request( + ingestItemsMutation, + variables, + { versionOverride: '2026-07' }, + ); + + const { job_id, upload_url } = ingestRes.ingest_items; + + const csv = buildCsv(params.items); + const uploadResponse = await fetch(upload_url, { + method: 'PUT', + headers: { 'Content-Type': 'text/csv' }, + body: csv, + }); + + if (!uploadResponse.ok) { + throw new Error(`S3 upload failed: HTTP ${uploadResponse.status}`); + } + + const jobStatus = await pollJobStatus(mondayApi.request.bind(mondayApi), job_id); + + if (jobStatus.failure_reason) { + return { + content: { + job_id, + error: jobStatus.failure_reason, + message: jobStatus.failure_message, + }, + }; + } + + return { + content: { + job_id, + counts: jobStatus.counts, + }, + }; +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/create-bulk-items-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/create-bulk-items-tool.ts new file mode 100644 index 000000000..f59234588 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/create-bulk-items-tool.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; +import { BaseMondayApiTool, createMondayApiAnnotations } from '../base-monday-api-tool'; +import { boardIdSchema, groupIdSchema, itemsSchema, executeIngestItems } from './bulk-items.utils'; + +export const createBulkItemsSchema = { + board_id: boardIdSchema, + group_id: groupIdSchema.optional().default('topics'), + items: itemsSchema, +}; + +export class CreateBulkItemsTool extends BaseMondayApiTool { + name = 'create_bulk_items'; + type = ToolType.WRITE; + annotations = createMondayApiAnnotations({ + title: 'Create Bulk Items', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + }); + + getDescription(): string { + return ( + 'Bulk create up to 10,000 new items on a monday.com board. ' + + 'Provide an array of row objects where keys are column IDs (e.g. name, status, date4) and values are strings. ' + + 'Every row is created as a new item in the specified group. The default group_id "topics" is the first group on the board. ' + + 'Returns the job ID and final counts (created, failed, invalid).' + ); + } + + getInputSchema(): typeof createBulkItemsSchema { + return createBulkItemsSchema; + } + + protected async executeInternal(input: ToolInputType): Promise> { + const { board_id, group_id, items } = input; + return executeIngestItems(this.mondayApi, { + boardId: String(board_id), + groupId: group_id, + items, + }); + } +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/index.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/index.ts new file mode 100644 index 000000000..5761b9393 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/index.ts @@ -0,0 +1,2 @@ +export { CreateBulkItemsTool } from './create-bulk-items-tool'; +export { UpdateBulkItemsTool } from './update-bulk-items-tool'; diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/update-bulk-items-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/update-bulk-items-tool.ts new file mode 100644 index 000000000..cbf592f36 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/update-bulk-items-tool.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; +import { BaseMondayApiTool, createMondayApiAnnotations } from '../base-monday-api-tool'; +import { boardIdSchema, groupIdSchema, itemsSchema, executeIngestItems } from './bulk-items.utils'; + +const onMatchSchema = z.object({ + match_column_id: z.string().describe('The column ID to match existing items against (e.g. "name" or "email")'), + behaviour: z.enum(['UPSERT', 'SKIP']).describe('UPSERT to update matched items with new values, SKIP to leave matched items unchanged').optional().default('UPSERT'), +}); + +export const updateBulkItemsSchema = { + board_id: boardIdSchema, + group_id: groupIdSchema, + items: itemsSchema, + on_match: onMatchSchema.describe('Controls how existing items are matched and updated'), +}; + +export class UpdateBulkItemsTool extends BaseMondayApiTool { + name = 'update_bulk_items'; + type = ToolType.WRITE; + annotations = createMondayApiAnnotations({ + title: 'Update Bulk Items', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + }); + + getDescription(): string { + return ( + 'Bulk update existing items on a monday.com board by matching against a column value. ' + + 'Provide an array of row objects and an on_match config specifying which column to match on and the behaviour (UPSERT or SKIP). ' + + 'Matched items are updated with new values; unmatched rows are created as new items. ' + + 'Important: the operation is scoped to a single group.' + + 'If the board has multiple groups, call this tool once per group to update all items. ' + + 'Returns the job ID and final counts (created, updated, skipped, failed, invalid).' + ); + } + + getInputSchema(): typeof updateBulkItemsSchema { + return updateBulkItemsSchema; + } + + protected async executeInternal(input: ToolInputType): Promise> { + const { board_id, group_id, items, on_match } = input; + return executeIngestItems(this.mondayApi, { + boardId: String(board_id), + groupId: group_id, + items, + onMatch: { match_column_id: on_match.match_column_id, behaviour: on_match.behaviour}, + }); + } +} diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts deleted file mode 100644 index 589cbd092..000000000 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/create-bulk-items-tool/create-bulk-items-tool.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { z } from 'zod'; -import { ToolInputType, ToolOutputType, ToolType } from '../../../tool'; -import { BaseMondayApiTool, createMondayApiAnnotations } from '../base-monday-api-tool'; -import { ingestItemsMutation, fetchJobStatusQuery } from './create-bulk-items.graphql'; - -const onMatchSchema = z - .object({ - match_column_id: z.string().describe('The column ID to match existing items against for upsert'), - behaviour: z.enum(['UPSERT', 'SKIP']).describe('UPSERT to update matched items, SKIP to ignore them'), - }) - .optional() - .describe('If provided, enables upsert mode. When omitted, all rows are created as new items.'); - -export const createBulkItemsSchema = { - board_id: z.number().describe('The ID of the target board to create items on'), - group_id: z.string().optional().default('topics').describe('The ID of the group to create items in. Defaults to "topics".'), - items: z - .array( - z.record(z.string(), z.string()).describe('A row object where keys are column IDs and values are strings'), - ) - .min(1) - .max(10000) - .describe( - 'Array of item objects (up to 10,000). Each object represents a row — keys are column IDs (e.g. name, status, date4), values are strings. The "name" key is required in every row.', - ), - on_match: onMatchSchema, -}; - -interface IngestItemsResponse { - ingest_items: { - job_id: string; - upload_url: string; - }; -} - -interface JobStatusResponse { - fetch_job_status: { - status: string; - progress_percentage: number; - fully_imported: boolean; - counts: { - submitted: number; - invalid: number; - skipped: number; - created: number; - updated: number; - failed: number; - }; - failure_reason: string | null; - failure_message: string | null; - }; -} - -const POLL_INTERVAL_MS = 2000; -const POLL_TIMEOUT_MS = 60000; - -function buildCsv(items: Record[]): string { - const columnIds = [...new Set(items.flatMap((item) => Object.keys(item)))]; - const header = columnIds.join(','); - const rows = items.map((item) => - columnIds.map((col) => escapeCsvField(item[col] ?? '')).join(','), - ); - return [header, ...rows].join('\n'); -} - -function escapeCsvField(value: string): string { - if (value.includes(',') || value.includes('"') || value.includes('\n')) { - return `"${value.replace(/"/g, '""')}"`; - } - return value; -} - -async function pollJobStatus( - request: (query: string, variables?: Record, options?: any) => Promise, - jobId: string, -): Promise { - const start = Date.now(); - while (Date.now() - start < POLL_TIMEOUT_MS) { - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); - const res = await request(fetchJobStatusQuery, { jobId }, { versionOverride: '2026-07' }); - const status = res.fetch_job_status; - if (status.fully_imported) { - return status; - } - if (status.failure_reason) { - return status; - } - } - throw new Error('Polling timed out after 60 seconds'); -} - -export class CreateBulkItemsTool extends BaseMondayApiTool { - name = 'create_bulk_items'; - type = ToolType.WRITE; - annotations = createMondayApiAnnotations({ - title: 'Create Bulk Items', - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - }); - - getDescription(): string { - return ( - 'Bulk create or update up to 10,000 items on a monday.com board using the ingest_items mutation. ' + - 'Provide an array of row objects where keys are column IDs (e.g. name, status, date4) and values are strings. ' + - 'Optionally provide on_match to enable upsert mode — matched items are updated or skipped instead of duplicated. ' + - 'Returns the job ID and final counts (created, updated, skipped, invalid, failed).' - ); - } - - getInputSchema(): typeof createBulkItemsSchema { - return createBulkItemsSchema; - } - - protected async executeInternal(input: ToolInputType): Promise> { - const { board_id, group_id, items, on_match } = input; - - const variables: Record = { boardId: String(board_id), groupId: group_id }; - if (on_match) { - variables.onMatch = { match_column_id: on_match.match_column_id, behaviour: on_match.behaviour }; - } - - const ingestRes = await this.mondayApi.request( - ingestItemsMutation, - variables, - { versionOverride: '2026-07' }, - ); - - const { job_id, upload_url } = ingestRes.ingest_items; - - const csv = buildCsv(items); - const uploadResponse = await fetch(upload_url, { - method: 'PUT', - headers: { 'Content-Type': 'text/csv' }, - body: csv, - }); - - if (!uploadResponse.ok) { - throw new Error(`S3 upload failed: HTTP ${uploadResponse.status}`); - } - - const jobStatus = await pollJobStatus( - this.mondayApi.request.bind(this.mondayApi), - job_id, - ); - - if (jobStatus.failure_reason) { - return { - content: { - job_id, - error: jobStatus.failure_reason, - message: jobStatus.failure_message, - }, - }; - } - - return { - content: { - job_id, - counts: jobStatus.counts, - }, - }; - } -} 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 bfa9fd8d9..954597a25 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 @@ -72,7 +72,7 @@ import { CreateAgentTool } from './agents-tools/create-agent/create-agent-tool'; import { DeleteAgentTool } from './agents-tools/delete-agent/delete-agent-tool'; import { ListWorkflowsTool } from './workflows-tools/list-workflows/list-workflows-tool'; import { ManageWorkflowsTool } from './workflows-tools/manage-workflows/manage-workflows-tool'; -import { CreateBulkItemsTool } from './create-bulk-items-tool/create-bulk-items-tool'; +import { CreateBulkItemsTool, UpdateBulkItemsTool } from './bulk-items-tools'; export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ DeleteItemTool, @@ -153,6 +153,7 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ ManageWorkflowsTool, // Bulk operations CreateBulkItemsTool, + UpdateBulkItemsTool, ]; export * from './all-monday-api-tool'; @@ -228,6 +229,6 @@ export * from './workflows-tools'; // Dashboard Tools export * from './dashboard-tools'; // Bulk operations -export * from './create-bulk-items-tool/create-bulk-items-tool'; +export * from './bulk-items-tools'; // Monday Dev Tools export * from '../monday-dev-tools'; From b4ddd11463104e7725bfbba57025dfb8b2362c72 Mon Sep 17 00:00:00 2001 From: rami-monday Date: Mon, 25 May 2026 17:47:05 +0300 Subject: [PATCH 4/4] feat: cross-reference bulk tools in create_item and change_item_column_values descriptions Co-Authored-By: Claude Opus 4.6 --- .../tools/platform-api-tools/change-item-column-values-tool.ts | 1 + .../platform-api-tools/create-item-tool/create-item-tool.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-tool.ts index 89cdc1a30..a8d0a7fd0 100644 --- a/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-tool.ts +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/change-item-column-values-tool.ts @@ -44,6 +44,7 @@ export class ChangeItemColumnValuesTool extends BaseMondayApiTool { getDescription(): string { return ( - 'Create a new item with provided values, create a subitem under a parent item, or duplicate an existing item and update it with new values. Use parentItemId when creating a subitem under an existing item. Use duplicateFromItemId when copying an existing item with modifications.' + + 'Create a new item with provided values, create a subitem under a parent item, or duplicate an existing item and update it with new values. For creating many items at once, use create_bulk_items instead. Use parentItemId when creating a subitem under an existing item. Use duplicateFromItemId when copying an existing item with modifications.' + `[REQUIRED PRECONDITION]: Before using this tool, if new columns were added to the board or if you are not familiar with the board's structure (column IDs, column types, status labels, etc.), first use get_board_info to understand the board metadata. This is essential for constructing proper column values and knowing which columns are available.` ); }