Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
e767eef
feat(agent-toolkit): add create_workflow tool
amitrossmonday May 24, 2026
82389dd
fix(agent-toolkit): null-check create_workflow response and narrow pr…
amitrossmonday May 25, 2026
9305892
chore(agent-toolkit): remove codegen exclusion for workflow-builder-t…
amitrossmonday May 25, 2026
28a6dce
chore(agent-toolkit): fetch dev schema and migrate create_workflow_bu…
amitrossmonday May 26, 2026
8b8b2a3
docs(agent-toolkit): improve create_workflow_builder tool description
amitrossmonday May 26, 2026
9cd6034
fix(agent-toolkit): remove unsafe chars from create_workflow_builder …
amitrossmonday May 26, 2026
9761e87
chore(agent-toolkit): bump version to 5.15.0 and simplify codegen.yml…
amitrossmonday May 26, 2026
c762083
fix(agent-toolkit): update draft terminology in create_workflow_build…
amitrossmonday May 26, 2026
fd09a9a
fix(agent-toolkit): use correct workflowObjectId terminology in descr…
amitrossmonday May 26, 2026
186b171
fix(agent-toolkit): replace 'activated' with 'published' in create_wo…
amitrossmonday May 26, 2026
454f0ba
fix(agent-toolkit): clarify draft terminology in create_workflow_buil…
amitrossmonday May 26, 2026
86ba9e9
refactor(agent-toolkit): remove workflow-builder-tools/index.ts, impo…
amitrossmonday May 26, 2026
543a620
refactor(agent-toolkit): move CreateWorkflowBuilderTool to its own se…
amitrossmonday May 26, 2026
fe86aba
refactor(agent-toolkit): restore original workflows section comment
amitrossmonday May 26, 2026
1f2032f
chore: trigger CI
amitrossmonday May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/agent-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mondaydotcomorg/agent-toolkit",
"version": "5.14.0",
"version": "5.15.0",
"description": "monday.com agent toolkit",
"exports": {
"./mcp": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { DeleteAgentTool } from './agents-tools/delete-agent/delete-agent-tool';
import { ListAutomationsTool } from './workflows-tools/list-workflows/list-workflows-tool';
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';

export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [
DeleteItemTool,
Expand Down Expand Up @@ -153,6 +154,8 @@ export const allGraphqlApiTools: BaseMondayApiToolConstructor[] = [
ManageWorkflowsTool,
// Cast: ctor signature (api, apiToken, context?) doesn't match BaseMondayApiToolConstructor.
CreateAutomationTool as unknown as BaseMondayApiToolConstructor,
// Workflow Builder Tools
CreateWorkflowBuilderTool,
];

export * from './all-monday-api-tool';
Expand Down Expand Up @@ -225,6 +228,8 @@ export * from './fetch-file-content-tool/fetch-file-content-tool';
export * from './agents-tools';
// Workflows
export * from './workflows-tools';
// Workflow Builder Tools
export * from './workflow-builder-tools/create-workflow/create-workflow-tool';
// Dashboard Tools
export * from './dashboard-tools';
// Monday Dev Tools
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { MondayAgentToolkit } from 'src/mcp/toolkit';
import { callToolByNameRawAsync, createMockApiClient, parseToolResult } from '../../test-utils/mock-api-client';
import { CreateWorkflowMutation } from '../../../../../monday-graphql/generated/graphql.dev/graphql';

describe('CreateWorkflowBuilderTool', () => {
let mocks: ReturnType<typeof createMockApiClient>;

beforeEach(() => {
mocks = createMockApiClient();
jest.spyOn(MondayAgentToolkit.prototype as any, 'createApiClient').mockReturnValue(mocks.mockApiClient);
});

const mockResponse: CreateWorkflowMutation = {
create_workflow: {
workflow_object_id: '999',
workflow_draft_id: '111',
},
};

it('should return workflowObjectId and workflowDraftId on success', async () => {
mocks.setResponseOnce(mockResponse);

const result = await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42' });
const parsed = parseToolResult(result);

expect(parsed.workflowObjectId).toBe('999');
expect(parsed.workflowDraftId).toBe('111');
expect(parsed.message).toContain('42');
});

it('should call the mutation with workspace_id and versionOverride dev', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.stringContaining('createWorkflow'),
expect.objectContaining({ workspace_id: '42' }),
expect.objectContaining({ versionOverride: 'dev' }),
);
});

it('should pass title when provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', title: 'My Workflow' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ title: 'My Workflow' }),
expect.anything(),
);
});

