diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.graphql.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.graphql.ts new file mode 100644 index 000000000..56b6e8566 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/bulk-items-tools/bulk-items.graphql.ts @@ -0,0 +1,32 @@ +import { gql } from 'graphql-request'; + +export const ingestItemsMutation = gql` + mutation IngestItems($boardId: ID!, $groupId: ID!, $onMatch: OnMatchInput) { + ingest_items(board_id: $boardId, group_id: $groupId, on_match: $onMatch) { + job_id + upload_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/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/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.` ); } 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..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,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, UpdateBulkItemsTool } from './bulk-items-tools'; export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ DeleteItemTool, @@ -150,6 +151,9 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ // Workflows (subgraph still on dev API version) ListWorkflowsTool, ManageWorkflowsTool, + // Bulk operations + CreateBulkItemsTool, + UpdateBulkItemsTool, ]; export * from './all-monday-api-tool'; @@ -224,5 +228,7 @@ export * from './agents-tools'; export * from './workflows-tools'; // Dashboard Tools export * from './dashboard-tools'; +// Bulk operations +export * from './bulk-items-tools'; // Monday Dev Tools export * from '../monday-dev-tools';