diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index d7ae05105db..208cec09b42 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 aec51ff51b4..66570ec3af3 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 1ef36ef015a..2658fa2c390 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 00000000000..72f7d11e32d --- /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 500\) | +| `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 d04467626b1..05928b13eeb 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 00000000000..6bb725e499b --- /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 0dec44c268f..2b4fb9244ba 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 7a2f6cd107f..2fc0a078569 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 00000000000..938c9e15147 --- /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 00000000000..3fd973e0460 --- /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 00000000000..7f566c6181b --- /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 2b2541a4d37..69ae3bb3641 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 d7ae05105db..208cec09b42 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 6b053994257..dc91317ebb2 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 c4423b52e33..9c6a137cea3 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 0a593a5bf63..53632d8330f 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 2c7ef8bb691..4bd3af9d0d2 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 46d7c7c0903..c01f1cbdd50 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 2c8e401bf84..8515f1ecd0d 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 308d6978b2f..70525b7127e 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 d41e67e2522..5c39f53440a 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 91db5086698..bd6173b2b90 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 c4a8ea07ea4..6cad489554d 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 00000000000..ff65e81acb8 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/monday.ts @@ -0,0 +1,332 @@ +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: ${JSON.stringify(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) { + 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 332add6598f..5ee7605b782 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 eca39260ecc..fd32f9d696a 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 00000000000..4aeeabcd823 --- /dev/null +++ b/apps/sim/tools/monday/archive_item.ts @@ -0,0 +1,70 @@ +import type { MondayArchiveItemParams, MondayArchiveItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.itemId, '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 00000000000..49dabaf712b --- /dev/null +++ b/apps/sim/tools/monday/create_group.ts @@ -0,0 +1,109 @@ +import type { MondayCreateGroupParams, MondayCreateGroupResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.boardId, '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 00000000000..40f1d84c1c5 --- /dev/null +++ b/apps/sim/tools/monday/create_item.ts @@ -0,0 +1,149 @@ +import type { MondayCreateItemParams, MondayCreateItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.boardId, '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 00000000000..6c982e8a77b --- /dev/null +++ b/apps/sim/tools/monday/create_subitem.ts @@ -0,0 +1,142 @@ +import type { MondayCreateSubitemParams, MondayCreateSubitemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.parentItemId, '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 00000000000..d4323bef549 --- /dev/null +++ b/apps/sim/tools/monday/create_update.ts @@ -0,0 +1,96 @@ +import type { MondayCreateUpdateParams, MondayCreateUpdateResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.itemId, '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 00000000000..4aaa8f3164d --- /dev/null +++ b/apps/sim/tools/monday/delete_item.ts @@ -0,0 +1,69 @@ +import type { MondayDeleteItemParams, MondayDeleteItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${sanitizeNumericId(params.itemId, '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 00000000000..8ee195aa365 --- /dev/null +++ b/apps/sim/tools/monday/get_board.ts @@ -0,0 +1,142 @@ +import type { MondayGetBoardParams, MondayGetBoardResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: [${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 } } }`, + }), + }, + + 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 00000000000..b06b0294b6b --- /dev/null +++ b/apps/sim/tools/monday/get_item.ts @@ -0,0 +1,119 @@ +import type { MondayGetItemParams, MondayGetItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: [${sanitizeNumericId(params.itemId, '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 00000000000..51428505b86 --- /dev/null +++ b/apps/sim/tools/monday/get_items.ts @@ -0,0 +1,175 @@ +import type { MondayGetItemsParams, MondayGetItemsResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeLimit, + sanitizeNumericId, +} 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 = sanitizeLimit(params.limit, 25, 500) + const boardId = sanitizeNumericId(params.boardId, 'boardId') + if (params.groupId) { + return { + 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: [${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 00000000000..417c7daa810 --- /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 00000000000..e7aa6f21d7d --- /dev/null +++ b/apps/sim/tools/monday/list_boards.ts @@ -0,0 +1,102 @@ +import type { MondayListBoardsParams, MondayListBoardsResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeLimit, +} 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 500)', + }, + 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 = 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 } }`, + } + }, + }, + + 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 00000000000..492d777ab3a --- /dev/null +++ b/apps/sim/tools/monday/move_item_to_group.ts @@ -0,0 +1,130 @@ +import type { + MondayMoveItemToGroupParams, + MondayMoveItemToGroupResponse, +} from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${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 } }`, + }), + }, + + 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 00000000000..0349fe56150 --- /dev/null +++ b/apps/sim/tools/monday/search_items.ts @@ -0,0 +1,176 @@ +import type { MondaySearchItemsParams, MondaySearchItemsResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeLimit, + sanitizeNumericId, +} 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 = 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 } } }`, + } + } + const boardId = sanitizeNumericId(params.boardId, 'boardId') + let columnsJson: string + try { + columnsJson = + typeof params.columns === 'string' + ? JSON.stringify(JSON.parse(params.columns)) + : JSON.stringify(params.columns) + } catch { + throw new Error( + 'Column filters must be a valid JSON array, e.g. [{"column_id":"status","column_values":["Done"]}]' + ) + } + return { + query: `query { items_page_by_column_values(limit: ${limit}, board_id: ${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 00000000000..b1a939e16af --- /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 00000000000..d0638b30a5e --- /dev/null +++ b/apps/sim/tools/monday/update_item.ts @@ -0,0 +1,131 @@ +import type { MondayUpdateItemParams, MondayUpdateItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} 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: ${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 } }`, + }), + }, + + 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 00000000000..fc75dc8e181 --- /dev/null +++ b/apps/sim/tools/monday/utils.ts @@ -0,0 +1,45 @@ +import { validateMondayNumericId } from '@/lib/core/security/input-validation' + +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', + } +} + +/** + * 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 result = validateMondayNumericId(value, paramName) + if (!result.isValid) { + throw new Error(result.error!) + } + 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 { + 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 44144459a42..8ed66ad1626 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 00000000000..08155279f82 --- /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 00000000000..a8191777b21 --- /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 00000000000..ce2167e3ab4 --- /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 00000000000..5083a314706 --- /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 00000000000..f1379e81c3f --- /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 00000000000..974a97f6267 --- /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 00000000000..793c8b49cd3 --- /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 00000000000..bc1cc0d22a4 --- /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 00000000000..1dd7403c2e5 --- /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 00000000000..34850eda989 --- /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 00000000000..6a0b15e5399 --- /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 c1895086494..53326d0185e 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,