it('should pass privacy_kind when provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', privacyKind: 'PRIVATE' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ privacy_kind: 'PRIVATE' }),
expect.anything(),
);
});

it('should accept SHAREABLE as a valid privacyKind', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', privacyKind: 'SHAREABLE' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ privacy_kind: 'SHAREABLE' }),
expect.anything(),
);
});

it('should pass description when provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', description: 'My description' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ description: 'My description' }),
expect.anything(),
);
});

it('should pass folder_id when provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', folderId: '77' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ folder_id: '77' }),
expect.anything(),
);
});

it('should pass owner_ids when provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42', ownerIds: ['1', '2'] });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ owner_ids: ['1', '2'] }),
expect.anything(),
);
});

it('should not include optional fields when not provided', async () => {
mocks.setResponseOnce(mockResponse);

await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42' });

expect(mocks.getMockRequest()).toHaveBeenCalledWith(
expect.anything(),
expect.not.objectContaining({ title: expect.anything() }),
expect.anything(),
);
});

it('should reject invalid privacyKind values', async () => {
const result = await callToolByNameRawAsync('create_workflow_builder', {
workspaceId: '42',
privacyKind: 'private',
});

expect(result.content[0].text).toContain('Invalid enum value');
});

it('should reject whitespace-only workspaceId', async () => {
const result = await callToolByNameRawAsync('create_workflow_builder', { workspaceId: ' ' });

expect(result.content[0].text).toContain('workspaceId must be a non-empty string');
});

