diff --git a/packages/agent-toolkit/package.json b/packages/agent-toolkit/package.json index 94ff379b..06bede2e 100644 --- a/packages/agent-toolkit/package.json +++ b/packages/agent-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@mondaydotcomorg/agent-toolkit", -"version": "5.16.0", +"version": "5.17.0", "description": "monday.com agent toolkit", "exports": { "./mcp": { 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 fa923969..f2048851 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 @@ -74,6 +74,7 @@ import { ListAutomationsTool } from './workflows-tools/list-workflows/list-workf import { ManageWorkflowsTool } from './workflows-tools/manage-workflows/manage-workflows-tool'; import { CreateAutomationTool } from './workflows-tools/create-automation/create-automation-tool'; import { CreateWorkflowBuilderTool } from './workflow-builder-tools/create-workflow/create-workflow-tool'; +import { UpdateWorkflowTool } from './workflow-builder-tools/update-workflow/update-workflow-tool'; export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ DeleteItemTool, @@ -156,6 +157,8 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [ CreateAutomationTool as unknown as BaseMondayApiToolConstructor, // Workflow Builder Tools CreateWorkflowBuilderTool, + // Cast: ctor signature (api, apiToken, context?) doesn't match BaseMondayApiToolConstructor. + UpdateWorkflowTool as unknown as BaseMondayApiToolConstructor, ]; export * from './all-monday-api-tool'; @@ -230,6 +233,7 @@ export * from './agents-tools'; export * from './workflows-tools'; // Workflow Builder Tools export * from './workflow-builder-tools/create-workflow/create-workflow-tool'; +export * from './workflow-builder-tools/update-workflow/update-workflow-tool'; // Dashboard Tools export * from './dashboard-tools'; // Monday Dev Tools diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/constants.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/constants.ts new file mode 100644 index 00000000..9e3a7110 --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/constants.ts @@ -0,0 +1 @@ +export const WORKFLOW_BUILDER_AGENT_URL = 'https://api.monday.com/platform-ai-gateway/agents/workflow-builder'; diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.test.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.test.ts new file mode 100644 index 00000000..4ba8632e --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.test.ts @@ -0,0 +1,150 @@ +import { MondayAgentToolkit } from 'src/mcp/toolkit'; +import { callToolByNameRawAsync, createMockApiClient, parseToolResult } from '../../test-utils/mock-api-client'; +import { WORKFLOW_BUILDER_AGENT_URL } from '../constants'; + +function mockFetchResponse({ + ok = true, + status = 200, + body = {}, +}: { + ok?: boolean; + status?: number; + body?: unknown; +} = {}): Response { + return { + ok, + status, + json: async () => body, + text: async () => (typeof body === 'string' ? body : JSON.stringify(body)), + } as unknown as Response; +} + +describe('UpdateWorkflowTool', () => { + let mocks: ReturnType; + let fetchSpy: jest.SpyInstance; + + beforeEach(() => { + mocks = createMockApiClient(); + jest.spyOn(MondayAgentToolkit.prototype as any, 'createApiClient').mockReturnValue(mocks.mockApiClient); + fetchSpy = jest.spyOn(global, 'fetch'); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + it('posts to the workflow-builder agent URL with correct body and Authorization header', async () => { + fetchSpy.mockResolvedValue( + mockFetchResponse({ + body: { workflowObjectId: 5002722216, workflowDraftId: 43023, result: 'Added an email step' }, + }), + ); + + await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: 'add a step that sends an email when an item is created', + }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, init] = fetchSpy.mock.calls[0]; + expect(url).toBe(WORKFLOW_BUILDER_AGENT_URL); + expect(init.method).toBe('POST'); + expect(init.headers).toMatchObject({ + Authorization: 'test-token', + 'Content-Type': 'application/json', + }); + expect(JSON.parse(init.body)).toEqual({ + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: 'add a step that sends an email when an item is created', + }); + }); + + it('passes through the agent result on success', async () => { + fetchSpy.mockResolvedValue( + mockFetchResponse({ + body: { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + result: 'Added an email notification step after the trigger', + }, + }), + ); + + const result = await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: 'add an email step', + }); + const parsed = parseToolResult(result); + + expect(parsed.workflowObjectId).toBe(5002722216); + expect(parsed.workflowDraftId).toBe(43023); + expect(parsed.result).toBe('Added an email notification step after the trigger'); + }); + + it('rejects missing workflowObjectId before making any HTTP call', async () => { + const result = await callToolByNameRawAsync('update_workflow', { + workflowDraftId: 43023, + prompt: 'add a step', + }); + + expect(result.content[0].text).toContain('workflowObjectId'); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('rejects missing workflowDraftId before making any HTTP call', async () => { + const result = await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + prompt: 'add a step', + }); + + expect(result.content[0].text).toContain('workflowDraftId'); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('rejects empty prompt before making any HTTP call', async () => { + const result = await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: ' ', + }); + + expect(result.content[0].text).toContain('prompt must be a non-empty string'); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('surfaces the error envelope on 4xx', async () => { + fetchSpy.mockResolvedValue( + mockFetchResponse({ + ok: false, + status: 400, + body: { error: 'Prompt exceeds maximum length of 2000 characters', code: 'PROMPT_TOO_LONG' }, + }), + ); + + const result = await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: 'add a step', + }); + + expect(result.content[0].text).toContain('Failed to update workflow'); + expect(result.content[0].text).toContain('HTTP 400'); + expect(result.content[0].text).toContain('PROMPT_TOO_LONG'); + }); + + it('wraps network errors with operation context', async () => { + fetchSpy.mockRejectedValue(new Error('network failure')); + + const result = await callToolByNameRawAsync('update_workflow', { + workflowObjectId: 5002722216, + workflowDraftId: 43023, + prompt: 'add a step', + }); + + expect(result.content[0].text).toContain('Failed to update workflow'); + expect(result.content[0].text).toContain('network failure'); + }); +}); diff --git a/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.ts b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.ts new file mode 100644 index 00000000..a9f5e79c --- /dev/null +++ b/packages/agent-toolkit/src/core/tools/platform-api-tools/workflow-builder-tools/update-workflow/update-workflow-tool.ts @@ -0,0 +1,102 @@ +import { ApiClient } from '@mondaydotcomorg/api'; +import { z } from 'zod'; +import { ToolInputType, ToolOutputType, ToolType } from '../../../../tool'; +import { BaseMondayApiTool, MondayApiToolContext, createMondayApiAnnotations } from '../../base-monday-api-tool'; +import { rethrowWithContext } from '../../../../../utils'; +import { WORKFLOW_BUILDER_AGENT_URL } from '../constants'; + +const REQUEST_TIMEOUT_MS = 180_000; + +export const updateWorkflowToolSchema = { + workflowObjectId: z + .number() + .describe( + 'The workflow object ID returned by create_workflow. Identifies the workflow across all its drafts and published versions. Does not change across publishes.', + ), + workflowDraftId: z + .number() + .describe( + 'The draft version ID to update. Use the workflowDraftId from the previous create_workflow or update_workflow response — the agent may return a new draft ID, so always read it from the latest response rather than reusing an earlier value.', + ), + prompt: z + .string() + .trim() + .min(1, 'prompt must be a non-empty string') + .max(2000, 'prompt must not exceed 2000 characters') + .describe( + 'Natural-language description of the changes to make. Describe what steps to add, remove, or modify in plain English (e.g. "Add a trigger that fires when an item is created on the Marketing board"). The agent interprets this and applies the right structural changes. Maximum 2000 characters.', + ), +}; + +export class UpdateWorkflowTool extends BaseMondayApiTool { + name = 'update_workflow'; + type = ToolType.WRITE; + annotations = createMondayApiAnnotations({ + title: 'Update Workflow', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + }); + + constructor( + api: ApiClient, + private readonly apiToken: string, + context?: MondayApiToolContext, + ) { + super(api, context); + } + + getDescription(): string { + return `Updates an existing workflow draft using an AI agent. + +The agent interprets the prompt and applies structural changes to the workflow — creating, updating, or deleting steps. Pass clear, descriptive instructions and the agent will decide which operations to perform, then return a summary of what it did. + +Use this after create_workflow to build out the workflow step by step. You can call it multiple times on the same draft to iteratively refine the workflow. + +Parameters: +- workflowObjectId and workflowDraftId: both returned by create_workflow — they identify which draft to update. +- prompt: describe what you want to change in plain English (e.g. "Add a trigger that fires when an item is created on the Marketing board"). Maximum 2000 characters. + +Returns: +- workflowObjectId: the workflow object ID (unchanged) +- workflowDraftId: the draft version ID (unchanged) +- result: agent response describing the changes made + +Note: the workflow runs only after it is published to live version. +`; + } + + getInputSchema() { + return updateWorkflowToolSchema; + } + + protected async executeInternal( + input: ToolInputType, + ): Promise> { + try { + const response = await fetch(WORKFLOW_BUILDER_AGENT_URL, { + method: 'POST', + headers: { + Authorization: this.apiToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workflowObjectId: input.workflowObjectId, + workflowDraftId: input.workflowDraftId, + prompt: input.prompt, + }), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new Error(`workflow-builder responded with HTTP ${response.status}${body ? `: ${body}` : ''}`); + } + + const body = (await response.json()) as Record; + return { content: body }; + } catch (error) { + rethrowWithContext(error, 'update workflow'); + } + } +}