From 1723d7330db8290579b8d35bd33e45c640e750bf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 16 Apr 2026 18:47:42 -0700 Subject: [PATCH 1/7] feat(monday): add full Monday.com integration with tools, block, triggers, and OAuth Adds a comprehensive Monday.com integration: - 13 tools: list/get boards, CRUD items, search, subitems, updates, groups, move, archive - Block with operation dropdown, board/group selectors, OAuth credential, advanced mode - 9 webhook triggers with auto-subscription lifecycle (create/delete via GraphQL API) - OAuth config with 7 scopes (boards, updates, webhooks, me:read) - Provider handler with challenge verification, formatInput, idempotency - Docs, icon, selectors, and all registry wiring Co-Authored-By: Claude Opus 4.6 --- apps/docs/components/icons.tsx | 23 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + apps/docs/content/docs/en/tools/monday.mdx | 387 ++++++++++++++ apps/docs/content/docs/en/triggers/meta.json | 1 + apps/docs/content/docs/en/triggers/monday.mdx | 215 ++++++++ .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 117 +++++ apps/sim/app/api/tools/monday/boards/route.ts | 86 ++++ apps/sim/app/api/tools/monday/groups/route.ts | 93 ++++ apps/sim/blocks/blocks/monday.ts | 481 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 23 + apps/sim/hooks/selectors/registry.ts | 90 ++++ apps/sim/hooks/selectors/types.ts | 3 + apps/sim/lib/auth/auth.ts | 54 ++ apps/sim/lib/core/config/env.ts | 2 + .../core/security/input-validation.test.ts | 229 +++++++++ .../sim/lib/core/security/input-validation.ts | 164 ++++++ apps/sim/lib/oauth/oauth.ts | 37 ++ apps/sim/lib/oauth/types.ts | 1 + apps/sim/lib/oauth/utils.ts | 10 +- apps/sim/lib/webhooks/processor.ts | 2 +- apps/sim/lib/webhooks/providers/monday.ts | 341 +++++++++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/lib/workflows/subblocks/context.ts | 1 + apps/sim/tools/monday/archive_item.ts | 65 +++ apps/sim/tools/monday/create_group.ts | 104 ++++ apps/sim/tools/monday/create_item.ts | 144 ++++++ apps/sim/tools/monday/create_subitem.ts | 137 +++++ apps/sim/tools/monday/create_update.ts | 91 ++++ apps/sim/tools/monday/delete_item.ts | 64 +++ apps/sim/tools/monday/get_board.ts | 137 +++++ apps/sim/tools/monday/get_item.ts | 114 +++++ apps/sim/tools/monday/get_items.ts | 168 ++++++ apps/sim/tools/monday/index.ts | 13 + apps/sim/tools/monday/list_boards.ts | 97 ++++ apps/sim/tools/monday/move_item_to_group.ts | 125 +++++ apps/sim/tools/monday/search_items.ts | 160 ++++++ apps/sim/tools/monday/types.ts | 222 ++++++++ apps/sim/tools/monday/update_item.ts | 126 +++++ apps/sim/tools/monday/utils.ts | 22 + apps/sim/tools/registry.ts | 28 + apps/sim/triggers/monday/column_changed.ts | 18 + apps/sim/triggers/monday/index.ts | 9 + apps/sim/triggers/monday/item_archived.ts | 18 + apps/sim/triggers/monday/item_created.ts | 19 + apps/sim/triggers/monday/item_deleted.ts | 18 + apps/sim/triggers/monday/item_moved.ts | 18 + apps/sim/triggers/monday/item_name_changed.ts | 18 + apps/sim/triggers/monday/status_changed.ts | 18 + apps/sim/triggers/monday/subitem_created.ts | 18 + apps/sim/triggers/monday/update_created.ts | 18 + apps/sim/triggers/monday/utils.ts | 155 ++++++ apps/sim/triggers/registry.ts | 20 + 55 files changed, 4531 insertions(+), 2 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/monday.mdx create mode 100644 apps/docs/content/docs/en/triggers/monday.mdx create mode 100644 apps/sim/app/api/tools/monday/boards/route.ts create mode 100644 apps/sim/app/api/tools/monday/groups/route.ts create mode 100644 apps/sim/blocks/blocks/monday.ts create mode 100644 apps/sim/lib/webhooks/providers/monday.ts create mode 100644 apps/sim/tools/monday/archive_item.ts create mode 100644 apps/sim/tools/monday/create_group.ts create mode 100644 apps/sim/tools/monday/create_item.ts create mode 100644 apps/sim/tools/monday/create_subitem.ts create mode 100644 apps/sim/tools/monday/create_update.ts create mode 100644 apps/sim/tools/monday/delete_item.ts create mode 100644 apps/sim/tools/monday/get_board.ts create mode 100644 apps/sim/tools/monday/get_item.ts create mode 100644 apps/sim/tools/monday/get_items.ts create mode 100644 apps/sim/tools/monday/index.ts create mode 100644 apps/sim/tools/monday/list_boards.ts create mode 100644 apps/sim/tools/monday/move_item_to_group.ts create mode 100644 apps/sim/tools/monday/search_items.ts create mode 100644 apps/sim/tools/monday/types.ts create mode 100644 apps/sim/tools/monday/update_item.ts create mode 100644 apps/sim/tools/monday/utils.ts create mode 100644 apps/sim/triggers/monday/column_changed.ts create mode 100644 apps/sim/triggers/monday/index.ts create mode 100644 apps/sim/triggers/monday/item_archived.ts create mode 100644 apps/sim/triggers/monday/item_created.ts create mode 100644 apps/sim/triggers/monday/item_deleted.ts create mode 100644 apps/sim/triggers/monday/item_moved.ts create mode 100644 apps/sim/triggers/monday/item_name_changed.ts create mode 100644 apps/sim/triggers/monday/status_changed.ts create mode 100644 apps/sim/triggers/monday/subitem_created.ts create mode 100644 apps/sim/triggers/monday/update_created.ts create mode 100644 apps/sim/triggers/monday/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index d7ae05105d..208cec09b4 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3602,6 +3602,29 @@ export function OpenRouterIcon(props: SVGProps) { ) } +export function MondayIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function MongoDBIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index aec51ff51b..66570ec3af 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -119,6 +119,7 @@ import { MicrosoftSharepointIcon, MicrosoftTeamsIcon, MistralIcon, + MondayIcon, MongoDBIcon, MySQLIcon, Neo4jIcon, @@ -327,6 +328,7 @@ export const blockTypeToIconMap: Record = { microsoft_teams: MicrosoftTeamsIcon, mistral_parse: MistralIcon, mistral_parse_v3: MistralIcon, + monday: MondayIcon, mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 1ef36ef015..2658fa2c39 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -115,6 +115,7 @@ "microsoft_planner", "microsoft_teams", "mistral_parse", + "monday", "mongodb", "mysql", "neo4j", diff --git a/apps/docs/content/docs/en/tools/monday.mdx b/apps/docs/content/docs/en/tools/monday.mdx new file mode 100644 index 0000000000..91f7f1e7d9 --- /dev/null +++ b/apps/docs/content/docs/en/tools/monday.mdx @@ -0,0 +1,387 @@ +--- +title: Monday +description: Manage Monday.com boards, items, and groups +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate with Monday.com to list boards, get board details, fetch and search items, create and update items, archive or delete items, create subitems, move items between groups, add updates, and create groups. + + + +## Tools + +### `monday_list_boards` + +List boards from your Monday.com account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Maximum number of boards to return \(default 25, max 100\) | +| `page` | number | No | Page number for pagination \(starts at 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boards` | array | List of Monday.com boards | +| ↳ `id` | string | Board ID | +| ↳ `name` | string | Board name | +| ↳ `description` | string | Board description | +| ↳ `state` | string | Board state \(active, archived, deleted\) | +| ↳ `boardKind` | string | Board kind \(public, private, share\) | +| ↳ `itemsCount` | number | Number of items on the board | +| ↳ `url` | string | Board URL | +| ↳ `updatedAt` | string | Last updated timestamp | +| `count` | number | Number of boards returned | + +### `monday_get_board` + +Get a specific Monday.com board with its groups and columns + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `board` | json | Board details | +| ↳ `id` | string | Board ID | +| ↳ `name` | string | Board name | +| ↳ `description` | string | Board description | +| ↳ `state` | string | Board state | +| ↳ `boardKind` | string | Board kind \(public, private, share\) | +| ↳ `itemsCount` | number | Number of items | +| ↳ `url` | string | Board URL | +| ↳ `updatedAt` | string | Last updated timestamp | +| `groups` | array | Groups on the board | +| ↳ `id` | string | Group ID | +| ↳ `title` | string | Group title | +| ↳ `color` | string | Group color \(hex\) | +| ↳ `archived` | boolean | Whether the group is archived | +| ↳ `deleted` | boolean | Whether the group is deleted | +| ↳ `position` | string | Group position | +| `columns` | array | Columns on the board | +| ↳ `id` | string | Column ID | +| ↳ `title` | string | Column title | +| ↳ `type` | string | Column type | + +### `monday_get_item` + +Get a specific item by ID from Monday.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the item to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The requested item | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | + +### `monday_get_items` + +Get items from a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to get items from | +| `groupId` | string | No | Filter items by group ID | +| `limit` | number | No | Maximum number of items to return \(default 25, max 500\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | List of items from the board | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state \(active, archived, deleted\) | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values for the item | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Human-readable text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | +| `count` | number | Number of items returned | + +### `monday_search_items` + +Search for items on a Monday.com board by column values + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to search | +| `columns` | string | Yes | JSON array of column filters, e.g. \[\{"column_id":"status","column_values":\["Done"\]\}\] | +| `limit` | number | No | Maximum number of items to return \(default 25, max 500\) | +| `cursor` | string | No | Pagination cursor from a previous search response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Matching items | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | +| `count` | number | Number of items returned | +| `cursor` | string | Pagination cursor for fetching the next page | + +### `monday_create_item` + +Create a new item on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to create the item on | +| `itemName` | string | Yes | The name of the new item | +| `groupId` | string | No | The group ID to create the item in | +| `columnValues` | string | No | JSON string of column values to set \(e.g., \{"status":"Done","date":"2024-01-01"\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The created item | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | + +### `monday_update_item` + +Update column values of an item on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board containing the item | +| `itemId` | string | Yes | The ID of the item to update | +| `columnValues` | string | Yes | JSON string of column values to update \(e.g., \{"status":"Done","date":"2024-01-01"\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The updated item | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | + +### `monday_delete_item` + +Delete an item from a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the item to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | The ID of the deleted item | + +### `monday_archive_item` + +Archive an item on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the item to archive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | The ID of the archived item | + +### `monday_move_item_to_group` + +Move an item to a different group on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the item to move | +| `groupId` | string | Yes | The ID of the target group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The moved item with updated group | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | + +### `monday_create_subitem` + +Create a subitem under a parent item on Monday.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `parentItemId` | string | Yes | The ID of the parent item | +| `itemName` | string | Yes | The name of the new subitem | +| `columnValues` | string | No | JSON string of column values to set | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The created subitem | +| ↳ `id` | string | Item ID | +| ↳ `name` | string | Item name | +| ↳ `state` | string | Item state | +| ↳ `boardId` | string | Board ID | +| ↳ `groupId` | string | Group ID | +| ↳ `groupTitle` | string | Group title | +| ↳ `columnValues` | array | Column values | +| ↳ `id` | string | Column ID | +| ↳ `text` | string | Text value | +| ↳ `value` | string | Raw JSON value | +| ↳ `type` | string | Column type | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `url` | string | Item URL | + +### `monday_create_update` + +Add an update (comment) to a Monday.com item + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the item to add the update to | +| `body` | string | Yes | The update text content \(supports HTML\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `update` | json | The created update | +| ↳ `id` | string | Update ID | +| ↳ `body` | string | Update body \(HTML\) | +| ↳ `textBody` | string | Plain text body | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `creatorId` | string | Creator user ID | +| ↳ `itemId` | string | Item ID | + +### `monday_create_group` + +Create a new group on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to create the group on | +| `groupName` | string | Yes | The name of the new group \(max 255 characters\) | +| `groupColor` | string | No | The group color as a hex code \(e.g., "#ff642e"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `group` | json | The created group | +| ↳ `id` | string | Group ID | +| ↳ `title` | string | Group title | +| ↳ `color` | string | Group color \(hex\) | +| ↳ `archived` | boolean | Whether archived | +| ↳ `deleted` | boolean | Whether deleted | +| ↳ `position` | string | Group position | + + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index d04467626b..05928b13ee 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -30,6 +30,7 @@ "lemlist", "linear", "microsoft-teams", + "monday", "notion", "outlook", "resend", diff --git a/apps/docs/content/docs/en/triggers/monday.mdx b/apps/docs/content/docs/en/triggers/monday.mdx new file mode 100644 index 0000000000..6bb725e499 --- /dev/null +++ b/apps/docs/content/docs/en/triggers/monday.mdx @@ -0,0 +1,215 @@ +--- +title: Monday +description: Available Monday triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Monday provides 9 triggers for automating workflows based on events. + +## Triggers + +### Monday Column Value Changed + +Trigger workflow when any column value changes on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `columnId` | string | The ID of the changed column | +| `columnType` | string | The type of the changed column | +| `columnTitle` | string | The title of the changed column | +| `value` | json | The new value of the column | +| `previousValue` | json | The previous value of the column | + + +--- + +### Monday Item Archived + +Trigger workflow when an item is archived on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | + + +--- + +### Monday Item Created + +Trigger workflow when a new item is created on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | + + +--- + +### Monday Item Deleted + +Trigger workflow when an item is deleted on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | + + +--- + +### Monday Item Moved to Group + +Trigger workflow when an item is moved to any group on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `destGroupId` | string | The destination group ID the item was moved to | +| `sourceGroupId` | string | The source group ID the item was moved from | + + +--- + +### Monday Item Name Changed + +Trigger workflow when an item name changes on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `columnId` | string | The ID of the changed column | +| `columnType` | string | The type of the changed column | +| `columnTitle` | string | The title of the changed column | +| `value` | json | The new value of the column | +| `previousValue` | json | The previous value of the column | + + +--- + +### Monday Status Changed + +Trigger workflow when a status column value changes on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `columnId` | string | The ID of the changed column | +| `columnType` | string | The type of the changed column | +| `columnTitle` | string | The title of the changed column | +| `value` | json | The new value of the column | +| `previousValue` | json | The previous value of the column | + + +--- + +### Monday Subitem Created + +Trigger workflow when a subitem is created on a Monday.com board + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `parentItemId` | string | The parent item ID | +| `parentItemBoardId` | string | The parent item board ID | + + +--- + +### Monday Update Posted + +Trigger workflow when an update or comment is posted on a Monday.com item + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `boardId` | string | The board ID where the event occurred | +| `itemId` | string | The item ID \(pulseId\) | +| `itemName` | string | The item name \(pulseName\) | +| `groupId` | string | The group ID of the item | +| `userId` | string | The ID of the user who triggered the event | +| `triggerTime` | string | ISO timestamp of when the event occurred | +| `triggerUuid` | string | Unique identifier for this event | +| `subscriptionId` | string | The webhook subscription ID | +| `updateId` | string | The ID of the created update | +| `body` | string | The HTML body of the update | +| `textBody` | string | The plain text body of the update | + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 0dec44c268..2b4fb9244b 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -119,6 +119,7 @@ import { MicrosoftSharepointIcon, MicrosoftTeamsIcon, MistralIcon, + MondayIcon, MongoDBIcon, MySQLIcon, Neo4jIcon, @@ -312,6 +313,7 @@ export const blockTypeToIconMap: Record = { microsoft_planner: MicrosoftPlannerIcon, microsoft_teams: MicrosoftTeamsIcon, mistral_parse_v3: MistralIcon, + monday: MondayIcon, mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7a2f6cd107..2fc0a07856 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -8603,6 +8603,123 @@ "integrationTypes": ["ai", "documents"], "tags": ["document-processing", "ocr"] }, + { + "type": "monday", + "slug": "monday", + "name": "Monday", + "description": "Manage Monday.com boards, items, and groups", + "longDescription": "Integrate with Monday.com to list boards, get board details, fetch and search items, create and update items, archive or delete items, create subitems, move items between groups, add updates, and create groups.", + "bgColor": "#FFFFFF", + "iconName": "MondayIcon", + "docsUrl": "https://docs.sim.ai/tools/monday", + "operations": [ + { + "name": "List Boards", + "description": "List boards from your Monday.com account" + }, + { + "name": "Get Board", + "description": "Get a specific Monday.com board with its groups and columns" + }, + { + "name": "Get Item", + "description": "Get a specific item by ID from Monday.com" + }, + { + "name": "Get Items", + "description": "Get items from a Monday.com board" + }, + { + "name": "Search Items", + "description": "Search for items on a Monday.com board by column values" + }, + { + "name": "Create Item", + "description": "Create a new item on a Monday.com board" + }, + { + "name": "Update Item", + "description": "Update column values of an item on a Monday.com board" + }, + { + "name": "Delete Item", + "description": "Delete an item from a Monday.com board" + }, + { + "name": "Archive Item", + "description": "Archive an item on a Monday.com board" + }, + { + "name": "Move Item to Group", + "description": "Move an item to a different group on a Monday.com board" + }, + { + "name": "Create Subitem", + "description": "Create a subitem under a parent item on Monday.com" + }, + { + "name": "Create Update", + "description": "Add an update (comment) to a Monday.com item" + }, + { + "name": "Create Group", + "description": "Create a new group on a Monday.com board" + } + ], + "operationCount": 13, + "triggers": [ + { + "id": "monday_item_created", + "name": "Monday Item Created", + "description": "Trigger workflow when a new item is created on a Monday.com board" + }, + { + "id": "monday_column_changed", + "name": "Monday Column Value Changed", + "description": "Trigger workflow when any column value changes on a Monday.com board" + }, + { + "id": "monday_status_changed", + "name": "Monday Status Changed", + "description": "Trigger workflow when a status column value changes on a Monday.com board" + }, + { + "id": "monday_item_name_changed", + "name": "Monday Item Name Changed", + "description": "Trigger workflow when an item name changes on a Monday.com board" + }, + { + "id": "monday_item_archived", + "name": "Monday Item Archived", + "description": "Trigger workflow when an item is archived on a Monday.com board" + }, + { + "id": "monday_item_deleted", + "name": "Monday Item Deleted", + "description": "Trigger workflow when an item is deleted on a Monday.com board" + }, + { + "id": "monday_item_moved", + "name": "Monday Item Moved to Group", + "description": "Trigger workflow when an item is moved to any group on a Monday.com board" + }, + { + "id": "monday_subitem_created", + "name": "Monday Subitem Created", + "description": "Trigger workflow when a subitem is created on a Monday.com board" + }, + { + "id": "monday_update_created", + "name": "Monday Update Posted", + "description": "Trigger workflow when an update or comment is posted on a Monday.com item" + } + ], + "triggerCount": 9, + "authType": "oauth", + "category": "tools", + "integrationTypes": ["productivity"], + "tags": ["project-management"] + }, { "type": "mongodb", "slug": "mongodb", diff --git a/apps/sim/app/api/tools/monday/boards/route.ts b/apps/sim/app/api/tools/monday/boards/route.ts new file mode 100644 index 0000000000..938c9e1514 --- /dev/null +++ b/apps/sim/app/api/tools/monday/boards/route.ts @@ -0,0 +1,86 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayBoardsAPI') + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + const authz = await authorizeCredentialUse(request as any, { + credentialId: credential, + workflowId, + }) + if (!authz.ok || !authz.credentialOwnerUserId) { + return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + credential, + authz.credentialOwnerUserId, + requestId + ) + if (!accessToken) { + logger.error('Failed to get access token', { + credentialId: credential, + userId: authz.credentialOwnerUserId, + }) + return NextResponse.json( + { error: 'Could not retrieve access token', authRequired: true }, + { status: 401 } + ) + } + + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken, + 'API-Version': '2024-10', + }, + body: JSON.stringify({ + query: '{ boards(limit: 100, state: active) { id name } }', + }), + }) + + const data = await response.json() + + if (data.errors?.length) { + logger.error('Monday.com API error', { errors: data.errors }) + return NextResponse.json( + { error: data.errors[0].message || 'Monday.com API error' }, + { status: 500 } + ) + } + + if (data.error_message) { + logger.error('Monday.com API error', { error_message: data.error_message }) + return NextResponse.json({ error: data.error_message }, { status: 500 }) + } + + const boards = (data.data?.boards || []).map((board: { id: string; name: string }) => ({ + id: board.id, + name: board.name, + })) + + return NextResponse.json({ boards }) + } catch (error) { + logger.error('Error processing Monday boards request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Monday boards', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/monday/groups/route.ts b/apps/sim/app/api/tools/monday/groups/route.ts new file mode 100644 index 0000000000..3fd973e046 --- /dev/null +++ b/apps/sim/app/api/tools/monday/groups/route.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateMondayNumericId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MondayGroupsAPI') + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, boardId, workflowId } = body + + if (!credential || !boardId) { + logger.error('Missing credential or boardId in request') + return NextResponse.json({ error: 'Credential and boardId are required' }, { status: 400 }) + } + + const boardIdValidation = validateMondayNumericId(boardId, 'boardId') + if (!boardIdValidation.isValid) { + return NextResponse.json({ error: boardIdValidation.error }, { status: 400 }) + } + + const authz = await authorizeCredentialUse(request as any, { + credentialId: credential, + workflowId, + }) + if (!authz.ok || !authz.credentialOwnerUserId) { + return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + credential, + authz.credentialOwnerUserId, + requestId + ) + if (!accessToken) { + logger.error('Failed to get access token', { + credentialId: credential, + userId: authz.credentialOwnerUserId, + }) + return NextResponse.json( + { error: 'Could not retrieve access token', authRequired: true }, + { status: 401 } + ) + } + + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: accessToken, + 'API-Version': '2024-10', + }, + body: JSON.stringify({ + query: `{ boards(ids: [${boardIdValidation.sanitized}]) { groups { id title } } }`, + }), + }) + + const data = await response.json() + + if (data.errors?.length) { + logger.error('Monday.com API error', { errors: data.errors }) + return NextResponse.json( + { error: data.errors[0].message || 'Monday.com API error' }, + { status: 500 } + ) + } + + if (data.error_message) { + logger.error('Monday.com API error', { error_message: data.error_message }) + return NextResponse.json({ error: data.error_message }, { status: 500 }) + } + + const board = data.data?.boards?.[0] + const groups = (board?.groups || []).map((group: { id: string; title: string }) => ({ + id: group.id, + name: group.title, + })) + + return NextResponse.json({ groups }) + } catch (error) { + logger.error('Error processing Monday groups request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Monday groups', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/monday.ts b/apps/sim/blocks/blocks/monday.ts new file mode 100644 index 0000000000..7f566c6181 --- /dev/null +++ b/apps/sim/blocks/blocks/monday.ts @@ -0,0 +1,481 @@ +import { MondayIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { + MondayArchiveItemResponse, + MondayCreateGroupResponse, + MondayCreateItemResponse, + MondayCreateSubitemResponse, + MondayCreateUpdateResponse, + MondayDeleteItemResponse, + MondayGetBoardResponse, + MondayGetItemResponse, + MondayGetItemsResponse, + MondayListBoardsResponse, + MondayMoveItemToGroupResponse, + MondaySearchItemsResponse, + MondayUpdateItemResponse, +} from '@/tools/monday/types' +import { getTrigger } from '@/triggers' + +type MondayResponse = + | MondayListBoardsResponse + | MondayGetBoardResponse + | MondayGetItemResponse + | MondayGetItemsResponse + | MondayCreateItemResponse + | MondayUpdateItemResponse + | MondayDeleteItemResponse + | MondayArchiveItemResponse + | MondayCreateUpdateResponse + | MondayCreateGroupResponse + | MondaySearchItemsResponse + | MondayCreateSubitemResponse + | MondayMoveItemToGroupResponse + +const BOARD_OPS = [ + 'get_board', + 'get_items', + 'search_items', + 'create_item', + 'update_item', + 'create_group', +] + +const ITEM_ID_OPS = [ + 'get_item', + 'update_item', + 'delete_item', + 'archive_item', + 'create_update', + 'move_item_to_group', +] + +export const MondayBlock: BlockConfig = { + type: 'monday', + name: 'Monday', + description: 'Manage Monday.com boards, items, and groups', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate with Monday.com to list boards, get board details, fetch and search items, create and update items, archive or delete items, create subitems, move items between groups, add updates, and create groups.', + docsLink: 'https://docs.sim.ai/tools/monday', + category: 'tools', + integrationType: IntegrationType.Productivity, + tags: ['project-management'], + bgColor: '#FFFFFF', + icon: MondayIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Boards', id: 'list_boards' }, + { label: 'Get Board', id: 'get_board' }, + { label: 'Get Item', id: 'get_item' }, + { label: 'Get Items', id: 'get_items' }, + { label: 'Search Items', id: 'search_items' }, + { label: 'Create Item', id: 'create_item' }, + { label: 'Update Item', id: 'update_item' }, + { label: 'Delete Item', id: 'delete_item' }, + { label: 'Archive Item', id: 'archive_item' }, + { label: 'Move Item to Group', id: 'move_item_to_group' }, + { label: 'Create Subitem', id: 'create_subitem' }, + { label: 'Create Update', id: 'create_update' }, + { label: 'Create Group', id: 'create_group' }, + ], + value: () => 'list_boards', + }, + { + id: 'credential', + title: 'Monday Account', + type: 'oauth-input', + serviceId: 'monday', + canonicalParamId: 'oauthCredential', + mode: 'basic', + requiredScopes: getScopesForService('monday'), + placeholder: 'Select Monday.com account', + required: true, + }, + { + id: 'manualCredential', + title: 'Monday Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + // Board selector (basic mode) + { + id: 'boardSelector', + title: 'Board', + type: 'project-selector', + canonicalParamId: 'boardId', + serviceId: 'monday', + selectorKey: 'monday.boards', + placeholder: 'Select a board', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: BOARD_OPS }, + required: { field: 'operation', value: BOARD_OPS }, + }, + // Board ID (advanced mode) + { + id: 'manualBoardId', + title: 'Board ID', + type: 'short-input', + canonicalParamId: 'boardId', + placeholder: 'Enter board ID', + mode: 'advanced', + condition: { field: 'operation', value: BOARD_OPS }, + required: { field: 'operation', value: BOARD_OPS }, + }, + { + id: 'itemId', + title: 'Item ID', + type: 'short-input', + placeholder: 'Enter item ID', + condition: { field: 'operation', value: ITEM_ID_OPS }, + required: { field: 'operation', value: ITEM_ID_OPS }, + }, + { + id: 'parentItemId', + title: 'Parent Item ID', + type: 'short-input', + placeholder: 'Enter parent item ID', + condition: { field: 'operation', value: 'create_subitem' }, + required: { field: 'operation', value: 'create_subitem' }, + }, + { + id: 'itemName', + title: 'Item Name', + type: 'short-input', + placeholder: 'Enter item name', + condition: { field: 'operation', value: ['create_item', 'create_subitem'] }, + required: { field: 'operation', value: ['create_item', 'create_subitem'] }, + }, + // Group selector (basic mode) + { + id: 'groupSelector', + title: 'Group', + type: 'project-selector', + canonicalParamId: 'groupId', + serviceId: 'monday', + selectorKey: 'monday.groups', + placeholder: 'Select a group', + dependsOn: ['credential', 'boardSelector'], + mode: 'basic', + condition: { + field: 'operation', + value: ['get_items', 'create_item', 'move_item_to_group'], + }, + required: { field: 'operation', value: 'move_item_to_group' }, + }, + // Group ID (advanced mode) + { + id: 'manualGroupId', + title: 'Group ID', + type: 'short-input', + canonicalParamId: 'groupId', + placeholder: 'Enter group ID', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_items', 'create_item', 'move_item_to_group'], + }, + required: { field: 'operation', value: 'move_item_to_group' }, + }, + { + id: 'searchColumns', + title: 'Column Filters', + type: 'long-input', + placeholder: '[{"column_id":"status","column_values":["Done"]}]', + condition: { field: 'operation', value: 'search_items' }, + required: { field: 'operation', value: 'search_items' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of Monday.com column filters. Each object needs column_id and column_values array. Return ONLY the JSON array - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'columnValues', + title: 'Column Values', + type: 'long-input', + placeholder: '{"status":"Done","date":"2024-01-01"}', + condition: { + field: 'operation', + value: ['create_item', 'update_item', 'create_subitem'], + }, + required: { field: 'operation', value: 'update_item' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of Monday.com column values. Keys are column IDs and values depend on column type. Return ONLY the JSON object string - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'updateBody', + title: 'Update Text', + type: 'long-input', + placeholder: 'Enter update text (supports HTML)', + condition: { field: 'operation', value: 'create_update' }, + required: { field: 'operation', value: 'create_update' }, + }, + { + id: 'groupName', + title: 'Group Name', + type: 'short-input', + placeholder: 'Enter group name', + condition: { field: 'operation', value: 'create_group' }, + required: { field: 'operation', value: 'create_group' }, + }, + { + id: 'groupColor', + title: 'Group Color', + type: 'short-input', + placeholder: '#ff642e', + mode: 'advanced', + condition: { field: 'operation', value: 'create_group' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max results (default 25)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['list_boards', 'get_items', 'search_items'], + }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: 'Page number (starts at 1)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_boards' }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous search', + mode: 'advanced', + condition: { field: 'operation', value: 'search_items' }, + }, + ...getTrigger('monday_item_created').subBlocks, + ...getTrigger('monday_column_changed').subBlocks, + ...getTrigger('monday_status_changed').subBlocks, + ...getTrigger('monday_item_name_changed').subBlocks, + ...getTrigger('monday_item_archived').subBlocks, + ...getTrigger('monday_item_deleted').subBlocks, + ...getTrigger('monday_item_moved').subBlocks, + ...getTrigger('monday_subitem_created').subBlocks, + ...getTrigger('monday_update_created').subBlocks, + ], + tools: { + access: [ + 'monday_list_boards', + 'monday_get_board', + 'monday_get_item', + 'monday_get_items', + 'monday_search_items', + 'monday_create_item', + 'monday_update_item', + 'monday_delete_item', + 'monday_archive_item', + 'monday_move_item_to_group', + 'monday_create_subitem', + 'monday_create_update', + 'monday_create_group', + ], + config: { + tool: (params) => { + const op = typeof params.operation === 'string' ? params.operation.trim() : 'list_boards' + return `monday_${op}` + }, + params: (params) => { + const baseParams: Record = { + oauthCredential: params.oauthCredential, + } + const op = typeof params.operation === 'string' ? params.operation.trim() : 'list_boards' + + switch (op) { + case 'list_boards': + return { + ...baseParams, + limit: params.limit ? Number(params.limit) : undefined, + page: params.page ? Number(params.page) : undefined, + } + case 'get_board': + return { ...baseParams, boardId: params.boardId } + case 'get_item': + return { ...baseParams, itemId: params.itemId } + case 'get_items': + return { + ...baseParams, + boardId: params.boardId, + groupId: params.groupId || undefined, + limit: params.limit ? Number(params.limit) : undefined, + } + case 'search_items': + return { + ...baseParams, + boardId: params.boardId, + columns: params.searchColumns, + limit: params.limit ? Number(params.limit) : undefined, + cursor: params.cursor || undefined, + } + case 'create_item': + return { + ...baseParams, + boardId: params.boardId, + itemName: params.itemName, + groupId: params.groupId || undefined, + columnValues: params.columnValues || undefined, + } + case 'update_item': + return { + ...baseParams, + boardId: params.boardId, + itemId: params.itemId, + columnValues: params.columnValues, + } + case 'delete_item': + return { ...baseParams, itemId: params.itemId } + case 'archive_item': + return { ...baseParams, itemId: params.itemId } + case 'move_item_to_group': + return { + ...baseParams, + itemId: params.itemId, + groupId: params.groupId, + } + case 'create_subitem': + return { + ...baseParams, + parentItemId: params.parentItemId, + itemName: params.itemName, + columnValues: params.columnValues || undefined, + } + case 'create_update': + return { + ...baseParams, + itemId: params.itemId, + body: params.updateBody, + } + case 'create_group': + return { + ...baseParams, + boardId: params.boardId, + groupName: params.groupName, + groupColor: params.groupColor || undefined, + } + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Monday.com operation to perform' }, + oauthCredential: { type: 'string', description: 'Monday.com OAuth credential' }, + boardId: { type: 'string', description: 'Board ID' }, + itemId: { type: 'string', description: 'Item ID' }, + parentItemId: { type: 'string', description: 'Parent item ID for subitems' }, + itemName: { type: 'string', description: 'Item name for creation' }, + groupId: { type: 'string', description: 'Group ID' }, + searchColumns: { type: 'string', description: 'JSON array of column filters for search' }, + columnValues: { type: 'string', description: 'JSON string of column values' }, + updateBody: { type: 'string', description: 'Update text content' }, + groupName: { type: 'string', description: 'Group name' }, + groupColor: { type: 'string', description: 'Group color hex code' }, + limit: { type: 'number', description: 'Maximum number of results' }, + page: { type: 'number', description: 'Page number for pagination' }, + cursor: { type: 'string', description: 'Pagination cursor for search' }, + }, + outputs: { + boards: { + type: 'json', + description: + 'List of boards (id, name, description, state, boardKind, itemsCount, url, updatedAt)', + condition: { field: 'operation', value: 'list_boards' }, + }, + board: { + type: 'json', + description: + 'Board details (id, name, description, state, boardKind, itemsCount, url, updatedAt)', + condition: { field: 'operation', value: 'get_board' }, + }, + groups: { + type: 'json', + description: 'Board groups (id, title, color, archived, deleted, position)', + condition: { field: 'operation', value: 'get_board' }, + }, + columns: { + type: 'json', + description: 'Board columns (id, title, type)', + condition: { field: 'operation', value: 'get_board' }, + }, + items: { + type: 'json', + description: + 'List of items (id, name, state, boardId, groupId, groupTitle, columnValues, createdAt, updatedAt, url)', + condition: { field: 'operation', value: ['get_items', 'search_items'] }, + }, + item: { + type: 'json', + description: + 'Item details (id, name, state, boardId, groupId, groupTitle, columnValues, createdAt, updatedAt, url)', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'update_item', 'create_subitem', 'move_item_to_group'], + }, + }, + id: { + type: 'string', + description: 'ID of the deleted or archived item', + condition: { field: 'operation', value: ['delete_item', 'archive_item'] }, + }, + update: { + type: 'json', + description: 'Created update (id, body, textBody, createdAt, creatorId, itemId)', + condition: { field: 'operation', value: 'create_update' }, + }, + group: { + type: 'json', + description: 'Created group (id, title, color, archived, deleted, position)', + condition: { field: 'operation', value: 'create_group' }, + }, + count: { + type: 'number', + description: 'Number of returned results', + condition: { field: 'operation', value: ['list_boards', 'get_items', 'search_items'] }, + }, + cursor: { + type: 'string', + description: 'Pagination cursor for fetching the next page of search results', + condition: { field: 'operation', value: 'search_items' }, + }, + }, + triggers: { + enabled: true, + available: [ + 'monday_item_created', + 'monday_column_changed', + 'monday_status_changed', + 'monday_item_name_changed', + 'monday_item_archived', + 'monday_item_deleted', + 'monday_item_moved', + 'monday_subitem_created', + 'monday_update_created', + ], + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 2b2541a4d3..69ae3bb364 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -129,6 +129,7 @@ import { MistralParseV2Block, MistralParseV3Block, } from '@/blocks/blocks/mistral_parse' +import { MondayBlock } from '@/blocks/blocks/monday' import { MongoDBBlock } from '@/blocks/blocks/mongodb' import { MothershipBlock } from '@/blocks/blocks/mothership' import { MySQLBlock } from '@/blocks/blocks/mysql' @@ -371,6 +372,7 @@ export const registry: Record = { mistral_parse: MistralParseBlock, mistral_parse_v2: MistralParseV2Block, mistral_parse_v3: MistralParseV3Block, + monday: MondayBlock, mongodb: MongoDBBlock, mothership: MothershipBlock, mysql: MySQLBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index d7ae05105d..208cec09b4 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3602,6 +3602,29 @@ export function OpenRouterIcon(props: SVGProps) { ) } +export function MondayIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function MongoDBIcon(props: SVGProps) { return ( diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 6b05399425..dc91317ebb 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -1257,6 +1257,96 @@ const registry: Record = { return { id: issue.id, label: issue.name } }, }, + 'monday.boards': { + key: 'monday.boards', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.boards', + context.oauthCredential ?? 'none', + ], + enabled: ({ context }) => Boolean(context.oauthCredential), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'monday.boards') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ boards: { id: string; name: string }[] }>( + '/api/tools/monday/boards', + { + method: 'POST', + body, + } + ) + return (data.boards || []).map((board) => ({ + id: board.id, + label: board.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'monday.boards') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ boards: { id: string; name: string }[] }>( + '/api/tools/monday/boards', + { + method: 'POST', + body, + } + ) + const board = (data.boards || []).find((b) => b.id === detailId) ?? null + if (!board) return null + return { id: board.id, label: board.name } + }, + }, + 'monday.groups': { + key: 'monday.groups', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'monday.groups', + context.oauthCredential ?? 'none', + context.boardId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.oauthCredential && context.boardId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'monday.groups') + const body = JSON.stringify({ + credential: credentialId, + boardId: context.boardId, + workflowId: context.workflowId, + }) + const data = await fetchJson<{ groups: { id: string; name: string }[] }>( + '/api/tools/monday/groups', + { + method: 'POST', + body, + } + ) + return (data.groups || []).map((group) => ({ + id: group.id, + label: group.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'monday.groups') + if (!context.boardId) return null + const body = JSON.stringify({ + credential: credentialId, + boardId: context.boardId, + workflowId: context.workflowId, + }) + const data = await fetchJson<{ groups: { id: string; name: string }[] }>( + '/api/tools/monday/groups', + { + method: 'POST', + body, + } + ) + const group = (data.groups || []).find((g) => g.id === detailId) ?? null + if (!group) return null + return { id: group.id, label: group.name } + }, + }, 'linear.teams': { key: 'linear.teams', staleTime: SELECTOR_STALE, diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index c4423b52e3..9c6a137cea 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -52,6 +52,8 @@ export type SelectorKey = | 'webflow.items' | 'cloudwatch.logGroups' | 'cloudwatch.logStreams' + | 'monday.boards' + | 'monday.groups' | 'sim.workflows' export interface SelectorOption { @@ -82,6 +84,7 @@ export interface SelectorContext { datasetId?: string serviceDeskId?: string impersonateUserEmail?: string + boardId?: string awsAccessKeyId?: string awsSecretAccessKey?: string awsRegion?: string diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 0a593a5bf6..53632d8330 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -623,6 +623,7 @@ export const auth = betterAuth({ 'zoom', 'wordpress', 'linear', + 'monday', 'attio', 'shopify', 'trello', @@ -2027,6 +2028,59 @@ export const auth = betterAuth({ }, }, + // Monday.com provider + { + providerId: 'monday', + clientId: env.MONDAY_CLIENT_ID as string, + clientSecret: env.MONDAY_CLIENT_SECRET as string, + authorizationUrl: 'https://auth.monday.com/oauth2/authorize', + tokenUrl: 'https://auth.monday.com/oauth2/token', + userInfoUrl: 'https://api.monday.com/v2', + scopes: getCanonicalScopesForProvider('monday'), + responseType: 'code', + pkce: false, + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/monday`, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://api.monday.com/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'API-Version': '2024-10', + Authorization: tokens.accessToken ?? '', + }, + body: JSON.stringify({ query: '{ me { id name email } }' }), + }) + + if (!response.ok) { + await response.text().catch(() => {}) + logger.error('Error fetching Monday.com user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const data = await response.json() + const user = data.data?.me + if (!user) return null + + const now = new Date() + return { + id: `${user.id.toString()}-${generateId()}`, + name: user.name || 'Monday.com User', + email: user.email || `${user.id}@monday.user`, + emailVerified: !!user.email, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Monday.com getUserInfo:', { error }) + return null + } + }, + }, + // Reddit provider { providerId: 'reddit', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 2c7ef8bb69..4bd3af9d0d 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -275,6 +275,8 @@ export const env = createEnv({ SUPABASE_CLIENT_SECRET: z.string().optional(), // Supabase OAuth client secret NOTION_CLIENT_ID: z.string().optional(), // Notion OAuth client ID NOTION_CLIENT_SECRET: z.string().optional(), // Notion OAuth client secret + MONDAY_CLIENT_ID: z.string().optional(), // Monday.com OAuth client ID + MONDAY_CLIENT_SECRET: z.string().optional(), // Monday.com OAuth client secret DISCORD_CLIENT_ID: z.string().optional(), // Discord OAuth client ID DISCORD_CLIENT_SECRET: z.string().optional(), // Discord OAuth client secret DOCUSIGN_CLIENT_ID: z.string().optional(), // DocuSign OAuth client ID diff --git a/apps/sim/lib/core/security/input-validation.test.ts b/apps/sim/lib/core/security/input-validation.test.ts index 46d7c7c090..c01f1cbdd5 100644 --- a/apps/sim/lib/core/security/input-validation.test.ts +++ b/apps/sim/lib/core/security/input-validation.test.ts @@ -14,6 +14,9 @@ import { validateJiraCloudId, validateJiraIssueKey, validateMicrosoftGraphId, + validateMondayColumnId, + validateMondayGroupId, + validateMondayNumericId, validateNumericId, validatePathSegment, validateProxyUrl, @@ -1491,3 +1494,229 @@ describe('validateS3BucketName', () => { }) }) }) + +describe('validateMondayNumericId', () => { + describe('valid inputs', () => { + it.concurrent('should accept standard numeric board IDs', () => { + const result = validateMondayNumericId('1234567890', 'boardId') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('1234567890') + }) + + it.concurrent('should accept small numeric IDs', () => { + const result = validateMondayNumericId('12', 'webhookId') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('12') + }) + + it.concurrent('should accept single digit IDs', () => { + const result = validateMondayNumericId('0', 'itemId') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('0') + }) + + it.concurrent('should accept very large numeric IDs', () => { + const result = validateMondayNumericId('98765432101234567890') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept number type input', () => { + const result = validateMondayNumericId(1234567890, 'boardId') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('1234567890') + }) + + it.concurrent('should trim whitespace from numeric IDs', () => { + const result = validateMondayNumericId(' 12345 ', 'boardId') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('12345') + }) + }) + + describe('invalid inputs', () => { + it.concurrent('should reject null', () => { + const result = validateMondayNumericId(null, 'boardId') + expect(result.isValid).toBe(false) + expect(result.error).toContain('boardId') + }) + + it.concurrent('should reject undefined', () => { + const result = validateMondayNumericId(undefined) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject empty string', () => { + const result = validateMondayNumericId('') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with letters', () => { + const result = validateMondayNumericId('abc123') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject GraphQL injection attempts', () => { + const result = validateMondayNumericId('1234]) { subscribers { id } } #') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject negative numbers', () => { + const result = validateMondayNumericId('-1') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject decimal numbers', () => { + const result = validateMondayNumericId('12.34') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with special characters', () => { + const result = validateMondayNumericId('123;DROP TABLE') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with brackets', () => { + const result = validateMondayNumericId('123])') + expect(result.isValid).toBe(false) + }) + }) +}) + +describe('validateMondayGroupId', () => { + describe('valid inputs', () => { + it.concurrent('should accept simple group IDs', () => { + const result = validateMondayGroupId('topics') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('topics') + }) + + it.concurrent('should accept group IDs with underscores', () => { + const result = validateMondayGroupId('new_group') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept group IDs with spaces', () => { + const result = validateMondayGroupId('test group id') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept group IDs with uppercase letters', () => { + const result = validateMondayGroupId('Group One') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept group IDs with digits', () => { + const result = validateMondayGroupId('group123') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept auto-generated group IDs', () => { + const result = validateMondayGroupId('group_title') + expect(result.isValid).toBe(true) + }) + }) + + describe('invalid inputs', () => { + it.concurrent('should reject null', () => { + const result = validateMondayGroupId(null) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject empty string', () => { + const result = validateMondayGroupId('') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with brackets', () => { + const result = validateMondayGroupId('group"]){id}#') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with quotes', () => { + const result = validateMondayGroupId('group")') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject control characters', () => { + const result = validateMondayGroupId('group\x00id') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings exceeding max length', () => { + const result = validateMondayGroupId('a'.repeat(256)) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings with special characters', () => { + const result = validateMondayGroupId('group;DROP') + expect(result.isValid).toBe(false) + }) + }) +}) + +describe('validateMondayColumnId', () => { + describe('valid inputs', () => { + it.concurrent('should accept simple column IDs', () => { + const result = validateMondayColumnId('status') + expect(result.isValid).toBe(true) + expect(result.sanitized).toBe('status') + }) + + it.concurrent('should accept column IDs with digits', () => { + const result = validateMondayColumnId('date4') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept auto-generated column IDs', () => { + const result = validateMondayColumnId('email_mksr9hcd') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept column IDs with underscores', () => { + const result = validateMondayColumnId('color_mksreyj6') + expect(result.isValid).toBe(true) + }) + + it.concurrent('should accept single character column IDs', () => { + const result = validateMondayColumnId('a') + expect(result.isValid).toBe(true) + }) + }) + + describe('invalid inputs', () => { + it.concurrent('should reject null', () => { + const result = validateMondayColumnId(null) + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject empty string', () => { + const result = validateMondayColumnId('') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject uppercase letters', () => { + const result = validateMondayColumnId('Status') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject spaces', () => { + const result = validateMondayColumnId('my column') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject hyphens', () => { + const result = validateMondayColumnId('my-column') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject special characters', () => { + const result = validateMondayColumnId('col;DROP') + expect(result.isValid).toBe(false) + }) + + it.concurrent('should reject strings exceeding max length', () => { + const result = validateMondayColumnId('a'.repeat(256)) + expect(result.isValid).toBe(false) + }) + }) +}) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 2c8e401bf8..8515f1ecd0 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1236,6 +1236,170 @@ const MICROSOFT_CONTENT_SUFFIXES = [ * @param url - The URL to check * @returns Whether the URL belongs to a trusted Microsoft content host */ +/** + * Validates a Monday.com numeric ID (board, item, webhook, workspace, user IDs). + * + * Monday.com uses numeric integer IDs for boards, items, webhooks, workspaces, and users. + * These are always positive integers, represented as strings in GraphQL `ID!` scalars. + * + * @param value - The ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + * + * @example + * ```typescript + * const result = validateMondayNumericId(boardId, 'boardId') + * if (!result.isValid) { + * return NextResponse.json({ error: result.error }, { status: 400 }) + * } + * ``` + */ +export function validateMondayNumericId( + value: string | number | null | undefined, + paramName = 'ID' +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + const str = String(value).trim() + + if (!/^\d+$/.test(str)) { + logger.warn('Monday.com ID is not a valid numeric integer', { + paramName, + value: str.substring(0, 50), + }) + return { + isValid: false, + error: `${paramName} must be a numeric integer`, + } + } + + return { isValid: true, sanitized: str } +} + +/** + * Validates a Monday.com group ID. + * + * Monday.com group IDs are strings that can contain lowercase/uppercase letters, + * digits, underscores, and spaces. They are user-visible identifiers like + * "topics", "new_group", or "test group id". Auto-generated IDs may also + * include "group_title" patterns. + * + * @param value - The group ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + * + * @example + * ```typescript + * const result = validateMondayGroupId(groupId, 'groupId') + * if (!result.isValid) { + * return NextResponse.json({ error: result.error }, { status: 400 }) + * } + * ``` + */ +export function validateMondayGroupId( + value: string | null | undefined, + paramName = 'groupId' +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + if (value.length > 255) { + logger.warn('Monday.com group ID exceeds maximum length', { + paramName, + length: value.length, + }) + return { + isValid: false, + error: `${paramName} exceeds maximum length of 255 characters`, + } + } + + if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) { + logger.warn('Monday.com group ID contains control characters', { paramName }) + return { + isValid: false, + error: `${paramName} contains invalid control characters`, + } + } + + if (!/^[a-zA-Z0-9_ ]+$/.test(value)) { + logger.warn('Monday.com group ID contains disallowed characters', { + paramName, + value: value.substring(0, 100), + }) + return { + isValid: false, + error: `${paramName} can only contain letters, digits, underscores, and spaces`, + } + } + + return { isValid: true, sanitized: value } +} + +/** + * Validates a Monday.com column ID. + * + * Column IDs are strings containing lowercase letters (a-z), digits (0-9), + * and underscores. User-specified IDs are 1-20 characters of [a-z_]. + * Auto-generated IDs follow patterns like "status", "date4", "email_mksr9hcd". + * + * @param value - The column ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + * + * @example + * ```typescript + * const result = validateMondayColumnId(columnId, 'columnId') + * if (!result.isValid) { + * return NextResponse.json({ error: result.error }, { status: 400 }) + * } + * ``` + */ +export function validateMondayColumnId( + value: string | null | undefined, + paramName = 'columnId' +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + if (value.length > 255) { + logger.warn('Monday.com column ID exceeds maximum length', { + paramName, + length: value.length, + }) + return { + isValid: false, + error: `${paramName} exceeds maximum length of 255 characters`, + } + } + + if (!/^[a-z0-9_]+$/.test(value)) { + logger.warn('Monday.com column ID contains disallowed characters', { + paramName, + value: value.substring(0, 100), + }) + return { + isValid: false, + error: `${paramName} can only contain lowercase letters, digits, and underscores`, + } + } + + return { isValid: true, sanitized: value } +} + export function isMicrosoftContentUrl(url: string): boolean { let hostname: string try { diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 308d6978b2..70525b7127 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -33,6 +33,7 @@ import { MicrosoftPlannerIcon, MicrosoftSharepointIcon, MicrosoftTeamsIcon, + MondayIcon, NotionIcon, OutlookIcon, PipedriveIcon, @@ -613,6 +614,29 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'linear', }, + monday: { + name: 'Monday.com', + icon: MondayIcon, + services: { + monday: { + name: 'Monday.com', + description: 'Manage boards, items, and groups in Monday.com.', + providerId: 'monday', + icon: MondayIcon, + baseProviderIcon: MondayIcon, + scopes: [ + 'boards:read', + 'boards:write', + 'updates:read', + 'updates:write', + 'webhooks:read', + 'webhooks:write', + 'me:read', + ], + }, + }, + defaultService: 'monday', + }, box: { name: 'Box', icon: BoxCompanyIcon, @@ -1386,6 +1410,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'monday': { + const { clientId, clientSecret } = getCredentials( + env.MONDAY_CLIENT_ID, + env.MONDAY_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://auth.monday.com/oauth2/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: false, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d41e67e252..5c39f53440 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -101,6 +101,7 @@ export type OAuthService = | 'calcom' | 'docusign' | 'github' + | 'monday' export interface OAuthProviderConfig { name: string diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 91db508669..bd6173b2b9 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -337,7 +337,6 @@ export const SCOPE_DESCRIPTIONS: Record = { 'mail:full': 'Full access to manage Pipedrive emails', 'projects:read': 'Read Pipedrive projects', 'projects:full': 'Full access to manage Pipedrive projects', - 'webhooks:read': 'Read Pipedrive webhooks', 'webhooks:full': 'Full access to manage Pipedrive webhooks', // LinkedIn scopes @@ -414,6 +413,15 @@ export const SCOPE_DESCRIPTIONS: Record = { 'comment:read-write': 'Read and write comments and threads', 'user_management:read': 'View workspace members', 'webhook:read-write': 'Manage webhooks', + + // Monday.com scopes + 'boards:read': 'Read boards, items, and columns', + 'boards:write': 'Create and modify boards, items, and groups', + 'updates:read': 'Read updates and comments', + 'updates:write': 'Create and edit updates and comments', + 'webhooks:read': 'Read webhook subscriptions', + 'webhooks:write': 'Create and manage webhook subscriptions', + 'me:read': 'Read your user profile', } /** diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index c4a8ea07ea..6cad489554 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -117,7 +117,7 @@ export async function parseWebhookBody( } /** Providers that implement challenge/verification handling, checked before webhook lookup. */ -const CHALLENGE_PROVIDERS = ['slack', 'microsoft-teams', 'whatsapp', 'zoom'] as const +const CHALLENGE_PROVIDERS = ['monday', 'slack', 'microsoft-teams', 'whatsapp', 'zoom'] as const export async function handleProviderChallenges( body: unknown, diff --git a/apps/sim/lib/webhooks/providers/monday.ts b/apps/sim/lib/webhooks/providers/monday.ts new file mode 100644 index 0000000000..5c25b94b2b --- /dev/null +++ b/apps/sim/lib/webhooks/providers/monday.ts @@ -0,0 +1,341 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { validateMondayNumericId } from '@/lib/core/security/input-validation' +import { + getCredentialOwner, + getNotificationUrl, + getProviderConfig, +} from '@/lib/webhooks/provider-subscription-utils' +import type { + DeleteSubscriptionContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('WebhookProvider:Monday') + +const MONDAY_API_URL = 'https://api.monday.com/v2' + +/** + * Resolves an OAuth access token from the webhook's credential configuration. + * Follows the Airtable pattern: credentialId → getCredentialOwner → refreshAccessTokenIfNeeded. + */ +async function resolveAccessToken( + config: Record, + userId: string, + requestId: string +): Promise { + const credentialId = config.credentialId as string | undefined + + if (credentialId) { + const credentialOwner = await getCredentialOwner(credentialId, requestId) + if (credentialOwner) { + const token = await refreshAccessTokenIfNeeded( + credentialOwner.accountId, + credentialOwner.userId, + requestId + ) + if (token) return token + } + } + + const fallbackToken = await getOAuthToken(userId, 'monday') + if (fallbackToken) return fallbackToken + + throw new Error( + 'Monday.com account connection required. Please connect your Monday.com account in the trigger configuration and try again.' + ) +} + +export const mondayHandler: WebhookProviderHandler = { + /** + * Handle Monday.com's webhook challenge verification. + * When a webhook is created, Monday.com sends a POST with `{"challenge": "..."}`. + * We must echo back `{"challenge": "..."}` with a 200 status. + */ + handleChallenge(body: unknown) { + const payload = body as Record + // Monday.com challenges have a `challenge` string field but no `type` field + // (Slack challenges use `type: 'url_verification'`). Check both conditions + // to avoid intercepting challenges meant for other providers. + if (payload && typeof payload.challenge === 'string' && !('type' in payload)) { + logger.info('Monday.com webhook challenge received, echoing back') + return NextResponse.json({ challenge: payload.challenge }, { status: 200 }) + } + return null + }, + + /** + * Create a Monday.com webhook subscription via their GraphQL API. + * Monday.com webhooks are board-scoped and event-type-specific. + */ + async createSubscription(ctx: SubscriptionContext): Promise { + const config = getProviderConfig(ctx.webhook) + const triggerId = config.triggerId as string | undefined + const boardId = config.boardId as string | undefined + + if (!triggerId) { + logger.warn(`[${ctx.requestId}] Missing triggerId for Monday webhook ${ctx.webhook.id}`) + throw new Error('Trigger type is required for Monday.com webhook creation.') + } + + if (!boardId) { + logger.warn(`[${ctx.requestId}] Missing boardId for Monday webhook ${ctx.webhook.id}`) + throw new Error( + 'Board ID is required. Please provide a valid Monday.com board ID in the trigger configuration.' + ) + } + + const boardIdValidation = validateMondayNumericId(boardId, 'boardId') + if (!boardIdValidation.isValid) { + throw new Error(boardIdValidation.error!) + } + + const { MONDAY_EVENT_TYPE_MAP } = await import('@/triggers/monday/utils') + const eventType = MONDAY_EVENT_TYPE_MAP[triggerId] + if (!eventType) { + logger.warn(`[${ctx.requestId}] Unknown Monday trigger ID: ${triggerId}`) + throw new Error(`Unknown Monday.com trigger type: ${triggerId}`) + } + + const accessToken = await resolveAccessToken(config, ctx.userId, ctx.requestId) + const notificationUrl = getNotificationUrl(ctx.webhook) + + try { + const response = await fetch(MONDAY_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'API-Version': '2024-10', + Authorization: accessToken, + }, + body: JSON.stringify({ + query: `mutation { create_webhook(board_id: ${boardIdValidation.sanitized}, url: "${notificationUrl}", event: ${eventType}) { id board_id } }`, + }), + }) + + if (!response.ok) { + throw new Error( + `Monday.com API returned HTTP ${response.status}. Please verify your account connection and try again.` + ) + } + + const data = await response.json() + const errors = data.errors as Array<{ message: string }> | undefined + + if (errors && errors.length > 0) { + const errorMsg = errors.map((e) => e.message).join(', ') + logger.error(`[${ctx.requestId}] Failed to create Monday webhook`, { + errors: errorMsg, + webhookId: ctx.webhook.id, + }) + throw new Error(errorMsg || 'Failed to create Monday.com webhook.') + } + + if (data.error_message) { + throw new Error(data.error_message as string) + } + + const result = data.data?.create_webhook + if (!result?.id) { + throw new Error( + 'Monday.com webhook was created but the API response did not include a webhook ID.' + ) + } + + const externalId = String(result.id) + + logger.info( + `[${ctx.requestId}] Created Monday webhook ${externalId} for webhook ${ctx.webhook.id} (event: ${eventType}, board: ${boardId})` + ) + + return { + providerConfigUpdates: { + externalId, + }, + } + } catch (error) { + if (error instanceof Error && error.message !== 'fetch failed') { + throw error + } + logger.error(`[${ctx.requestId}] Error creating Monday webhook`, { + error: error instanceof Error ? error.message : String(error), + }) + throw new Error( + 'Failed to create Monday.com webhook. Please verify your account connection and board ID, then try again.' + ) + } + }, + + /** + * Delete a Monday.com webhook subscription via their GraphQL API. + * Errors are logged but not thrown (non-fatal cleanup). + */ + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const config = getProviderConfig(ctx.webhook) + const externalId = config.externalId as string | undefined + + if (!externalId) { + return + } + + const externalIdValidation = validateMondayNumericId(externalId, 'webhookId') + if (!externalIdValidation.isValid) { + logger.warn( + `[${ctx.requestId}] Invalid externalId format for Monday webhook deletion: ${externalId}` + ) + return + } + + let accessToken: string | null = null + try { + const credentialId = config.credentialId as string | undefined + if (credentialId) { + const credentialOwner = await getCredentialOwner(credentialId, ctx.requestId) + if (credentialOwner) { + accessToken = await refreshAccessTokenIfNeeded( + credentialOwner.accountId, + credentialOwner.userId, + ctx.requestId + ) + } + } + } catch (error) { + logger.warn( + `[${ctx.requestId}] Could not resolve credentials for Monday webhook deletion (non-fatal)`, + { error: error instanceof Error ? error.message : String(error) } + ) + } + + if (!accessToken) { + try { + const fallbackToken = await getOAuthToken(ctx.webhook.userId, 'monday') + if (fallbackToken) accessToken = fallbackToken + } catch { + // Non-fatal — fall through to the guard below + } + } + + if (!accessToken) { + logger.warn( + `[${ctx.requestId}] No access token available for Monday webhook deletion ${externalId} (non-fatal)` + ) + return + } + + try { + const response = await fetch(MONDAY_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'API-Version': '2024-10', + Authorization: accessToken, + }, + body: JSON.stringify({ + query: `mutation { delete_webhook(id: ${externalIdValidation.sanitized}) { id board_id } }`, + }), + }) + + if (!response.ok) { + logger.warn( + `[${ctx.requestId}] Monday API returned HTTP ${response.status} during webhook deletion for ${externalId}` + ) + return + } + + const data = await response.json() + + if (data.errors?.length > 0 || data.error_message) { + const errorMsg = + data.errors?.map((e: { message: string }) => e.message).join(', ') || + data.error_message || + 'Unknown error' + logger.warn( + `[${ctx.requestId}] Monday webhook deletion GraphQL error for ${externalId}: ${errorMsg}` + ) + return + } + + if (data.data?.delete_webhook?.id) { + logger.info( + `[${ctx.requestId}] Deleted Monday webhook ${externalId} for webhook ${ctx.webhook.id}` + ) + } else { + logger.warn(`[${ctx.requestId}] Monday webhook deletion returned no data for ${externalId}`) + } + } catch (error) { + logger.warn(`[${ctx.requestId}] Error deleting Monday webhook ${externalId} (non-fatal)`, { + error: error instanceof Error ? error.message : String(error), + }) + } + }, + + /** + * Transform Monday.com webhook payload into trigger output format. + * Extracts fields from the `event` object and flattens them to match trigger outputs. + */ + async formatInput({ body }: FormatInputContext): Promise { + const payload = body as Record + const event = payload.event as Record | undefined + + if (!event) { + return { + input: payload, + } + } + + const input: Record = { + boardId: event.boardId ? String(event.boardId) : null, + itemId: event.pulseId ? String(event.pulseId) : event.itemId ? String(event.itemId) : null, + itemName: (event.pulseName as string) ?? null, + groupId: (event.groupId as string) ?? null, + userId: event.userId ? String(event.userId) : null, + triggerTime: (event.triggerTime as string) ?? null, + triggerUuid: (event.triggerUuid as string) ?? null, + subscriptionId: event.subscriptionId ? String(event.subscriptionId) : null, + } + + if (event.columnId !== undefined) { + input.columnId = (event.columnId as string) ?? null + input.columnType = (event.columnType as string) ?? null + input.columnTitle = (event.columnTitle as string) ?? null + input.value = event.value ?? null + input.previousValue = event.previousValue ?? null + } + + if (event.destGroupId !== undefined) { + input.destGroupId = (event.destGroupId as string) ?? null + input.sourceGroupId = (event.sourceGroupId as string) ?? null + } + + if (event.parentItemId !== undefined) { + input.parentItemId = event.parentItemId ? String(event.parentItemId) : null + input.parentItemBoardId = event.parentItemBoardId ? String(event.parentItemBoardId) : null + } + + if (event.updateId !== undefined) { + input.updateId = event.updateId ? String(event.updateId) : null + input.body = (event.body as string) ?? null + input.textBody = (event.textBody as string) ?? null + } + + return { input } + }, + + /** + * Extract idempotency ID from Monday.com webhook payload. + * Uses the unique triggerUuid provided by Monday.com. + */ + extractIdempotencyId(body: unknown): string | null { + const payload = body as Record + const event = payload.event as Record | undefined + if (event?.triggerUuid) { + return String(event.triggerUuid) + } + return null + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 332add6598..5ee7605b78 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -23,6 +23,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira' import { lemlistHandler } from '@/lib/webhooks/providers/lemlist' import { linearHandler } from '@/lib/webhooks/providers/linear' import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams' +import { mondayHandler } from '@/lib/webhooks/providers/monday' import { notionHandler } from '@/lib/webhooks/providers/notion' import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { resendHandler } from '@/lib/webhooks/providers/resend' @@ -67,6 +68,7 @@ const PROVIDER_HANDLERS: Record = { jira: jiraHandler, lemlist: lemlistHandler, linear: linearHandler, + monday: mondayHandler, resend: resendHandler, 'microsoft-teams': microsoftTeamsHandler, notion: notionHandler, diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index eca39260ec..fd32f9d696 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -23,6 +23,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'datasetId', 'serviceDeskId', 'impersonateUserEmail', + 'boardId', 'awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion', diff --git a/apps/sim/tools/monday/archive_item.ts b/apps/sim/tools/monday/archive_item.ts new file mode 100644 index 0000000000..c5cb730853 --- /dev/null +++ b/apps/sim/tools/monday/archive_item.ts @@ -0,0 +1,65 @@ +import type { MondayArchiveItemParams, MondayArchiveItemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayArchiveItemTool: ToolConfig = + { + id: 'monday_archive_item', + name: 'Monday Archive Item', + description: 'Archive an item on a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to archive', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `mutation { archive_item(item_id: ${params.itemId}) { id } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { id: '' }, error } + } + + const raw = data.data?.archive_item + if (!raw) { + return { success: false, output: { id: '' }, error: 'Failed to archive item' } + } + + return { + success: true, + output: { id: raw.id as string }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'The ID of the archived item', + }, + }, + } diff --git a/apps/sim/tools/monday/create_group.ts b/apps/sim/tools/monday/create_group.ts new file mode 100644 index 0000000000..4a8bccb69c --- /dev/null +++ b/apps/sim/tools/monday/create_group.ts @@ -0,0 +1,104 @@ +import type { MondayCreateGroupParams, MondayCreateGroupResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayCreateGroupTool: ToolConfig = + { + id: 'monday_create_group', + name: 'Monday Create Group', + description: 'Create a new group on a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to create the group on', + }, + groupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the new group (max 255 characters)', + }, + groupColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group color as a hex code (e.g., "#ff642e")', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_id: ${params.boardId}`, + `group_name: ${JSON.stringify(params.groupName)}`, + ] + if (params.groupColor) { + args.push(`group_color: ${JSON.stringify(params.groupColor)}`) + } + return { + query: `mutation { create_group(${args.join(', ')}) { id title color archived deleted position } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { group: null }, error } + } + + const raw = data.data?.create_group + if (!raw) { + return { success: false, output: { group: null }, error: 'Failed to create group' } + } + + return { + success: true, + output: { + group: { + id: raw.id as string, + title: (raw.title as string) ?? '', + color: (raw.color as string) ?? '', + archived: (raw.archived as boolean) ?? null, + deleted: (raw.deleted as boolean) ?? null, + position: (raw.position as string) ?? '', + }, + }, + } + }, + + outputs: { + group: { + type: 'json', + description: 'The created group', + optional: true, + properties: { + id: { type: 'string', description: 'Group ID' }, + title: { type: 'string', description: 'Group title' }, + color: { type: 'string', description: 'Group color (hex)' }, + archived: { type: 'boolean', description: 'Whether archived', optional: true }, + deleted: { type: 'boolean', description: 'Whether deleted', optional: true }, + position: { type: 'string', description: 'Group position' }, + }, + }, + }, + } diff --git a/apps/sim/tools/monday/create_item.ts b/apps/sim/tools/monday/create_item.ts new file mode 100644 index 0000000000..30611d8a36 --- /dev/null +++ b/apps/sim/tools/monday/create_item.ts @@ -0,0 +1,144 @@ +import type { MondayCreateItemParams, MondayCreateItemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayCreateItemTool: ToolConfig = { + id: 'monday_create_item', + name: 'Monday Create Item', + description: 'Create a new item on a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to create the item on', + }, + itemName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the new item', + }, + groupId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group ID to create the item in', + }, + columnValues: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON string of column values to set (e.g., {"status":"Done","date":"2024-01-01"})', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_id: ${params.boardId}`, + `item_name: ${JSON.stringify(params.itemName)}`, + ] + if (params.groupId) { + args.push(`group_id: ${JSON.stringify(params.groupId)}`) + } + if (params.columnValues) { + args.push(`column_values: ${JSON.stringify(params.columnValues)}`) + } + return { + query: `mutation { create_item(${args.join(', ')}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { item: null }, error } + } + + const raw = data.data?.create_item + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to create item' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The created item', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/create_subitem.ts b/apps/sim/tools/monday/create_subitem.ts new file mode 100644 index 0000000000..a87be2993c --- /dev/null +++ b/apps/sim/tools/monday/create_subitem.ts @@ -0,0 +1,137 @@ +import type { MondayCreateSubitemParams, MondayCreateSubitemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayCreateSubitemTool: ToolConfig< + MondayCreateSubitemParams, + MondayCreateSubitemResponse +> = { + id: 'monday_create_subitem', + name: 'Monday Create Subitem', + description: 'Create a subitem under a parent item on Monday.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + parentItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the parent item', + }, + itemName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the new subitem', + }, + columnValues: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of column values to set', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `parent_item_id: ${params.parentItemId}`, + `item_name: ${JSON.stringify(params.itemName)}`, + ] + if (params.columnValues) { + args.push(`column_values: ${JSON.stringify(params.columnValues)}`) + } + return { + query: `mutation { create_subitem(${args.join(', ')}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { item: null }, error } + } + + const raw = data.data?.create_subitem + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to create subitem' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The created subitem', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/create_update.ts b/apps/sim/tools/monday/create_update.ts new file mode 100644 index 0000000000..07edf16cf3 --- /dev/null +++ b/apps/sim/tools/monday/create_update.ts @@ -0,0 +1,91 @@ +import type { MondayCreateUpdateParams, MondayCreateUpdateResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayCreateUpdateTool: ToolConfig< + MondayCreateUpdateParams, + MondayCreateUpdateResponse +> = { + id: 'monday_create_update', + name: 'Monday Create Update', + description: 'Add an update (comment) to a Monday.com item', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to add the update to', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The update text content (supports HTML)', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `mutation { create_update(item_id: ${params.itemId}, body: ${JSON.stringify(params.body)}) { id body text_body created_at creator_id item_id } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { update: null }, error } + } + + const raw = data.data?.create_update + if (!raw) { + return { success: false, output: { update: null }, error: 'Failed to create update' } + } + + return { + success: true, + output: { + update: { + id: raw.id as string, + body: (raw.body as string) ?? '', + textBody: (raw.text_body as string) ?? null, + createdAt: (raw.created_at as string) ?? null, + creatorId: (raw.creator_id as string) ?? null, + itemId: (raw.item_id as string) ?? null, + }, + }, + } + }, + + outputs: { + update: { + type: 'json', + description: 'The created update', + optional: true, + properties: { + id: { type: 'string', description: 'Update ID' }, + body: { type: 'string', description: 'Update body (HTML)' }, + textBody: { type: 'string', description: 'Plain text body', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + creatorId: { type: 'string', description: 'Creator user ID', optional: true }, + itemId: { type: 'string', description: 'Item ID', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/delete_item.ts b/apps/sim/tools/monday/delete_item.ts new file mode 100644 index 0000000000..a4c4aa7db1 --- /dev/null +++ b/apps/sim/tools/monday/delete_item.ts @@ -0,0 +1,64 @@ +import type { MondayDeleteItemParams, MondayDeleteItemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayDeleteItemTool: ToolConfig = { + id: 'monday_delete_item', + name: 'Monday Delete Item', + description: 'Delete an item from a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to delete', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `mutation { delete_item(item_id: ${params.itemId}) { id } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { id: '' }, error } + } + + const raw = data.data?.delete_item + if (!raw) { + return { success: false, output: { id: '' }, error: 'Failed to delete item' } + } + + return { + success: true, + output: { id: raw.id as string }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'The ID of the deleted item', + }, + }, +} diff --git a/apps/sim/tools/monday/get_board.ts b/apps/sim/tools/monday/get_board.ts new file mode 100644 index 0000000000..6ae971548b --- /dev/null +++ b/apps/sim/tools/monday/get_board.ts @@ -0,0 +1,137 @@ +import type { MondayGetBoardParams, MondayGetBoardResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayGetBoardTool: ToolConfig = { + id: 'monday_get_board', + name: 'Monday Get Board', + description: 'Get a specific Monday.com board with its groups and columns', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to retrieve', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `query { boards(ids: [${params.boardId}]) { id name description state board_kind items_count url updated_at groups { id title color archived deleted position } columns { id title type } } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { board: null, groups: [], columns: [] }, error } + } + + const boards = data.data?.boards ?? [] + if (boards.length === 0) { + return { + success: false, + output: { board: null, groups: [], columns: [] }, + error: 'Board not found', + } + } + + const b = boards[0] + const board = { + id: b.id as string, + name: (b.name as string) ?? '', + description: (b.description as string) ?? null, + state: (b.state as string) ?? 'active', + boardKind: (b.board_kind as string) ?? 'public', + itemsCount: (b.items_count as number) ?? 0, + url: (b.url as string) ?? '', + updatedAt: (b.updated_at as string) ?? null, + } + + const groups = (b.groups ?? []).map((g: Record) => ({ + id: g.id as string, + title: (g.title as string) ?? '', + color: (g.color as string) ?? '', + archived: (g.archived as boolean) ?? null, + deleted: (g.deleted as boolean) ?? null, + position: (g.position as string) ?? '', + })) + + const columns = (b.columns ?? []).map((c: Record) => ({ + id: c.id as string, + title: (c.title as string) ?? '', + type: (c.type as string) ?? '', + })) + + return { + success: true, + output: { board, groups, columns }, + } + }, + + outputs: { + board: { + type: 'json', + description: 'Board details', + optional: true, + properties: { + id: { type: 'string', description: 'Board ID' }, + name: { type: 'string', description: 'Board name' }, + description: { type: 'string', description: 'Board description', optional: true }, + state: { type: 'string', description: 'Board state' }, + boardKind: { type: 'string', description: 'Board kind (public, private, share)' }, + itemsCount: { type: 'number', description: 'Number of items' }, + url: { type: 'string', description: 'Board URL' }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + }, + }, + groups: { + type: 'array', + description: 'Groups on the board', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Group ID' }, + title: { type: 'string', description: 'Group title' }, + color: { type: 'string', description: 'Group color (hex)' }, + archived: { + type: 'boolean', + description: 'Whether the group is archived', + optional: true, + }, + deleted: { type: 'boolean', description: 'Whether the group is deleted', optional: true }, + position: { type: 'string', description: 'Group position' }, + }, + }, + }, + columns: { + type: 'array', + description: 'Columns on the board', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + title: { type: 'string', description: 'Column title' }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/get_item.ts b/apps/sim/tools/monday/get_item.ts new file mode 100644 index 0000000000..e94b98306d --- /dev/null +++ b/apps/sim/tools/monday/get_item.ts @@ -0,0 +1,114 @@ +import type { MondayGetItemParams, MondayGetItemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayGetItemTool: ToolConfig = { + id: 'monday_get_item', + name: 'Monday Get Item', + description: 'Get a specific item by ID from Monday.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to retrieve', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `query { items(ids: [${params.itemId}]) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { item: null }, error } + } + + const items = data.data?.items ?? [] + if (items.length === 0) { + return { success: false, output: { item: null }, error: 'Item not found' } + } + + const raw = items[0] + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The requested item', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/get_items.ts b/apps/sim/tools/monday/get_items.ts new file mode 100644 index 0000000000..005b6df2ba --- /dev/null +++ b/apps/sim/tools/monday/get_items.ts @@ -0,0 +1,168 @@ +import type { MondayGetItemsParams, MondayGetItemsResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +function mapItem(item: Record): { + id: string + name: string + state: string | null + boardId: string | null + groupId: string | null + groupTitle: string | null + columnValues: { id: string; text: string | null; value: string | null; type: string }[] + createdAt: string | null + updatedAt: string | null + url: string | null +} { + const board = item.board as Record | null + const group = item.group as Record | null + const columnValues = ((item.column_values as Record[]) ?? []).map((cv) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + })) + + return { + id: item.id as string, + name: (item.name as string) ?? '', + state: (item.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (item.created_at as string) ?? null, + updatedAt: (item.updated_at as string) ?? null, + url: (item.url as string) ?? null, + } +} + +export const mondayGetItemsTool: ToolConfig = { + id: 'monday_get_items', + name: 'Monday Get Items', + description: 'Get items from a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to get items from', + }, + groupId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter items by group ID', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (default 25, max 500)', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const limit = params.limit ?? 25 + if (params.groupId) { + return { + query: `query { boards(ids: [${params.boardId}]) { groups(ids: ["${params.groupId}"]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } } }`, + } + } + return { + query: `query { boards(ids: [${params.boardId}]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { items: [], count: 0 }, error } + } + + const boards = data.data?.boards ?? [] + if (boards.length === 0) { + return { success: true, output: { items: [], count: 0 } } + } + + const board = boards[0] + let rawItems: Record[] = [] + + if (board.groups) { + for (const group of board.groups) { + const groupItems = group.items_page?.items ?? [] + rawItems = rawItems.concat(groupItems) + } + } else { + rawItems = board.items_page?.items ?? [] + } + + const items = rawItems.map(mapItem) + + return { + success: true, + output: { items, count: items.length }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'List of items from the board', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { + type: 'string', + description: 'Item state (active, archived, deleted)', + optional: true, + }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values for the item', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Human-readable text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, + count: { + type: 'number', + description: 'Number of items returned', + }, + }, +} diff --git a/apps/sim/tools/monday/index.ts b/apps/sim/tools/monday/index.ts new file mode 100644 index 0000000000..417c7daa81 --- /dev/null +++ b/apps/sim/tools/monday/index.ts @@ -0,0 +1,13 @@ +export { mondayArchiveItemTool } from '@/tools/monday/archive_item' +export { mondayCreateGroupTool } from '@/tools/monday/create_group' +export { mondayCreateItemTool } from '@/tools/monday/create_item' +export { mondayCreateSubitemTool } from '@/tools/monday/create_subitem' +export { mondayCreateUpdateTool } from '@/tools/monday/create_update' +export { mondayDeleteItemTool } from '@/tools/monday/delete_item' +export { mondayGetBoardTool } from '@/tools/monday/get_board' +export { mondayGetItemTool } from '@/tools/monday/get_item' +export { mondayGetItemsTool } from '@/tools/monday/get_items' +export { mondayListBoardsTool } from '@/tools/monday/list_boards' +export { mondayMoveItemToGroupTool } from '@/tools/monday/move_item_to_group' +export { mondaySearchItemsTool } from '@/tools/monday/search_items' +export { mondayUpdateItemTool } from '@/tools/monday/update_item' diff --git a/apps/sim/tools/monday/list_boards.ts b/apps/sim/tools/monday/list_boards.ts new file mode 100644 index 0000000000..7758907b56 --- /dev/null +++ b/apps/sim/tools/monday/list_boards.ts @@ -0,0 +1,97 @@ +import type { MondayListBoardsParams, MondayListBoardsResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayListBoardsTool: ToolConfig = { + id: 'monday_list_boards', + name: 'Monday List Boards', + description: 'List boards from your Monday.com account', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of boards to return (default 25, max 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number for pagination (starts at 1)', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const limit = params.limit ?? 25 + const page = params.page ?? 1 + return { + query: `query { boards(limit: ${limit}, page: ${page}, state: active) { id name description state board_kind items_count url updated_at } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { boards: [], count: 0 }, error } + } + + const boards = (data.data?.boards ?? []).map((b: Record) => ({ + id: b.id as string, + name: (b.name as string) ?? '', + description: (b.description as string) ?? null, + state: (b.state as string) ?? 'active', + boardKind: (b.board_kind as string) ?? 'public', + itemsCount: (b.items_count as number) ?? 0, + url: (b.url as string) ?? '', + updatedAt: (b.updated_at as string) ?? null, + })) + + return { + success: true, + output: { boards, count: boards.length }, + } + }, + + outputs: { + boards: { + type: 'array', + description: 'List of Monday.com boards', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Board ID' }, + name: { type: 'string', description: 'Board name' }, + description: { type: 'string', description: 'Board description', optional: true }, + state: { type: 'string', description: 'Board state (active, archived, deleted)' }, + boardKind: { type: 'string', description: 'Board kind (public, private, share)' }, + itemsCount: { type: 'number', description: 'Number of items on the board' }, + url: { type: 'string', description: 'Board URL' }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + }, + }, + }, + count: { + type: 'number', + description: 'Number of boards returned', + }, + }, +} diff --git a/apps/sim/tools/monday/move_item_to_group.ts b/apps/sim/tools/monday/move_item_to_group.ts new file mode 100644 index 0000000000..31f09f59c6 --- /dev/null +++ b/apps/sim/tools/monday/move_item_to_group.ts @@ -0,0 +1,125 @@ +import type { + MondayMoveItemToGroupParams, + MondayMoveItemToGroupResponse, +} from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayMoveItemToGroupTool: ToolConfig< + MondayMoveItemToGroupParams, + MondayMoveItemToGroupResponse +> = { + id: 'monday_move_item_to_group', + name: 'Monday Move Item to Group', + description: 'Move an item to a different group on a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to move', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the target group', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `mutation { move_item_to_group(item_id: ${params.itemId}, group_id: ${JSON.stringify(params.groupId)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { item: null }, error } + } + + const raw = data.data?.move_item_to_group + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to move item' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The moved item with updated group', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/search_items.ts b/apps/sim/tools/monday/search_items.ts new file mode 100644 index 0000000000..f049b909c5 --- /dev/null +++ b/apps/sim/tools/monday/search_items.ts @@ -0,0 +1,160 @@ +import type { MondaySearchItemsParams, MondaySearchItemsResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondaySearchItemsTool: ToolConfig = + { + id: 'monday_search_items', + name: 'Monday Search Items', + description: 'Search for items on a Monday.com board by column values', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board to search', + }, + columns: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of column filters, e.g. [{"column_id":"status","column_values":["Done"]}]', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (default 25, max 500)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous search response', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const limit = params.limit ?? 25 + if (params.cursor) { + return { + query: `query { next_items_page(limit: ${limit}, cursor: ${JSON.stringify(params.cursor)}) { cursor items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } }`, + } + } + const columnsJson = + typeof params.columns === 'string' ? params.columns : JSON.stringify(params.columns) + return { + query: `query { items_page_by_column_values(limit: ${limit}, board_id: ${params.boardId}, columns: ${columnsJson}) { cursor items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { items: [], count: 0, cursor: null }, error } + } + + const page = data.data?.items_page_by_column_values ?? data.data?.next_items_page + if (!page) { + return { success: true, output: { items: [], count: 0, cursor: null } } + } + + const items = (page.items ?? []).map((item: Record) => { + const board = item.board as Record | null + const group = item.group as Record | null + const columnValues = ((item.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + id: item.id as string, + name: (item.name as string) ?? '', + state: (item.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (item.created_at as string) ?? null, + updatedAt: (item.updated_at as string) ?? null, + url: (item.url as string) ?? null, + } + }) + + return { + success: true, + output: { + items, + count: items.length, + cursor: (page.cursor as string) ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Matching items', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, + count: { + type: 'number', + description: 'Number of items returned', + }, + cursor: { + type: 'string', + description: 'Pagination cursor for fetching the next page', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/monday/types.ts b/apps/sim/tools/monday/types.ts new file mode 100644 index 0000000000..b1a939e16a --- /dev/null +++ b/apps/sim/tools/monday/types.ts @@ -0,0 +1,222 @@ +import type { ToolResponse } from '@/tools/types' + +export interface MondayBoard { + id: string + name: string + description: string | null + state: string + boardKind: string + itemsCount: number + url: string + updatedAt: string | null +} + +export interface MondayGroup { + id: string + title: string + color: string + archived: boolean | null + deleted: boolean | null + position: string +} + +export interface MondayColumn { + id: string + title: string + type: string +} + +export interface MondayColumnValue { + id: string + text: string | null + value: string | null + type: string +} + +export interface MondayItem { + id: string + name: string + state: string | null + boardId: string | null + groupId: string | null + groupTitle: string | null + columnValues: MondayColumnValue[] + createdAt: string | null + updatedAt: string | null + url: string | null +} + +export interface MondayUpdate { + id: string + body: string + textBody: string | null + createdAt: string | null + creatorId: string | null + itemId: string | null +} + +export interface MondayListBoardsParams { + accessToken: string + limit?: number + page?: number +} + +export interface MondayListBoardsResponse extends ToolResponse { + output: { + boards: MondayBoard[] + count: number + } +} + +export interface MondayGetBoardParams { + accessToken: string + boardId: string +} + +export interface MondayGetBoardResponse extends ToolResponse { + output: { + board: MondayBoard | null + groups: MondayGroup[] + columns: MondayColumn[] + } +} + +export interface MondayGetItemsParams { + accessToken: string + boardId: string + groupId?: string + limit?: number +} + +export interface MondayGetItemsResponse extends ToolResponse { + output: { + items: MondayItem[] + count: number + } +} + +export interface MondayCreateItemParams { + accessToken: string + boardId: string + itemName: string + groupId?: string + columnValues?: string +} + +export interface MondayCreateItemResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayUpdateItemParams { + accessToken: string + boardId: string + itemId: string + columnValues: string +} + +export interface MondayUpdateItemResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayDeleteItemParams { + accessToken: string + itemId: string +} + +export interface MondayDeleteItemResponse extends ToolResponse { + output: { + id: string + } +} + +export interface MondayCreateUpdateParams { + accessToken: string + itemId: string + body: string +} + +export interface MondayCreateUpdateResponse extends ToolResponse { + output: { + update: MondayUpdate | null + } +} + +export interface MondaySearchItemsParams { + accessToken: string + boardId: string + columns: string + limit?: number + cursor?: string +} + +export interface MondaySearchItemsResponse extends ToolResponse { + output: { + items: MondayItem[] + count: number + cursor: string | null + } +} + +export interface MondayCreateSubitemParams { + accessToken: string + parentItemId: string + itemName: string + columnValues?: string +} + +export interface MondayCreateSubitemResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayMoveItemToGroupParams { + accessToken: string + itemId: string + groupId: string +} + +export interface MondayMoveItemToGroupResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayGetItemParams { + accessToken: string + itemId: string +} + +export interface MondayGetItemResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayArchiveItemParams { + accessToken: string + itemId: string +} + +export interface MondayArchiveItemResponse extends ToolResponse { + output: { + id: string + } +} + +export interface MondayCreateGroupParams { + accessToken: string + boardId: string + groupName: string + groupColor?: string +} + +export interface MondayCreateGroupResponse extends ToolResponse { + output: { + group: MondayGroup | null + } +} diff --git a/apps/sim/tools/monday/update_item.ts b/apps/sim/tools/monday/update_item.ts new file mode 100644 index 0000000000..c3970d1ce1 --- /dev/null +++ b/apps/sim/tools/monday/update_item.ts @@ -0,0 +1,126 @@ +import type { MondayUpdateItemParams, MondayUpdateItemResponse } from '@/tools/monday/types' +import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayUpdateItemTool: ToolConfig = { + id: 'monday_update_item', + name: 'Monday Update Item', + description: 'Update column values of an item on a Monday.com board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'monday', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Monday.com OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the board containing the item', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the item to update', + }, + columnValues: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON string of column values to update (e.g., {"status":"Done","date":"2024-01-01"})', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `mutation { change_multiple_column_values(item_id: ${params.itemId}, board_id: ${params.boardId}, column_values: ${JSON.stringify(params.columnValues)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { item: null }, error } + } + + const raw = data.data?.change_multiple_column_values + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to update item' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The updated item', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/utils.ts b/apps/sim/tools/monday/utils.ts new file mode 100644 index 0000000000..38ea22e282 --- /dev/null +++ b/apps/sim/tools/monday/utils.ts @@ -0,0 +1,22 @@ +export const MONDAY_API_URL = 'https://api.monday.com/v2' + +export function mondayHeaders(accessToken: string): Record { + return { + 'Content-Type': 'application/json', + Authorization: accessToken, + 'API-Version': '2024-10', + } +} + +export function extractMondayError(data: Record): string | null { + if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) { + const messages = (data.errors as Array>) + .map((e) => e.message as string) + .filter(Boolean) + return messages.length > 0 ? messages.join('; ') : 'Unknown Monday.com API error' + } + if (data.error_message) { + return data.error_message as string + } + return null +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 44144459a4..8ed66ad162 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1705,6 +1705,21 @@ import { microsoftTeamsWriteChatTool, } from '@/tools/microsoft_teams' import { mistralParserTool, mistralParserV2Tool, mistralParserV3Tool } from '@/tools/mistral' +import { + mondayArchiveItemTool, + mondayCreateGroupTool, + mondayCreateItemTool, + mondayCreateSubitemTool, + mondayCreateUpdateTool, + mondayDeleteItemTool, + mondayGetBoardTool, + mondayGetItemsTool, + mondayGetItemTool, + mondayListBoardsTool, + mondayMoveItemToGroupTool, + mondaySearchItemsTool, + mondayUpdateItemTool, +} from '@/tools/monday' import { mongodbDeleteTool, mongodbExecuteTool, @@ -3617,6 +3632,19 @@ export const tools: Record = { dspy_predict: predictTool, dspy_chain_of_thought: chainOfThoughtTool, dspy_react: reactTool, + monday_archive_item: mondayArchiveItemTool, + monday_create_group: mondayCreateGroupTool, + monday_create_item: mondayCreateItemTool, + monday_create_subitem: mondayCreateSubitemTool, + monday_create_update: mondayCreateUpdateTool, + monday_delete_item: mondayDeleteItemTool, + monday_get_board: mondayGetBoardTool, + monday_get_item: mondayGetItemTool, + monday_get_items: mondayGetItemsTool, + monday_list_boards: mondayListBoardsTool, + monday_move_item_to_group: mondayMoveItemToGroupTool, + monday_search_items: mondaySearchItemsTool, + monday_update_item: mondayUpdateItemTool, mongodb_query: mongodbQueryTool, mongodb_insert: mongodbInsertTool, mongodb_update: mongodbUpdateTool, diff --git a/apps/sim/triggers/monday/column_changed.ts b/apps/sim/triggers/monday/column_changed.ts new file mode 100644 index 0000000000..08155279f8 --- /dev/null +++ b/apps/sim/triggers/monday/column_changed.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildColumnChangeOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayColumnChangedTrigger: TriggerConfig = { + id: 'monday_column_changed', + name: 'Monday Column Value Changed', + provider: 'monday', + description: 'Trigger workflow when any column value changes on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_column_changed', + eventType: 'Column Value Changed', + }), + outputs: buildColumnChangeOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/index.ts b/apps/sim/triggers/monday/index.ts new file mode 100644 index 0000000000..a8191777b2 --- /dev/null +++ b/apps/sim/triggers/monday/index.ts @@ -0,0 +1,9 @@ +export { mondayColumnChangedTrigger } from './column_changed' +export { mondayItemArchivedTrigger } from './item_archived' +export { mondayItemCreatedTrigger } from './item_created' +export { mondayItemDeletedTrigger } from './item_deleted' +export { mondayItemMovedTrigger } from './item_moved' +export { mondayItemNameChangedTrigger } from './item_name_changed' +export { mondayStatusChangedTrigger } from './status_changed' +export { mondaySubitemCreatedTrigger } from './subitem_created' +export { mondayUpdateCreatedTrigger } from './update_created' diff --git a/apps/sim/triggers/monday/item_archived.ts b/apps/sim/triggers/monday/item_archived.ts new file mode 100644 index 0000000000..ce2167e3ab --- /dev/null +++ b/apps/sim/triggers/monday/item_archived.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildItemOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayItemArchivedTrigger: TriggerConfig = { + id: 'monday_item_archived', + name: 'Monday Item Archived', + provider: 'monday', + description: 'Trigger workflow when an item is archived on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_item_archived', + eventType: 'Item Archived', + }), + outputs: buildItemOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/item_created.ts b/apps/sim/triggers/monday/item_created.ts new file mode 100644 index 0000000000..5083a31470 --- /dev/null +++ b/apps/sim/triggers/monday/item_created.ts @@ -0,0 +1,19 @@ +import { MondayIcon } from '@/components/icons' +import { buildItemOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayItemCreatedTrigger: TriggerConfig = { + id: 'monday_item_created', + name: 'Monday Item Created', + provider: 'monday', + description: 'Trigger workflow when a new item is created on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_item_created', + eventType: 'Item Created', + includeDropdown: true, + }), + outputs: buildItemOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/item_deleted.ts b/apps/sim/triggers/monday/item_deleted.ts new file mode 100644 index 0000000000..f1379e81c3 --- /dev/null +++ b/apps/sim/triggers/monday/item_deleted.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildItemOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayItemDeletedTrigger: TriggerConfig = { + id: 'monday_item_deleted', + name: 'Monday Item Deleted', + provider: 'monday', + description: 'Trigger workflow when an item is deleted on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_item_deleted', + eventType: 'Item Deleted', + }), + outputs: buildItemOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/item_moved.ts b/apps/sim/triggers/monday/item_moved.ts new file mode 100644 index 0000000000..974a97f626 --- /dev/null +++ b/apps/sim/triggers/monday/item_moved.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildItemMovedOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayItemMovedTrigger: TriggerConfig = { + id: 'monday_item_moved', + name: 'Monday Item Moved to Group', + provider: 'monday', + description: 'Trigger workflow when an item is moved to any group on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_item_moved', + eventType: 'Item Moved to Group', + }), + outputs: buildItemMovedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/item_name_changed.ts b/apps/sim/triggers/monday/item_name_changed.ts new file mode 100644 index 0000000000..793c8b49cd --- /dev/null +++ b/apps/sim/triggers/monday/item_name_changed.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildColumnChangeOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayItemNameChangedTrigger: TriggerConfig = { + id: 'monday_item_name_changed', + name: 'Monday Item Name Changed', + provider: 'monday', + description: 'Trigger workflow when an item name changes on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_item_name_changed', + eventType: 'Item Name Changed', + }), + outputs: buildColumnChangeOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/status_changed.ts b/apps/sim/triggers/monday/status_changed.ts new file mode 100644 index 0000000000..bc1cc0d22a --- /dev/null +++ b/apps/sim/triggers/monday/status_changed.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildColumnChangeOutputs, buildMondaySubBlocks } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayStatusChangedTrigger: TriggerConfig = { + id: 'monday_status_changed', + name: 'Monday Status Changed', + provider: 'monday', + description: 'Trigger workflow when a status column value changes on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_status_changed', + eventType: 'Status Changed', + }), + outputs: buildColumnChangeOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/subitem_created.ts b/apps/sim/triggers/monday/subitem_created.ts new file mode 100644 index 0000000000..1dd7403c2e --- /dev/null +++ b/apps/sim/triggers/monday/subitem_created.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildMondaySubBlocks, buildSubitemOutputs } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondaySubitemCreatedTrigger: TriggerConfig = { + id: 'monday_subitem_created', + name: 'Monday Subitem Created', + provider: 'monday', + description: 'Trigger workflow when a subitem is created on a Monday.com board', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_subitem_created', + eventType: 'Subitem Created', + }), + outputs: buildSubitemOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/update_created.ts b/apps/sim/triggers/monday/update_created.ts new file mode 100644 index 0000000000..34850eda98 --- /dev/null +++ b/apps/sim/triggers/monday/update_created.ts @@ -0,0 +1,18 @@ +import { MondayIcon } from '@/components/icons' +import { buildMondaySubBlocks, buildUpdateOutputs } from '@/triggers/monday/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const mondayUpdateCreatedTrigger: TriggerConfig = { + id: 'monday_update_created', + name: 'Monday Update Posted', + provider: 'monday', + description: 'Trigger workflow when an update or comment is posted on a Monday.com item', + version: '1.0.0', + icon: MondayIcon, + subBlocks: buildMondaySubBlocks({ + triggerId: 'monday_update_created', + eventType: 'Update Posted', + }), + outputs: buildUpdateOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/monday/utils.ts b/apps/sim/triggers/monday/utils.ts new file mode 100644 index 0000000000..6a0b15e539 --- /dev/null +++ b/apps/sim/triggers/monday/utils.ts @@ -0,0 +1,155 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +export const mondayTriggerOptions = [ + { label: 'Item Created', id: 'monday_item_created' }, + { label: 'Column Value Changed', id: 'monday_column_changed' }, + { label: 'Status Changed', id: 'monday_status_changed' }, + { label: 'Item Name Changed', id: 'monday_item_name_changed' }, + { label: 'Item Archived', id: 'monday_item_archived' }, + { label: 'Item Deleted', id: 'monday_item_deleted' }, + { label: 'Item Moved to Group', id: 'monday_item_moved' }, + { label: 'Subitem Created', id: 'monday_subitem_created' }, + { label: 'Update Posted', id: 'monday_update_created' }, +] + +/** + * Maps trigger IDs to Monday.com webhook event types used in the + * `create_webhook` GraphQL mutation. + */ +export const MONDAY_EVENT_TYPE_MAP: Record = { + monday_item_created: 'create_item', + monday_column_changed: 'change_column_value', + monday_status_changed: 'change_status_column_value', + monday_item_name_changed: 'change_name', + monday_item_archived: 'item_archived', + monday_item_deleted: 'item_deleted', + monday_item_moved: 'item_moved_to_any_group', + monday_subitem_created: 'create_subitem', + monday_update_created: 'create_update', +} + +export function mondaySetupInstructions(eventType: string): string { + const instructions = [ + `This trigger automatically subscribes to ${eventType} events on the specified board.`, + 'Select your Monday.com account above.', + 'Enter the Board ID you want to monitor. You can find it in the board URL: https://your-org.monday.com/boards/BOARD_ID.', + 'Click "Save" to activate the trigger. The webhook will be created automatically.', + ] + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Builds the subBlock configuration for Monday.com triggers with auto-subscription. + * Pattern follows Linear V2: [dropdown?] → OAuth credential → boardId → instructions + */ +export function buildMondaySubBlocks(options: { + triggerId: string + eventType: string + includeDropdown?: boolean +}): SubBlockConfig[] { + const { triggerId, eventType, includeDropdown } = options + const subBlocks: SubBlockConfig[] = [] + + if (includeDropdown) { + subBlocks.push({ + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + options: mondayTriggerOptions, + value: () => triggerId, + mode: 'trigger', + }) + } + + subBlocks.push( + { + id: 'triggerCredentials', + title: 'Monday Account', + type: 'oauth-input', + description: 'Select your Monday.com account to create the webhook automatically.', + serviceId: 'monday', + requiredScopes: [], + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'boardId', + title: 'Board ID', + type: 'short-input', + placeholder: 'Enter the board ID from the board URL', + description: 'The ID of the board to monitor. Find it in the URL: monday.com/boards/BOARD_ID', + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: mondaySetupInstructions(eventType), + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + } + ) + + return subBlocks +} + +const baseEventOutputs: Record = { + boardId: { type: 'string', description: 'The board ID where the event occurred' }, + itemId: { type: 'string', description: 'The item ID (pulseId)' }, + itemName: { type: 'string', description: 'The item name (pulseName)' }, + groupId: { type: 'string', description: 'The group ID of the item' }, + userId: { type: 'string', description: 'The ID of the user who triggered the event' }, + triggerTime: { type: 'string', description: 'ISO timestamp of when the event occurred' }, + triggerUuid: { type: 'string', description: 'Unique identifier for this event' }, + subscriptionId: { type: 'string', description: 'The webhook subscription ID' }, +} + +export function buildItemOutputs(): Record { + return { ...baseEventOutputs } +} + +export function buildItemMovedOutputs(): Record { + return { + ...baseEventOutputs, + destGroupId: { type: 'string', description: 'The destination group ID the item was moved to' }, + sourceGroupId: { type: 'string', description: 'The source group ID the item was moved from' }, + } +} + +export function buildColumnChangeOutputs(): Record { + return { + ...baseEventOutputs, + columnId: { type: 'string', description: 'The ID of the changed column' }, + columnType: { type: 'string', description: 'The type of the changed column' }, + columnTitle: { type: 'string', description: 'The title of the changed column' }, + value: { type: 'json', description: 'The new value of the column' }, + previousValue: { type: 'json', description: 'The previous value of the column' }, + } +} + +export function buildSubitemOutputs(): Record { + return { + ...baseEventOutputs, + parentItemId: { type: 'string', description: 'The parent item ID' }, + parentItemBoardId: { type: 'string', description: 'The parent item board ID' }, + } +} + +export function buildUpdateOutputs(): Record { + return { + ...baseEventOutputs, + updateId: { type: 'string', description: 'The ID of the created update' }, + body: { type: 'string', description: 'The HTML body of the update' }, + textBody: { type: 'string', description: 'The plain text body of the update' }, + } +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index c189508649..53326d0185 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -207,6 +207,17 @@ import { microsoftTeamsChatSubscriptionTrigger, microsoftTeamsWebhookTrigger, } from '@/triggers/microsoftteams' +import { + mondayColumnChangedTrigger, + mondayItemArchivedTrigger, + mondayItemCreatedTrigger, + mondayItemDeletedTrigger, + mondayItemMovedTrigger, + mondayItemNameChangedTrigger, + mondayStatusChangedTrigger, + mondaySubitemCreatedTrigger, + mondayUpdateCreatedTrigger, +} from '@/triggers/monday' import { notionCommentCreatedTrigger, notionDatabaseCreatedTrigger, @@ -423,6 +434,15 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { linear_project_update_created_v2: linearProjectUpdateCreatedV2Trigger, linear_customer_request_created_v2: linearCustomerRequestCreatedV2Trigger, linear_customer_request_updated_v2: linearCustomerRequestUpdatedV2Trigger, + monday_item_created: mondayItemCreatedTrigger, + monday_column_changed: mondayColumnChangedTrigger, + monday_status_changed: mondayStatusChangedTrigger, + monday_item_name_changed: mondayItemNameChangedTrigger, + monday_item_archived: mondayItemArchivedTrigger, + monday_item_deleted: mondayItemDeletedTrigger, + monday_item_moved: mondayItemMovedTrigger, + monday_subitem_created: mondaySubitemCreatedTrigger, + monday_update_created: mondayUpdateCreatedTrigger, microsoftteams_webhook: microsoftTeamsWebhookTrigger, microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger, notion_page_created: notionPageCreatedTrigger, From 9d05503a554e9c6e790c151fc0770a84b0b8e01b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 16 Apr 2026 19:20:52 -0700 Subject: [PATCH 2/7] fix(monday): cast userId to string in deleteSubscription fallback The DeleteSubscriptionContext type has userId as unknown, causing a TypeScript error when passing it to getOAuthToken which expects string. Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/webhooks/providers/monday.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/monday.ts b/apps/sim/lib/webhooks/providers/monday.ts index 5c25b94b2b..1702c59778 100644 --- a/apps/sim/lib/webhooks/providers/monday.ts +++ b/apps/sim/lib/webhooks/providers/monday.ts @@ -213,7 +213,7 @@ export const mondayHandler: WebhookProviderHandler = { if (!accessToken) { try { - const fallbackToken = await getOAuthToken(ctx.webhook.userId, 'monday') + const fallbackToken = await getOAuthToken(ctx.webhook.userId as string, 'monday') if (fallbackToken) accessToken = fallbackToken } catch { // Non-fatal — fall through to the guard below From 580164c5b768ea300eebd2cddeac0040b39787ea Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 16 Apr 2026 19:25:28 -0700 Subject: [PATCH 3/7] fix(monday): escape string params in GraphQL, align deleteSubscription with established patterns - Use JSON.stringify() for groupId in get_items.ts (matches create_item.ts and move_item_to_group.ts) - Use JSON.stringify() for notificationUrl in webhook provider - Remove non-standard getOAuthToken fallback in deleteSubscription to match Airtable/Webflow pattern (credential resolution only, warn and return on failure) Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/webhooks/providers/monday.ts | 11 +---------- apps/sim/tools/monday/get_items.ts | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/monday.ts b/apps/sim/lib/webhooks/providers/monday.ts index 1702c59778..ff65e81acb 100644 --- a/apps/sim/lib/webhooks/providers/monday.ts +++ b/apps/sim/lib/webhooks/providers/monday.ts @@ -114,7 +114,7 @@ export const mondayHandler: WebhookProviderHandler = { Authorization: accessToken, }, body: JSON.stringify({ - query: `mutation { create_webhook(board_id: ${boardIdValidation.sanitized}, url: "${notificationUrl}", event: ${eventType}) { id board_id } }`, + query: `mutation { create_webhook(board_id: ${boardIdValidation.sanitized}, url: ${JSON.stringify(notificationUrl)}, event: ${eventType}) { id board_id } }`, }), }) @@ -211,15 +211,6 @@ export const mondayHandler: WebhookProviderHandler = { ) } - if (!accessToken) { - try { - const fallbackToken = await getOAuthToken(ctx.webhook.userId as string, 'monday') - if (fallbackToken) accessToken = fallbackToken - } catch { - // Non-fatal — fall through to the guard below - } - } - if (!accessToken) { logger.warn( `[${ctx.requestId}] No access token available for Monday webhook deletion ${externalId} (non-fatal)` diff --git a/apps/sim/tools/monday/get_items.ts b/apps/sim/tools/monday/get_items.ts index 005b6df2ba..423b1a6cba 100644 --- a/apps/sim/tools/monday/get_items.ts +++ b/apps/sim/tools/monday/get_items.ts @@ -83,7 +83,7 @@ export const mondayGetItemsTool: ToolConfig Date: Thu, 16 Apr 2026 19:37:29 -0700 Subject: [PATCH 4/7] fix(monday): sanitize columns JSON in search_items GraphQL query Parse and re-stringify the columns param to ensure well-formed JSON before interpolating into the GraphQL query, preventing injection via malformed input. Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/monday/search_items.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/monday/search_items.ts b/apps/sim/tools/monday/search_items.ts index f049b909c5..78884a1a8d 100644 --- a/apps/sim/tools/monday/search_items.ts +++ b/apps/sim/tools/monday/search_items.ts @@ -60,7 +60,9 @@ export const mondaySearchItemsTool: ToolConfig Date: Thu, 16 Apr 2026 19:46:58 -0700 Subject: [PATCH 5/7] fix(monday): validate all numeric IDs and sanitize columns in GraphQL queries - Add sanitizeNumericId() helper to tools/monday/utils.ts for consistent validation across all tool body builders - Apply to all 13 instances of boardId, itemId, parentItemId interpolation across 11 tool files, preventing GraphQL injection via crafted IDs - Wrap JSON.parse in search_items.ts with try-catch for user-friendly error on malformed column filter JSON Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/monday/archive_item.ts | 9 ++++++-- apps/sim/tools/monday/create_group.ts | 9 ++++++-- apps/sim/tools/monday/create_item.ts | 9 ++++++-- apps/sim/tools/monday/create_subitem.ts | 9 ++++++-- apps/sim/tools/monday/create_update.ts | 9 ++++++-- apps/sim/tools/monday/delete_item.ts | 9 ++++++-- apps/sim/tools/monday/get_board.ts | 9 ++++++-- apps/sim/tools/monday/get_item.ts | 9 ++++++-- apps/sim/tools/monday/get_items.ts | 12 +++++++--- apps/sim/tools/monday/move_item_to_group.ts | 9 ++++++-- apps/sim/tools/monday/search_items.ts | 25 ++++++++++++++++----- apps/sim/tools/monday/update_item.ts | 9 ++++++-- apps/sim/tools/monday/utils.ts | 12 ++++++++++ 13 files changed, 110 insertions(+), 29 deletions(-) diff --git a/apps/sim/tools/monday/archive_item.ts b/apps/sim/tools/monday/archive_item.ts index c5cb730853..4aeeabcd82 100644 --- a/apps/sim/tools/monday/archive_item.ts +++ b/apps/sim/tools/monday/archive_item.ts @@ -1,5 +1,10 @@ import type { MondayArchiveItemParams, MondayArchiveItemResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayArchiveItemTool: ToolConfig = @@ -34,7 +39,7 @@ export const mondayArchiveItemTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => ({ - query: `mutation { archive_item(item_id: ${params.itemId}) { id } }`, + query: `mutation { archive_item(item_id: ${sanitizeNumericId(params.itemId, 'itemId')}) { id } }`, }), }, diff --git a/apps/sim/tools/monday/create_group.ts b/apps/sim/tools/monday/create_group.ts index 4a8bccb69c..49dabaf712 100644 --- a/apps/sim/tools/monday/create_group.ts +++ b/apps/sim/tools/monday/create_group.ts @@ -1,5 +1,10 @@ import type { MondayCreateGroupParams, MondayCreateGroupResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayCreateGroupTool: ToolConfig = @@ -47,7 +52,7 @@ export const mondayCreateGroupTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { const args: string[] = [ - `board_id: ${params.boardId}`, + `board_id: ${sanitizeNumericId(params.boardId, 'boardId')}`, `group_name: ${JSON.stringify(params.groupName)}`, ] if (params.groupColor) { diff --git a/apps/sim/tools/monday/create_item.ts b/apps/sim/tools/monday/create_item.ts index 30611d8a36..40f1d84c1c 100644 --- a/apps/sim/tools/monday/create_item.ts +++ b/apps/sim/tools/monday/create_item.ts @@ -1,5 +1,10 @@ import type { MondayCreateItemParams, MondayCreateItemResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayCreateItemTool: ToolConfig = { @@ -53,7 +58,7 @@ export const mondayCreateItemTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { const args: string[] = [ - `board_id: ${params.boardId}`, + `board_id: ${sanitizeNumericId(params.boardId, 'boardId')}`, `item_name: ${JSON.stringify(params.itemName)}`, ] if (params.groupId) { diff --git a/apps/sim/tools/monday/create_subitem.ts b/apps/sim/tools/monday/create_subitem.ts index a87be2993c..6c982e8a77 100644 --- a/apps/sim/tools/monday/create_subitem.ts +++ b/apps/sim/tools/monday/create_subitem.ts @@ -1,5 +1,10 @@ import type { MondayCreateSubitemParams, MondayCreateSubitemResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayCreateSubitemTool: ToolConfig< @@ -49,7 +54,7 @@ export const mondayCreateSubitemTool: ToolConfig< headers: (params) => mondayHeaders(params.accessToken), body: (params) => { const args: string[] = [ - `parent_item_id: ${params.parentItemId}`, + `parent_item_id: ${sanitizeNumericId(params.parentItemId, 'parentItemId')}`, `item_name: ${JSON.stringify(params.itemName)}`, ] if (params.columnValues) { diff --git a/apps/sim/tools/monday/create_update.ts b/apps/sim/tools/monday/create_update.ts index 07edf16cf3..d4323bef54 100644 --- a/apps/sim/tools/monday/create_update.ts +++ b/apps/sim/tools/monday/create_update.ts @@ -1,5 +1,10 @@ import type { MondayCreateUpdateParams, MondayCreateUpdateResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayCreateUpdateTool: ToolConfig< @@ -42,7 +47,7 @@ export const mondayCreateUpdateTool: ToolConfig< method: 'POST', headers: (params) => mondayHeaders(params.accessToken), body: (params) => ({ - query: `mutation { create_update(item_id: ${params.itemId}, body: ${JSON.stringify(params.body)}) { id body text_body created_at creator_id item_id } }`, + query: `mutation { create_update(item_id: ${sanitizeNumericId(params.itemId, 'itemId')}, body: ${JSON.stringify(params.body)}) { id body text_body created_at creator_id item_id } }`, }), }, diff --git a/apps/sim/tools/monday/delete_item.ts b/apps/sim/tools/monday/delete_item.ts index a4c4aa7db1..4aaa8f3164 100644 --- a/apps/sim/tools/monday/delete_item.ts +++ b/apps/sim/tools/monday/delete_item.ts @@ -1,5 +1,10 @@ import type { MondayDeleteItemParams, MondayDeleteItemResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayDeleteItemTool: ToolConfig = { @@ -33,7 +38,7 @@ export const mondayDeleteItemTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => ({ - query: `mutation { delete_item(item_id: ${params.itemId}) { id } }`, + query: `mutation { delete_item(item_id: ${sanitizeNumericId(params.itemId, 'itemId')}) { id } }`, }), }, diff --git a/apps/sim/tools/monday/get_board.ts b/apps/sim/tools/monday/get_board.ts index 6ae971548b..8ee195aa36 100644 --- a/apps/sim/tools/monday/get_board.ts +++ b/apps/sim/tools/monday/get_board.ts @@ -1,5 +1,10 @@ import type { MondayGetBoardParams, MondayGetBoardResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayGetBoardTool: ToolConfig = { @@ -33,7 +38,7 @@ export const mondayGetBoardTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => ({ - query: `query { boards(ids: [${params.boardId}]) { id name description state board_kind items_count url updated_at groups { id title color archived deleted position } columns { id title type } } }`, + query: `query { boards(ids: [${sanitizeNumericId(params.boardId, 'boardId')}]) { id name description state board_kind items_count url updated_at groups { id title color archived deleted position } columns { id title type } } }`, }), }, diff --git a/apps/sim/tools/monday/get_item.ts b/apps/sim/tools/monday/get_item.ts index e94b98306d..b06b0294b6 100644 --- a/apps/sim/tools/monday/get_item.ts +++ b/apps/sim/tools/monday/get_item.ts @@ -1,5 +1,10 @@ import type { MondayGetItemParams, MondayGetItemResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayGetItemTool: ToolConfig = { @@ -33,7 +38,7 @@ export const mondayGetItemTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => ({ - query: `query { items(ids: [${params.itemId}]) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + query: `query { items(ids: [${sanitizeNumericId(params.itemId, 'itemId')}]) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, }), }, diff --git a/apps/sim/tools/monday/get_items.ts b/apps/sim/tools/monday/get_items.ts index 423b1a6cba..3f06c31a8c 100644 --- a/apps/sim/tools/monday/get_items.ts +++ b/apps/sim/tools/monday/get_items.ts @@ -1,5 +1,10 @@ import type { MondayGetItemsParams, MondayGetItemsResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' function mapItem(item: Record): { @@ -81,13 +86,14 @@ export const mondayGetItemsTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { const limit = params.limit ?? 25 + const boardId = sanitizeNumericId(params.boardId, 'boardId') if (params.groupId) { return { - query: `query { boards(ids: [${params.boardId}]) { groups(ids: [${JSON.stringify(params.groupId)}]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } } }`, + query: `query { boards(ids: [${boardId}]) { groups(ids: [${JSON.stringify(params.groupId)}]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } } }`, } } return { - query: `query { boards(ids: [${params.boardId}]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } }`, + query: `query { boards(ids: [${boardId}]) { items_page(limit: ${limit}) { items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } } }`, } }, }, diff --git a/apps/sim/tools/monday/move_item_to_group.ts b/apps/sim/tools/monday/move_item_to_group.ts index 31f09f59c6..492d777ab3 100644 --- a/apps/sim/tools/monday/move_item_to_group.ts +++ b/apps/sim/tools/monday/move_item_to_group.ts @@ -2,7 +2,12 @@ import type { MondayMoveItemToGroupParams, MondayMoveItemToGroupResponse, } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayMoveItemToGroupTool: ToolConfig< @@ -45,7 +50,7 @@ export const mondayMoveItemToGroupTool: ToolConfig< method: 'POST', headers: (params) => mondayHeaders(params.accessToken), body: (params) => ({ - query: `mutation { move_item_to_group(item_id: ${params.itemId}, group_id: ${JSON.stringify(params.groupId)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + query: `mutation { move_item_to_group(item_id: ${sanitizeNumericId(params.itemId, 'itemId')}, group_id: ${JSON.stringify(params.groupId)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, }), }, diff --git a/apps/sim/tools/monday/search_items.ts b/apps/sim/tools/monday/search_items.ts index 78884a1a8d..a223303ac4 100644 --- a/apps/sim/tools/monday/search_items.ts +++ b/apps/sim/tools/monday/search_items.ts @@ -1,5 +1,10 @@ import type { MondaySearchItemsParams, MondaySearchItemsResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondaySearchItemsTool: ToolConfig = @@ -59,12 +64,20 @@ export const mondaySearchItemsTool: ToolConfig = { @@ -46,7 +51,7 @@ export const mondayUpdateItemTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => ({ - query: `mutation { change_multiple_column_values(item_id: ${params.itemId}, board_id: ${params.boardId}, column_values: ${JSON.stringify(params.columnValues)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, + query: `mutation { change_multiple_column_values(item_id: ${sanitizeNumericId(params.itemId, 'itemId')}, board_id: ${sanitizeNumericId(params.boardId, 'boardId')}, column_values: ${JSON.stringify(params.columnValues)}) { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } }`, }), }, diff --git a/apps/sim/tools/monday/utils.ts b/apps/sim/tools/monday/utils.ts index 38ea22e282..5d4e09e64d 100644 --- a/apps/sim/tools/monday/utils.ts +++ b/apps/sim/tools/monday/utils.ts @@ -8,6 +8,18 @@ export function mondayHeaders(accessToken: string): Record { } } +/** + * Validates that a Monday.com numeric ID (boardId, itemId, etc.) contains only digits. + * Throws with a user-friendly message if invalid, preventing GraphQL injection. + */ +export function sanitizeNumericId(value: string | number, paramName: string): string { + const str = String(value).trim() + if (!/^\d+$/.test(str)) { + throw new Error(`${paramName} must be a numeric integer`) + } + return str +} + export function extractMondayError(data: Record): string | null { if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) { const messages = (data.errors as Array>) From 24a5c8af76c40155a767f2ee127d1825cdc6d608 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 16 Apr 2026 19:54:10 -0700 Subject: [PATCH 6/7] fix(monday): deduplicate numeric ID validation, sanitize limit/page params - Refactor sanitizeNumericId to delegate to validateMondayNumericId from input-validation.ts, eliminating duplicated regex logic - Add sanitizeLimit helper for safe integer coercion with bounds - Apply sanitizeLimit to limit/page params in list_boards, get_items, and search_items for consistent validation across all GraphQL params Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/monday/get_items.ts | 3 ++- apps/sim/tools/monday/list_boards.ts | 11 ++++++++--- apps/sim/tools/monday/search_items.ts | 3 ++- apps/sim/tools/monday/utils.ts | 23 +++++++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/sim/tools/monday/get_items.ts b/apps/sim/tools/monday/get_items.ts index 3f06c31a8c..51428505b8 100644 --- a/apps/sim/tools/monday/get_items.ts +++ b/apps/sim/tools/monday/get_items.ts @@ -3,6 +3,7 @@ import { extractMondayError, MONDAY_API_URL, mondayHeaders, + sanitizeLimit, sanitizeNumericId, } from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' @@ -85,7 +86,7 @@ export const mondayGetItemsTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { - const limit = params.limit ?? 25 + const limit = sanitizeLimit(params.limit, 25, 500) const boardId = sanitizeNumericId(params.boardId, 'boardId') if (params.groupId) { return { diff --git a/apps/sim/tools/monday/list_boards.ts b/apps/sim/tools/monday/list_boards.ts index 7758907b56..d19d967714 100644 --- a/apps/sim/tools/monday/list_boards.ts +++ b/apps/sim/tools/monday/list_boards.ts @@ -1,5 +1,10 @@ import type { MondayListBoardsParams, MondayListBoardsResponse } from '@/tools/monday/types' -import { extractMondayError, MONDAY_API_URL, mondayHeaders } from '@/tools/monday/utils' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeLimit, +} from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' export const mondayListBoardsTool: ToolConfig = { @@ -39,8 +44,8 @@ export const mondayListBoardsTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { - const limit = params.limit ?? 25 - const page = params.page ?? 1 + const limit = sanitizeLimit(params.limit, 25, 500) + const page = sanitizeLimit(params.page, 1, 10000) return { query: `query { boards(limit: ${limit}, page: ${page}, state: active) { id name description state board_kind items_count url updated_at } }`, } diff --git a/apps/sim/tools/monday/search_items.ts b/apps/sim/tools/monday/search_items.ts index a223303ac4..0349fe5615 100644 --- a/apps/sim/tools/monday/search_items.ts +++ b/apps/sim/tools/monday/search_items.ts @@ -3,6 +3,7 @@ import { extractMondayError, MONDAY_API_URL, mondayHeaders, + sanitizeLimit, sanitizeNumericId, } from '@/tools/monday/utils' import type { ToolConfig } from '@/tools/types' @@ -58,7 +59,7 @@ export const mondaySearchItemsTool: ToolConfig mondayHeaders(params.accessToken), body: (params) => { - const limit = params.limit ?? 25 + const limit = sanitizeLimit(params.limit, 25, 500) if (params.cursor) { return { query: `query { next_items_page(limit: ${limit}, cursor: ${JSON.stringify(params.cursor)}) { cursor items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } }`, diff --git a/apps/sim/tools/monday/utils.ts b/apps/sim/tools/monday/utils.ts index 5d4e09e64d..fc75dc8e18 100644 --- a/apps/sim/tools/monday/utils.ts +++ b/apps/sim/tools/monday/utils.ts @@ -1,3 +1,5 @@ +import { validateMondayNumericId } from '@/lib/core/security/input-validation' + export const MONDAY_API_URL = 'https://api.monday.com/v2' export function mondayHeaders(accessToken: string): Record { @@ -9,15 +11,24 @@ export function mondayHeaders(accessToken: string): Record { } /** - * Validates that a Monday.com numeric ID (boardId, itemId, etc.) contains only digits. - * Throws with a user-friendly message if invalid, preventing GraphQL injection. + * Validates a Monday.com numeric ID and returns the sanitized string. + * Delegates to validateMondayNumericId; throws on invalid input. */ export function sanitizeNumericId(value: string | number, paramName: string): string { - const str = String(value).trim() - if (!/^\d+$/.test(str)) { - throw new Error(`${paramName} must be a numeric integer`) + const result = validateMondayNumericId(value, paramName) + if (!result.isValid) { + throw new Error(result.error!) } - return str + return result.sanitized! +} + +/** + * Coerces a limit/page param to a safe integer within bounds. + */ +export function sanitizeLimit(value: number | undefined, defaultVal: number, max: number): number { + const n = Number(value ?? defaultVal) + if (!Number.isFinite(n) || n < 1) return defaultVal + return Math.min(n, max) } export function extractMondayError(data: Record): string | null { From 0bf1850433e78008a767fcf28cd8d1f3c97114fd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 16 Apr 2026 20:09:02 -0700 Subject: [PATCH 7/7] fix(monday): align list_boards limit description with code (max 500) The param description said "max 100" but sanitizeLimit caps at 500, which is what Monday.com's API supports for boards. Updated both the tool description and docs to say "max 500". Co-Authored-By: Claude Opus 4.6 --- apps/docs/content/docs/en/tools/monday.mdx | 2 +- apps/sim/tools/monday/list_boards.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/en/tools/monday.mdx b/apps/docs/content/docs/en/tools/monday.mdx index 91f7f1e7d9..72f7d11e32 100644 --- a/apps/docs/content/docs/en/tools/monday.mdx +++ b/apps/docs/content/docs/en/tools/monday.mdx @@ -26,7 +26,7 @@ List boards from your Monday.com account | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `limit` | number | No | Maximum number of boards to return \(default 25, max 100\) | +| `limit` | number | No | Maximum number of boards to return \(default 25, max 500\) | | `page` | number | No | Page number for pagination \(starts at 1\) | #### Output diff --git a/apps/sim/tools/monday/list_boards.ts b/apps/sim/tools/monday/list_boards.ts index d19d967714..e7aa6f21d7 100644 --- a/apps/sim/tools/monday/list_boards.ts +++ b/apps/sim/tools/monday/list_boards.ts @@ -29,7 +29,7 @@ export const mondayListBoardsTool: ToolConfig