it('should propagate GraphQL errors with operation context', async () => {
mocks.setError('Not authorized');

const result = await callToolByNameRawAsync('create_workflow_builder', { workspaceId: '42' });

expect(result.content[0].text).toContain('Failed to create Workflow Builder workflow');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { z } from 'zod';
import { ToolInputType, ToolOutputType, ToolType } from '../../../../tool';
import { BaseMondayApiTool, createMondayApiAnnotations } from '../../base-monday-api-tool';
import { rethrowWithContext } from '../../../../../utils';
import {
CreateWorkflowMutation,
CreateWorkflowMutationVariables,
WorkflowBuilderPrivacyKind,
} from '../../../../../monday-graphql/generated/graphql.dev/graphql';
import { createWorkflowMutation } from './create-workflow.graphql.dev';

export const createWorkflowToolSchema = {
workspaceId: z
.string()
.trim()
.min(1, 'workspaceId must be a non-empty string')
.describe('The ID of the workspace to create the workflow in.'),
title: z.string().optional().describe('Workflow title. Defaults to "New Workflow" if not provided.'),
privacyKind: z
.enum(['PUBLIC', 'PRIVATE', 'SHAREABLE'])
.optional()
.describe('Workflow visibility: PUBLIC (default), PRIVATE, or SHAREABLE (accessible to guests outside the account).'),
description: z.string().optional().describe('Optional workflow description.'),
folderId: z.string().optional().describe('Optional folder ID to place the workflow in.'),
ownerIds: z.array(z.string()).optional().describe('Optional list of user IDs to set as workflow owners.'),
};

export class CreateWorkflowBuilderTool extends BaseMondayApiTool<typeof createWorkflowToolSchema> {
name = 'create_workflow_builder';
type = ToolType.WRITE;
annotations = createMondayApiAnnotations({
title: 'Create Workflow Builder',
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
});

getDescription(): string {
return `Creates a new empty Workflow Builder workflow in a monday.com workspace.

Use this when the user wants to build a new standalone workflow from scratch. Workflow Builder workflows are cross-board, workspace-level automations — distinct from board automations (use create_automation for those). You only need a workspaceId to get started — all other fields are optional.

Returns:
- workflowObjectId: the workflow object ID
- workflowDraftId: the current draft version ID — workflows start as drafts and must be published before they run

Terminology:
- Workflow Builder vs. board automations: Workflow Builder workflows are standalone objects scoped to a workspace. Board automations (create_automation) are per-board trigger/action rules. They are different products.
- Draft: the editable, inactive version of a workflow. Changes are made on the draft version until it is published as the live version.
- Privacy: PUBLIC — visible to all workspace members (default). PRIVATE — restricted access. SHAREABLE — accessible to guests outside the account.
`;
}

getInputSchema() {
return createWorkflowToolSchema;
}

protected async executeInternal(
input: ToolInputType<typeof createWorkflowToolSchema>,
): Promise<ToolOutputType<never>> {
try {
const variables: CreateWorkflowMutationVariables = {
workspace_id: input.workspaceId,
...(input.title !== undefined ? { title: input.title } : {}),
...(input.privacyKind !== undefined ? { privacy_kind: input.privacyKind as WorkflowBuilderPrivacyKind } : {}),
...(input.description !== undefined ? { description: input.description } : {}),
...(input.folderId !== undefined ? { folder_id: input.folderId } : {}),
...(input.ownerIds !== undefined ? { owner_ids: input.ownerIds } : {}),
};

const res = await this.mondayApi.request<CreateWorkflowMutation>(createWorkflowMutation, variables, {
versionOverride: 'dev',
});

if (!res.create_workflow) {
throw new Error('create_workflow returned null');
}
const { workflow_object_id, workflow_draft_id } = res.create_workflow;

if (!workflow_object_id || !workflow_draft_id) {
throw new Error('create_workflow returned missing identifiers');
}

return {
content: {
message: `Workflow Builder workflow created in workspace ${input.workspaceId}`,
workflowObjectId: workflow_object_id,
workflowDraftId: workflow_draft_id,
},
};
} catch (error) {
rethrowWithContext(error, 'create Workflow Builder workflow');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { gql } from 'graphql-request';

export const createWorkflowMutation = gql`
mutation createWorkflow(
$workspace_id: ID!
$title: String
$privacy_kind: WorkflowBuilderPrivacyKind
$description: String
$folder_id: ID
$owner_ids: [ID!]
) {
create_workflow(
workspace_id: $workspace_id
title: $title
privacy_kind: $privacy_kind
description: $description
folder_id: $folder_id
owner_ids: $owner_ids
) {
workflow_object_id
workflow_draft_id
}
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Documents = {
"\n query SearchDocsDev($query: String!, $limit: Int, $workspaceIds: [ID!]) {\n search {\n docs(query: $query, limit: $limit, workspace_ids: $workspaceIds) {\n results {\n id\n indexed_data {\n id\n name\n }\n }\n }\n }\n }\n": typeof types.SearchDocsDevDocument,
"\n mutation BatchUndo($boardId: ID!, $undoRecordId: ID!) {\n batch_undo(board_id: $boardId, undo_record_id: $undoRecordId) {\n success\n }\n }\n": typeof types.BatchUndoDocument,
"\n query getUserContext {\n me {\n id\n name\n title\n account {\n tier\n active_members_count\n is_during_trial\n products {\n kind\n tier\n }\n }\n }\n favorites {\n object {\n id\n type\n }\n }\n intelligence {\n relevant_boards(limit: 10) {\n id\n board {\n name\n }\n }\n relevant_people(limit: 10) {\n id\n user {\n name\n }\n }\n }\n }\n": typeof types.GetUserContextDocument,
"\n mutation createWorkflow(\n $workspace_id: ID!\n $title: String\n $privacy_kind: WorkflowBuilderPrivacyKind\n $description: String\n $folder_id: ID\n $owner_ids: [ID!]\n ) {\n create_workflow(\n workspace_id: $workspace_id\n title: $title\n privacy_kind: $privacy_kind\n description: $description\n folder_id: $folder_id\n owner_ids: $owner_ids\n ) {\n workflow_object_id\n workflow_draft_id\n }\n }\n": typeof types.CreateWorkflowDocument,
"\n mutation activateLiveWorkflow($id: ID!) {\n activate_live_workflow(id: $id) {\n is_success\n }\n }\n": typeof types.ActivateLiveWorkflowDocument,
"\n mutation deactivateLiveWorkflow($id: ID!) {\n deactivate_live_workflow(id: $id) {\n is_success\n }\n }\n": typeof types.DeactivateLiveWorkflowDocument,
"\n mutation deleteLiveWorkflow($id: ID!) {\n delete_live_workflow(id: $id) {\n is_success\n }\n }\n": typeof types.DeleteLiveWorkflowDocument,
Expand All @@ -46,6 +47,7 @@ const documents: Documents = {
"\n query SearchDocsDev($query: String!, $limit: Int, $workspaceIds: [ID!]) {\n search {\n docs(query: $query, limit: $limit, workspace_ids: $workspaceIds) {\n results {\n id\n indexed_data {\n id\n name\n }\n }\n }\n }\n }\n": types.SearchDocsDevDocument,
"\n mutation BatchUndo($boardId: ID!, $undoRecordId: ID!) {\n batch_undo(board_id: $boardId, undo_record_id: $undoRecordId) {\n success\n }\n }\n": types.BatchUndoDocument,
"\n query getUserContext {\n me {\n id\n name\n title\n account {\n tier\n active_members_count\n is_during_trial\n products {\n kind\n tier\n }\n }\n }\n favorites {\n object {\n id\n type\n }\n }\n intelligence {\n relevant_boards(limit: 10) {\n id\n board {\n name\n }\n }\n relevant_people(limit: 10) {\n id\n user {\n name\n }\n }\n }\n }\n": types.GetUserContextDocument,
"\n mutation createWorkflow(\n $workspace_id: ID!\n $title: String\n $privacy_kind: WorkflowBuilderPrivacyKind\n $description: String\n $folder_id: ID\n $owner_ids: [ID!]\n ) {\n create_workflow(\n workspace_id: $workspace_id\n title: $title\n privacy_kind: $privacy_kind\n description: $description\n folder_id: $folder_id\n owner_ids: $owner_ids\n ) {\n workflow_object_id\n workflow_draft_id\n }\n }\n": types.CreateWorkflowDocument,
"\n mutation activateLiveWorkflow($id: ID!) {\n activate_live_workflow(id: $id) {\n is_success\n }\n }\n": types.ActivateLiveWorkflowDocument,
"\n mutation deactivateLiveWorkflow($id: ID!) {\n deactivate_live_workflow(id: $id) {\n is_success\n }\n }\n": types.DeactivateLiveWorkflowDocument,
"\n mutation deleteLiveWorkflow($id: ID!) {\n delete_live_workflow(id: $id) {\n is_success\n }\n }\n": types.DeleteLiveWorkflowDocument,
Expand Down Expand Up @@ -118,6 +120,10 @@ export function graphql(source: "\n mutation BatchUndo($boardId: ID!, $undoReco
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query getUserContext {\n me {\n id\n name\n title\n account {\n tier\n active_members_count\n is_during_trial\n products {\n kind\n tier\n }\n }\n }\n favorites {\n object {\n id\n type\n }\n }\n intelligence {\n relevant_boards(limit: 10) {\n id\n board {\n name\n }\n }\n relevant_people(limit: 10) {\n id\n user {\n name\n }\n }\n }\n }\n"): (typeof documents)["\n query getUserContext {\n me {\n id\n name\n title\n account {\n tier\n active_members_count\n is_during_trial\n products {\n kind\n tier\n }\n }\n }\n favorites {\n object {\n id\n type\n }\n }\n intelligence {\n relevant_boards(limit: 10) {\n id\n board {\n name\n }\n }\n relevant_people(limit: 10) {\n id\n user {\n name\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation createWorkflow(\n $workspace_id: ID!\n $title: String\n $privacy_kind: WorkflowBuilderPrivacyKind\n $description: String\n $folder_id: ID\n $owner_ids: [ID!]\n ) {\n create_workflow(\n workspace_id: $workspace_id\n title: $title\n privacy_kind: $privacy_kind\n description: $description\n folder_id: $folder_id\n owner_ids: $owner_ids\n ) {\n workflow_object_id\n workflow_draft_id\n }\n }\n"): (typeof documents)["\n mutation createWorkflow(\n $workspace_id: ID!\n $title: String\n $privacy_kind: WorkflowBuilderPrivacyKind\n $description: String\n $folder_id: ID\n $owner_ids: [ID!]\n ) {\n create_workflow(\n workspace_id: $workspace_id\n title: $title\n privacy_kind: $privacy_kind\n description: $description\n folder_id: $folder_id\n owner_ids: $owner_ids\n ) {\n workflow_object_id\n workflow_draft_id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading
Loading