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/execution/costs.mdx b/apps/docs/content/docs/en/execution/costs.mdx index 8ba43616d89..b32530febe6 100644 --- a/apps/docs/content/docs/en/execution/costs.mdx +++ b/apps/docs/content/docs/en/execution/costs.mdx @@ -367,12 +367,12 @@ Sim uses a **base subscription + overage** billing model: ### Threshold Billing -When on-demand is enabled and unbilled overage reaches $50, Sim automatically bills the full unbilled amount. +When on-demand is enabled and unbilled overage reaches $100, Sim automatically bills the full unbilled amount. **Example:** -- Day 10: $70 overage → Bill $70 immediately -- Day 15: Additional $35 usage ($105 total) → Already billed, no action -- Day 20: Another $50 usage ($155 total, $85 unbilled) → Bill $85 immediately +- Day 10: $120 overage → Bill $120 immediately +- Day 15: Additional $60 usage ($180 total) → Already billed, no action +- Day 20: Another $80 usage ($260 total, $140 unbilled) → Bill $140 immediately This spreads large overage charges throughout the month instead of one large bill at period end. @@ -480,5 +480,5 @@ import { FAQ } from '@/components/ui/faq' { question: "What happens when I exceed my plan's credit limit?", answer: "By default, your usage is capped at your plan's included credits and runs will stop. If you enable on-demand billing or manually raise your usage limit in Settings, you can continue running workflows and pay for the overage at the end of the billing period." }, { question: "How does the 1.1x hosted model multiplier work?", answer: "When you use Sim's hosted API keys (instead of bringing your own), a 1.1x multiplier is applied to the base model pricing for Agent blocks. This covers infrastructure and API management costs. You can avoid this multiplier by using your own API keys via the BYOK feature." }, { question: "Are there any free options for AI models?", answer: "Yes. If you run local models through Ollama or VLLM, there are no API costs for those model calls. You still pay the base run charge of 1 credit per run." }, - { question: "When does threshold billing trigger?", answer: "When on-demand billing is enabled and your unbilled overage reaches $50, Sim automatically bills the full unbilled amount. This spreads large charges throughout the month instead of accumulating one large bill at period end." }, + { question: "When does threshold billing trigger?", answer: "When on-demand billing is enabled and your unbilled overage reaches $100, Sim automatically bills the full unbilled amount. This spreads large charges throughout the month instead of accumulating one large bill at period end." }, ]} /> 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/confluence.mdx b/apps/docs/content/docs/en/triggers/confluence.mdx index e0e9420af8b..c2b6ca0dfa3 100644 --- a/apps/docs/content/docs/en/triggers/confluence.mdx +++ b/apps/docs/content/docs/en/triggers/confluence.mdx @@ -10,7 +10,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#E0E0E0" /> -Confluence provides 16 triggers for automating workflows based on events. +Confluence provides 23 triggers for automating workflows based on events. ## Triggers @@ -98,6 +98,49 @@ Trigger workflow when an attachment is removed in Confluence | `files` | file[] | Attachment file content downloaded from Confluence \(if includeFileContent is enabled with credentials\) | +--- + +### Confluence Attachment Updated + +Trigger workflow when an attachment is updated in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | +| `confluenceEmail` | string | No | Your Atlassian account email. Required together with API token to download attachment files. | +| `confluenceApiToken` | string | No | API token from https://id.atlassian.com/manage-profile/security/api-tokens. Required to download attachment file content. | +| `includeFileContent` | boolean | No | Download and include actual file content from attachments. Requires email, API token, and domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | +| `id` | number | Content ID | +| `title` | string | Content title | +| `contentType` | string | Content type \(page, blogpost, comment, attachment\) | +| `version` | number | Version number | +| `spaceKey` | string | Space key the content belongs to | +| `creatorAccountId` | string | Account ID of the creator | +| `lastModifierAccountId` | string | Account ID of the last modifier | +| `self` | string | URL link to the content | +| `creationDate` | number | Creation timestamp \(Unix epoch milliseconds\) | +| `modificationDate` | number | Last modification timestamp \(Unix epoch milliseconds\) | +| `attachment` | object | attachment output from the tool | +| ↳ `mediaType` | string | MIME type of the attachment | +| ↳ `fileSize` | number | File size in bytes | +| ↳ `parent` | object | parent output from the tool | +| ↳ `id` | number | Container page/blog ID | +| ↳ `title` | string | Container page/blog title | +| ↳ `contentType` | string | Container content type | +| `files` | file[] | Attachment file content downloaded from Confluence \(if includeFileContent is enabled with credentials\) | + + --- ### Confluence Blog Post Created @@ -142,6 +185,28 @@ Trigger workflow when a blog post is removed in Confluence | `accountType` | string | Account type \(e.g., customer\) | +--- + +### Confluence Blog Post Restored + +Trigger workflow when a blog post is restored from trash in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | + + --- ### Confluence Blog Post Updated @@ -242,6 +307,45 @@ Trigger workflow when a comment is removed in Confluence | ↳ `self` | string | URL link to the parent content | +--- + +### Confluence Comment Updated + +Trigger workflow when a comment is updated in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | +| `id` | number | Content ID | +| `title` | string | Content title | +| `contentType` | string | Content type \(page, blogpost, comment, attachment\) | +| `version` | number | Version number | +| `spaceKey` | string | Space key the content belongs to | +| `creatorAccountId` | string | Account ID of the creator | +| `lastModifierAccountId` | string | Account ID of the last modifier | +| `self` | string | URL link to the content | +| `creationDate` | number | Creation timestamp \(Unix epoch milliseconds\) | +| `modificationDate` | number | Last modification timestamp \(Unix epoch milliseconds\) | +| `comment` | object | comment output from the tool | +| ↳ `parent` | object | parent output from the tool | +| ↳ `id` | number | Parent page/blog ID | +| ↳ `title` | string | Parent page/blog title | +| ↳ `contentType` | string | Parent content type \(page or blogpost\) | +| ↳ `spaceKey` | string | Space key of the parent | +| ↳ `self` | string | URL link to the parent content | + + --- ### Confluence Label Added @@ -346,6 +450,40 @@ Trigger workflow when a page is moved in Confluence | `accountType` | string | Account type \(e.g., customer\) | +--- + +### Confluence Page Permissions Updated + +Trigger workflow when page permissions are changed in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | +| `id` | number | Content ID | +| `title` | string | Content title | +| `contentType` | string | Content type \(page, blogpost, comment, attachment\) | +| `version` | number | Version number | +| `spaceKey` | string | Space key the content belongs to | +| `creatorAccountId` | string | Account ID of the creator | +| `lastModifierAccountId` | string | Account ID of the last modifier | +| `self` | string | URL link to the content | +| `creationDate` | number | Creation timestamp \(Unix epoch milliseconds\) | +| `modificationDate` | number | Last modification timestamp \(Unix epoch milliseconds\) | +| `page` | object | page output from the tool | +| ↳ `permissions` | json | Updated permissions object for the page | + + --- ### Confluence Page Removed @@ -368,6 +506,28 @@ Trigger workflow when a page is removed or trashed in Confluence | `accountType` | string | Account type \(e.g., customer\) | +--- + +### Confluence Page Restored + +Trigger workflow when a page is restored from trash in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | + + --- ### Confluence Page Updated @@ -416,6 +576,32 @@ Trigger workflow when a new space is created in Confluence | ↳ `self` | string | URL link to the space | +--- + +### Confluence Space Removed + +Trigger workflow when a space is removed in Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | +| `space` | object | space output from the tool | +| ↳ `key` | string | Space key | +| ↳ `name` | string | Space name | +| ↳ `self` | string | URL link to the space | + + --- ### Confluence Space Updated @@ -442,6 +628,35 @@ Trigger workflow when a space is updated in Confluence | ↳ `self` | string | URL link to the space | +--- + +### Confluence User Created + +Trigger workflow when a new user is added to Confluence + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Confluence using HMAC signature | +| `confluenceDomain` | string | No | Your Confluence Cloud domain | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `timestamp` | number | Timestamp of the webhook event \(Unix epoch milliseconds\) | +| `userAccountId` | string | Account ID of the user who triggered the event | +| `accountType` | string | Account type \(e.g., customer\) | +| `user` | object | user output from the tool | +| ↳ `accountId` | string | Account ID of the new user | +| ↳ `accountType` | string | Account type \(e.g., atlassian, app\) | +| ↳ `displayName` | string | Display name of the user | +| ↳ `emailAddress` | string | Email address of the user \(may not be available due to GDPR/privacy settings\) | +| ↳ `publicName` | string | Public name of the user | +| ↳ `self` | string | URL link to the user profile | + + --- ### Confluence Webhook (All Events) @@ -472,5 +687,6 @@ Trigger workflow on any Confluence webhook event | `space` | json | Space object \(present in space events\) | | `label` | json | Label object \(present in label events\) | | `content` | json | Content object \(present in label events\) | +| `user` | json | User object \(present in user events\) | | `files` | file[] | Attachment file content \(present in attachment events when includeFileContent is enabled\) | diff --git a/apps/docs/content/docs/en/triggers/jira.mdx b/apps/docs/content/docs/en/triggers/jira.mdx index 08ac88bc82f..eff9d92990c 100644 --- a/apps/docs/content/docs/en/triggers/jira.mdx +++ b/apps/docs/content/docs/en/triggers/jira.mdx @@ -10,10 +10,182 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#E0E0E0" /> -Jira provides 6 triggers for automating workflows based on events. +Jira provides 15 triggers for automating workflows based on events. ## Triggers +### Jira Comment Deleted + +Trigger workflow when a comment is deleted from a Jira issue + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which comment deletions trigger this workflow using JQL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `votes` | json | Votes on this issue | +| ↳ `labels` | array | Array of labels applied to this issue | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `created` | string | Issue creation date \(ISO format\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `duedate` | string | Due date for the issue | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `watches` | json | Watchers information | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `progress` | json | Progress tracking information | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `security` | string | Security level | +| ↳ `subtasks` | array | Array of subtask objects | +| ↳ `versions` | array | Array of affected versions | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name | +| ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | +| `comment` | object | comment output from the tool | +| ↳ `id` | string | Comment ID | +| ↳ `body` | json | Comment body in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Comment author display name | +| ↳ `accountId` | string | Comment author account ID | +| ↳ `emailAddress` | string | Comment author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the comment | +| ↳ `accountId` | string | Account ID of the user who last updated the comment | +| ↳ `created` | string | Comment creation date \(ISO format\) | +| ↳ `updated` | string | Comment last updated date \(ISO format\) | +| ↳ `self` | string | REST API URL for this comment | + + +--- + +### Jira Comment Updated + +Trigger workflow when a comment is updated on a Jira issue + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which comment updates trigger this workflow using JQL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `votes` | json | Votes on this issue | +| ↳ `labels` | array | Array of labels applied to this issue | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `created` | string | Issue creation date \(ISO format\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `duedate` | string | Due date for the issue | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `watches` | json | Watchers information | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `progress` | json | Progress tracking information | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `security` | string | Security level | +| ↳ `subtasks` | array | Array of subtask objects | +| ↳ `versions` | array | Array of affected versions | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name | +| ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | +| `comment` | object | comment output from the tool | +| ↳ `id` | string | Comment ID | +| ↳ `body` | json | Comment body in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Comment author display name | +| ↳ `accountId` | string | Comment author account ID | +| ↳ `emailAddress` | string | Comment author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the comment | +| ↳ `accountId` | string | Account ID of the user who last updated the comment | +| ↳ `created` | string | Comment creation date \(ISO format\) | +| ↳ `updated` | string | Comment last updated date \(ISO format\) | +| ↳ `self` | string | REST API URL for this comment | + + +--- + ### Jira Issue Commented Trigger workflow when a comment is added to a Jira issue @@ -31,6 +203,10 @@ Trigger workflow when a comment is added to a Jira issue | --------- | ---- | ----------- | | `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | | `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | | `issue` | object | issue output from the tool | | ↳ `id` | string | Jira issue ID | | ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | @@ -46,19 +222,20 @@ Trigger workflow when a comment is added to a Jira issue | ↳ `creator` | object | creator output from the tool | | ↳ `displayName` | string | Creator display name | | ↳ `accountId` | string | Creator account ID | -| ↳ `emailAddress` | string | Creator email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `duedate` | string | Due date for the issue | | ↳ `project` | object | project output from the tool | | ↳ `key` | string | Project key | | ↳ `name` | string | Project name | | ↳ `id` | string | Project ID | | ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `updated` | string | Last updated date \(ISO format\) | | ↳ `watches` | json | Watchers information | | ↳ `assignee` | object | assignee output from the tool | | ↳ `displayName` | string | Assignee display name | | ↳ `accountId` | string | Assignee account ID | -| ↳ `emailAddress` | string | Assignee email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `priority` | object | priority output from the tool | | ↳ `name` | string | Priority name | | ↳ `id` | string | Priority ID | @@ -66,22 +243,31 @@ Trigger workflow when a comment is added to a Jira issue | ↳ `reporter` | object | reporter output from the tool | | ↳ `displayName` | string | Reporter display name | | ↳ `accountId` | string | Reporter account ID | -| ↳ `emailAddress` | string | Reporter email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `security` | string | Security level | | ↳ `subtasks` | array | Array of subtask objects | | ↳ `versions` | array | Array of affected versions | | ↳ `issuetype` | object | issuetype output from the tool | | ↳ `name` | string | Issue type name | | ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | | `comment` | object | comment output from the tool | | ↳ `id` | string | Comment ID | -| ↳ `body` | string | Comment text/body | +| ↳ `body` | json | Comment body in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `author` | object | author output from the tool | | ↳ `displayName` | string | Comment author display name | | ↳ `accountId` | string | Comment author account ID | | ↳ `emailAddress` | string | Comment author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the comment | +| ↳ `accountId` | string | Account ID of the user who last updated the comment | | ↳ `created` | string | Comment creation date \(ISO format\) | | ↳ `updated` | string | Comment last updated date \(ISO format\) | +| ↳ `self` | string | REST API URL for this comment | --- @@ -103,6 +289,10 @@ Trigger workflow when a new issue is created in Jira | --------- | ---- | ----------- | | `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | | `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | | `issue` | object | issue output from the tool | | ↳ `id` | string | Jira issue ID | | ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | @@ -118,19 +308,20 @@ Trigger workflow when a new issue is created in Jira | ↳ `creator` | object | creator output from the tool | | ↳ `displayName` | string | Creator display name | | ↳ `accountId` | string | Creator account ID | -| ↳ `emailAddress` | string | Creator email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `duedate` | string | Due date for the issue | | ↳ `project` | object | project output from the tool | | ↳ `key` | string | Project key | | ↳ `name` | string | Project name | | ↳ `id` | string | Project ID | | ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `updated` | string | Last updated date \(ISO format\) | | ↳ `watches` | json | Watchers information | | ↳ `assignee` | object | assignee output from the tool | | ↳ `displayName` | string | Assignee display name | | ↳ `accountId` | string | Assignee account ID | -| ↳ `emailAddress` | string | Assignee email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `priority` | object | priority output from the tool | | ↳ `name` | string | Priority name | | ↳ `id` | string | Priority ID | @@ -138,13 +329,18 @@ Trigger workflow when a new issue is created in Jira | ↳ `reporter` | object | reporter output from the tool | | ↳ `displayName` | string | Reporter display name | | ↳ `accountId` | string | Reporter account ID | -| ↳ `emailAddress` | string | Reporter email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `security` | string | Security level | | ↳ `subtasks` | array | Array of subtask objects | | ↳ `versions` | array | Array of affected versions | | ↳ `issuetype` | object | issuetype output from the tool | | ↳ `name` | string | Issue type name | | ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | | `issue_event_type_name` | string | Issue event type name from Jira \(only present in issue events\) | @@ -167,6 +363,10 @@ Trigger workflow when an issue is deleted in Jira | --------- | ---- | ----------- | | `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | | `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | | `issue` | object | issue output from the tool | | ↳ `id` | string | Jira issue ID | | ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | @@ -182,19 +382,20 @@ Trigger workflow when an issue is deleted in Jira | ↳ `creator` | object | creator output from the tool | | ↳ `displayName` | string | Creator display name | | ↳ `accountId` | string | Creator account ID | -| ↳ `emailAddress` | string | Creator email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `duedate` | string | Due date for the issue | | ↳ `project` | object | project output from the tool | | ↳ `key` | string | Project key | | ↳ `name` | string | Project name | | ↳ `id` | string | Project ID | | ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `updated` | string | Last updated date \(ISO format\) | | ↳ `watches` | json | Watchers information | | ↳ `assignee` | object | assignee output from the tool | | ↳ `displayName` | string | Assignee display name | | ↳ `accountId` | string | Assignee account ID | -| ↳ `emailAddress` | string | Assignee email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `priority` | object | priority output from the tool | | ↳ `name` | string | Priority name | | ↳ `id` | string | Priority ID | @@ -202,13 +403,18 @@ Trigger workflow when an issue is deleted in Jira | ↳ `reporter` | object | reporter output from the tool | | ↳ `displayName` | string | Reporter display name | | ↳ `accountId` | string | Reporter account ID | -| ↳ `emailAddress` | string | Reporter email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `security` | string | Security level | | ↳ `subtasks` | array | Array of subtask objects | | ↳ `versions` | array | Array of affected versions | | ↳ `issuetype` | object | issuetype output from the tool | | ↳ `name` | string | Issue type name | | ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | | `issue_event_type_name` | string | Issue event type name from Jira \(only present in issue events\) | @@ -232,6 +438,10 @@ Trigger workflow when an issue is updated in Jira | --------- | ---- | ----------- | | `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | | `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | | `issue` | object | issue output from the tool | | ↳ `id` | string | Jira issue ID | | ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | @@ -247,19 +457,20 @@ Trigger workflow when an issue is updated in Jira | ↳ `creator` | object | creator output from the tool | | ↳ `displayName` | string | Creator display name | | ↳ `accountId` | string | Creator account ID | -| ↳ `emailAddress` | string | Creator email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `duedate` | string | Due date for the issue | | ↳ `project` | object | project output from the tool | | ↳ `key` | string | Project key | | ↳ `name` | string | Project name | | ↳ `id` | string | Project ID | | ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `updated` | string | Last updated date \(ISO format\) | | ↳ `watches` | json | Watchers information | | ↳ `assignee` | object | assignee output from the tool | | ↳ `displayName` | string | Assignee display name | | ↳ `accountId` | string | Assignee account ID | -| ↳ `emailAddress` | string | Assignee email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `priority` | object | priority output from the tool | | ↳ `name` | string | Priority name | | ↳ `id` | string | Priority ID | @@ -267,18 +478,194 @@ Trigger workflow when an issue is updated in Jira | ↳ `reporter` | object | reporter output from the tool | | ↳ `displayName` | string | Reporter display name | | ↳ `accountId` | string | Reporter account ID | -| ↳ `emailAddress` | string | Reporter email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `security` | string | Security level | | ↳ `subtasks` | array | Array of subtask objects | | ↳ `versions` | array | Array of affected versions | | ↳ `issuetype` | object | issuetype output from the tool | | ↳ `name` | string | Issue type name | | ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | | `issue_event_type_name` | string | Issue event type name from Jira \(only present in issue events\) | | `changelog` | object | changelog output from the tool | | ↳ `id` | string | Changelog ID | +--- + +### Jira Project Created + +Trigger workflow when a project is created in Jira + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(project_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `project` | object | project output from the tool | +| ↳ `id` | string | Project ID | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `self` | string | REST API URL for this project | +| ↳ `projectTypeKey` | string | Project type \(e.g., software, business\) | +| ↳ `lead` | object | lead output from the tool | +| ↳ `displayName` | string | Project lead display name | +| ↳ `accountId` | string | Project lead account ID | + + +--- + +### Jira Sprint Closed + +Trigger workflow when a sprint is closed in Jira + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., sprint_started, sprint_closed\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `sprint` | object | sprint output from the tool | +| ↳ `id` | number | Sprint ID | +| ↳ `self` | string | REST API URL for this sprint | +| ↳ `state` | string | Sprint state \(future, active, closed\) | +| ↳ `name` | string | Sprint name | +| ↳ `startDate` | string | Sprint start date \(ISO format\) | +| ↳ `endDate` | string | Sprint end date \(ISO format\) | +| ↳ `completeDate` | string | Sprint completion date \(ISO format\) | +| ↳ `originBoardId` | number | Board ID the sprint belongs to | +| ↳ `goal` | string | Sprint goal | +| ↳ `createdDate` | string | Sprint creation date \(ISO format\) | + + +--- + +### Jira Sprint Created + +Trigger workflow when a sprint is created in Jira + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., sprint_started, sprint_closed\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `sprint` | object | sprint output from the tool | +| ↳ `id` | number | Sprint ID | +| ↳ `self` | string | REST API URL for this sprint | +| ↳ `state` | string | Sprint state \(future, active, closed\) | +| ↳ `name` | string | Sprint name | +| ↳ `startDate` | string | Sprint start date \(ISO format\) | +| ↳ `endDate` | string | Sprint end date \(ISO format\) | +| ↳ `completeDate` | string | Sprint completion date \(ISO format\) | +| ↳ `originBoardId` | number | Board ID the sprint belongs to | +| ↳ `goal` | string | Sprint goal | +| ↳ `createdDate` | string | Sprint creation date \(ISO format\) | + + +--- + +### Jira Sprint Started + +Trigger workflow when a sprint is started in Jira + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., sprint_started, sprint_closed\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `sprint` | object | sprint output from the tool | +| ↳ `id` | number | Sprint ID | +| ↳ `self` | string | REST API URL for this sprint | +| ↳ `state` | string | Sprint state \(future, active, closed\) | +| ↳ `name` | string | Sprint name | +| ↳ `startDate` | string | Sprint start date \(ISO format\) | +| ↳ `endDate` | string | Sprint end date \(ISO format\) | +| ↳ `completeDate` | string | Sprint completion date \(ISO format\) | +| ↳ `originBoardId` | number | Board ID the sprint belongs to | +| ↳ `goal` | string | Sprint goal | +| ↳ `createdDate` | string | Sprint creation date \(ISO format\) | + + +--- + +### Jira Version Released + +Trigger workflow when a version is released in Jira + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(jira:version_released\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `version` | object | version output from the tool | +| ↳ `id` | string | Version ID | +| ↳ `name` | string | Version name | +| ↳ `self` | string | REST API URL for this version | +| ↳ `released` | boolean | Whether the version is released | +| ↳ `releaseDate` | string | Release date \(ISO format\) | +| ↳ `projectId` | number | Project ID the version belongs to | +| ↳ `description` | string | Version description | +| ↳ `archived` | boolean | Whether the version is archived | + + --- ### Jira Webhook (All Events) @@ -337,6 +724,190 @@ Trigger workflow when time is logged on a Jira issue | --------- | ---- | ----------- | | `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | | `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `votes` | json | Votes on this issue | +| ↳ `labels` | array | Array of labels applied to this issue | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `created` | string | Issue creation date \(ISO format\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `duedate` | string | Due date for the issue | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `watches` | json | Watchers information | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `progress` | json | Progress tracking information | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `security` | string | Security level | +| ↳ `subtasks` | array | Array of subtask objects | +| ↳ `versions` | array | Array of affected versions | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name | +| ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | +| `worklog` | object | worklog output from the tool | +| ↳ `id` | string | Worklog entry ID | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Worklog author display name | +| ↳ `accountId` | string | Worklog author account ID | +| ↳ `emailAddress` | string | Worklog author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the worklog | +| ↳ `accountId` | string | Account ID of the user who last updated the worklog | +| ↳ `timeSpent` | string | Time spent \(e.g., "2h 30m"\) | +| ↳ `timeSpentSeconds` | number | Time spent in seconds | +| ↳ `comment` | string | Worklog comment/description | +| ↳ `started` | string | When the work was started \(ISO format\) | +| ↳ `created` | string | When the worklog entry was created \(ISO format\) | +| ↳ `updated` | string | When the worklog entry was last updated \(ISO format\) | +| ↳ `issueId` | string | ID of the issue this worklog belongs to | +| ↳ `self` | string | REST API URL for this worklog entry | + + +--- + +### Jira Worklog Deleted + +Trigger workflow when a worklog entry is deleted from a Jira issue + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which worklog deletions trigger this workflow using JQL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `votes` | json | Votes on this issue | +| ↳ `labels` | array | Array of labels applied to this issue | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `created` | string | Issue creation date \(ISO format\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `duedate` | string | Due date for the issue | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `watches` | json | Watchers information | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `progress` | json | Progress tracking information | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `security` | string | Security level | +| ↳ `subtasks` | array | Array of subtask objects | +| ↳ `versions` | array | Array of affected versions | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name | +| ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | +| `worklog` | object | worklog output from the tool | +| ↳ `id` | string | Worklog entry ID | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Worklog author display name | +| ↳ `accountId` | string | Worklog author account ID | +| ↳ `emailAddress` | string | Worklog author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the worklog | +| ↳ `accountId` | string | Account ID of the user who last updated the worklog | +| ↳ `timeSpent` | string | Time spent \(e.g., "2h 30m"\) | +| ↳ `timeSpentSeconds` | number | Time spent in seconds | +| ↳ `comment` | string | Worklog comment/description | +| ↳ `started` | string | When the work was started \(ISO format\) | +| ↳ `created` | string | When the worklog entry was created \(ISO format\) | +| ↳ `updated` | string | When the worklog entry was last updated \(ISO format\) | +| ↳ `issueId` | string | ID of the issue this worklog belongs to | +| ↳ `self` | string | REST API URL for this worklog entry | + + +--- + +### Jira Worklog Updated + +Trigger workflow when a worklog entry is updated on a Jira issue + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which worklog updates trigger this workflow using JQL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, comment_created, worklog_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| ↳ `emailAddress` | string | Email address of the user who triggered the event | | `issue` | object | issue output from the tool | | ↳ `id` | string | Jira issue ID | | ↳ `key` | string | Jira issue key \(e.g., PROJ-123\) | @@ -352,19 +923,20 @@ Trigger workflow when time is logged on a Jira issue | ↳ `creator` | object | creator output from the tool | | ↳ `displayName` | string | Creator display name | | ↳ `accountId` | string | Creator account ID | -| ↳ `emailAddress` | string | Creator email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `duedate` | string | Due date for the issue | | ↳ `project` | object | project output from the tool | | ↳ `key` | string | Project key | | ↳ `name` | string | Project name | | ↳ `id` | string | Project ID | | ↳ `summary` | string | Issue summary/title | +| ↳ `description` | json | Issue description in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | | ↳ `updated` | string | Last updated date \(ISO format\) | | ↳ `watches` | json | Watchers information | | ↳ `assignee` | object | assignee output from the tool | | ↳ `displayName` | string | Assignee display name | | ↳ `accountId` | string | Assignee account ID | -| ↳ `emailAddress` | string | Assignee email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `priority` | object | priority output from the tool | | ↳ `name` | string | Priority name | | ↳ `id` | string | Priority ID | @@ -372,21 +944,33 @@ Trigger workflow when time is logged on a Jira issue | ↳ `reporter` | object | reporter output from the tool | | ↳ `displayName` | string | Reporter display name | | ↳ `accountId` | string | Reporter account ID | -| ↳ `emailAddress` | string | Reporter email address | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | | ↳ `security` | string | Security level | | ↳ `subtasks` | array | Array of subtask objects | | ↳ `versions` | array | Array of affected versions | | ↳ `issuetype` | object | issuetype output from the tool | | ↳ `name` | string | Issue type name | | ↳ `id` | string | Issue type ID | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| ↳ `components` | array | Array of component objects associated with this issue | +| ↳ `fixVersions` | array | Array of fix version objects for this issue | | `worklog` | object | worklog output from the tool | | ↳ `id` | string | Worklog entry ID | | ↳ `author` | object | author output from the tool | | ↳ `displayName` | string | Worklog author display name | | ↳ `accountId` | string | Worklog author account ID | | ↳ `emailAddress` | string | Worklog author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the worklog | +| ↳ `accountId` | string | Account ID of the user who last updated the worklog | | ↳ `timeSpent` | string | Time spent \(e.g., "2h 30m"\) | | ↳ `timeSpentSeconds` | number | Time spent in seconds | | ↳ `comment` | string | Worklog comment/description | | ↳ `started` | string | When the work was started \(ISO format\) | +| ↳ `created` | string | When the worklog entry was created \(ISO format\) | +| ↳ `updated` | string | When the worklog entry was last updated \(ISO format\) | +| ↳ `issueId` | string | ID of the issue this worklog belongs to | +| ↳ `self` | string | REST API URL for this worklog entry | diff --git a/apps/docs/content/docs/en/triggers/jsm.mdx b/apps/docs/content/docs/en/triggers/jsm.mdx new file mode 100644 index 00000000000..6aabf82cade --- /dev/null +++ b/apps/docs/content/docs/en/triggers/jsm.mdx @@ -0,0 +1,314 @@ +--- +title: Jsm +description: Available Jsm triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Jsm provides 5 triggers for automating workflows based on events. + +## Triggers + +### JSM Request Commented + +Trigger workflow when a comment is added to a Jira Service Management request + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which service desk requests trigger this workflow using JQL \(Jira Query Language\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, jira:issue_updated, comment_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Issue key \(e.g., SD-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `summary` | string | Request summary/title | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Current status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name \(e.g., Service Request, Incident\) | +| ↳ `id` | string | Issue type ID | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `created` | string | Request creation date \(ISO format\) | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `duedate` | string | Due date for the request | +| ↳ `labels` | array | Array of labels applied to this request | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| `comment` | object | comment output from the tool | +| ↳ `id` | string | Comment ID | +| ↳ `body` | json | Comment body in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Comment author display name | +| ↳ `accountId` | string | Comment author account ID | +| ↳ `emailAddress` | string | Comment author email address | +| ↳ `updateAuthor` | object | updateAuthor output from the tool | +| ↳ `displayName` | string | Display name of the user who last updated the comment | +| ↳ `accountId` | string | Account ID of the user who last updated the comment | +| ↳ `created` | string | Comment creation date \(ISO format\) | +| ↳ `updated` | string | Comment last updated date \(ISO format\) | + + +--- + +### JSM Request Created + +Trigger workflow when a new service request is created in Jira Service Management + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which service desk requests trigger this workflow using JQL \(Jira Query Language\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, jira:issue_updated, comment_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Issue key \(e.g., SD-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `summary` | string | Request summary/title | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Current status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name \(e.g., Service Request, Incident\) | +| ↳ `id` | string | Issue type ID | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `created` | string | Request creation date \(ISO format\) | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `duedate` | string | Due date for the request | +| ↳ `labels` | array | Array of labels applied to this request | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| `issue_event_type_name` | string | Issue event type name from Jira | + + +--- + +### JSM Request Resolved + +Trigger workflow when a service request is resolved in Jira Service Management + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which service desk requests trigger this workflow using JQL \(Jira Query Language\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, jira:issue_updated, comment_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Issue key \(e.g., SD-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `summary` | string | Request summary/title | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Current status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name \(e.g., Service Request, Incident\) | +| ↳ `id` | string | Issue type ID | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `created` | string | Request creation date \(ISO format\) | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `duedate` | string | Due date for the request | +| ↳ `labels` | array | Array of labels applied to this request | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| `issue_event_type_name` | string | Issue event type name from Jira | +| `changelog` | object | changelog output from the tool | +| ↳ `id` | string | Changelog ID | + + +--- + +### JSM Request Updated + +Trigger workflow when a service request is updated in Jira Service Management + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which service desk requests trigger this workflow using JQL \(Jira Query Language\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `webhookEvent` | string | The webhook event type \(e.g., jira:issue_created, jira:issue_updated, comment_created\) | +| `timestamp` | number | Timestamp of the webhook event | +| `user` | object | user output from the tool | +| ↳ `displayName` | string | Display name of the user who triggered the event | +| ↳ `accountId` | string | Account ID of the user who triggered the event | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Jira issue ID | +| ↳ `key` | string | Issue key \(e.g., SD-123\) | +| ↳ `self` | string | REST API URL for this issue | +| ↳ `fields` | object | fields output from the tool | +| ↳ `summary` | string | Request summary/title | +| ↳ `status` | object | status output from the tool | +| ↳ `name` | string | Current status name | +| ↳ `id` | string | Status ID | +| ↳ `statusCategory` | json | Status category information | +| ↳ `priority` | object | priority output from the tool | +| ↳ `name` | string | Priority name | +| ↳ `id` | string | Priority ID | +| ↳ `issuetype` | object | issuetype output from the tool | +| ↳ `name` | string | Issue type name \(e.g., Service Request, Incident\) | +| ↳ `id` | string | Issue type ID | +| ↳ `project` | object | project output from the tool | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `id` | string | Project ID | +| ↳ `reporter` | object | reporter output from the tool | +| ↳ `displayName` | string | Reporter display name | +| ↳ `accountId` | string | Reporter account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `assignee` | object | assignee output from the tool | +| ↳ `displayName` | string | Assignee display name | +| ↳ `accountId` | string | Assignee account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `creator` | object | creator output from the tool | +| ↳ `displayName` | string | Creator display name | +| ↳ `accountId` | string | Creator account ID | +| ↳ `emailAddress` | string | Email address \(Jira Server only — not available in Jira Cloud webhook payloads\) | +| ↳ `created` | string | Request creation date \(ISO format\) | +| ↳ `updated` | string | Last updated date \(ISO format\) | +| ↳ `duedate` | string | Due date for the request | +| ↳ `labels` | array | Array of labels applied to this request | +| ↳ `resolution` | object | resolution output from the tool | +| ↳ `name` | string | Resolution name \(e.g., Done, Fixed\) | +| ↳ `id` | string | Resolution ID | +| `issue_event_type_name` | string | Issue event type name from Jira | +| `changelog` | object | changelog output from the tool | +| ↳ `id` | string | Changelog ID | + + +--- + +### JSM Webhook (All Events) + +Trigger workflow on any Jira Service Management webhook event + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookSecret` | string | No | Optional secret to validate webhook deliveries from Jira using HMAC signature | +| `jqlFilter` | string | No | Filter which service desk requests trigger this workflow using JQL \(Jira Query Language\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `changelog` | object | changelog output from the tool | +| ↳ `id` | string | Changelog ID | +| `comment` | object | comment output from the tool | +| ↳ `id` | string | Comment ID | +| ↳ `body` | string | Comment text/body | +| ↳ `author` | object | author output from the tool | +| ↳ `displayName` | string | Comment author display name | +| ↳ `accountId` | string | Comment author account ID | +| ↳ `emailAddress` | string | Comment author email address | +| ↳ `created` | string | Comment creation date \(ISO format\) | +| ↳ `updated` | string | Comment last updated date \(ISO format\) | + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index d04467626b1..ae727f30f3e 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -27,9 +27,11 @@ "imap", "intercom", "jira", + "jsm", "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..41eebfdb607 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -2522,6 +2522,16 @@ "name": "Confluence Page Moved", "description": "Trigger workflow when a page is moved in Confluence" }, + { + "id": "confluence_page_restored", + "name": "Confluence Page Restored", + "description": "Trigger workflow when a page is restored from trash in Confluence" + }, + { + "id": "confluence_page_permissions_updated", + "name": "Confluence Page Permissions Updated", + "description": "Trigger workflow when page permissions are changed in Confluence" + }, { "id": "confluence_comment_created", "name": "Confluence Comment Created", @@ -2532,6 +2542,11 @@ "name": "Confluence Comment Removed", "description": "Trigger workflow when a comment is removed in Confluence" }, + { + "id": "confluence_comment_updated", + "name": "Confluence Comment Updated", + "description": "Trigger workflow when a comment is updated in Confluence" + }, { "id": "confluence_blog_created", "name": "Confluence Blog Post Created", @@ -2547,6 +2562,11 @@ "name": "Confluence Blog Post Removed", "description": "Trigger workflow when a blog post is removed in Confluence" }, + { + "id": "confluence_blog_restored", + "name": "Confluence Blog Post Restored", + "description": "Trigger workflow when a blog post is restored from trash in Confluence" + }, { "id": "confluence_attachment_created", "name": "Confluence Attachment Created", @@ -2557,6 +2577,11 @@ "name": "Confluence Attachment Removed", "description": "Trigger workflow when an attachment is removed in Confluence" }, + { + "id": "confluence_attachment_updated", + "name": "Confluence Attachment Updated", + "description": "Trigger workflow when an attachment is updated in Confluence" + }, { "id": "confluence_space_created", "name": "Confluence Space Created", @@ -2567,6 +2592,11 @@ "name": "Confluence Space Updated", "description": "Trigger workflow when a space is updated in Confluence" }, + { + "id": "confluence_space_removed", + "name": "Confluence Space Removed", + "description": "Trigger workflow when a space is removed in Confluence" + }, { "id": "confluence_label_added", "name": "Confluence Label Added", @@ -2577,13 +2607,18 @@ "name": "Confluence Label Removed", "description": "Trigger workflow when a label is removed from content in Confluence" }, + { + "id": "confluence_user_created", + "name": "Confluence User Created", + "description": "Trigger workflow when a new user is added to Confluence" + }, { "id": "confluence_webhook", "name": "Confluence Webhook (All Events)", "description": "Trigger workflow on any Confluence webhook event" } ], - "triggerCount": 16, + "triggerCount": 23, "authType": "oauth", "category": "tools", "integrationTypes": ["documents", "productivity", "search"], @@ -6797,18 +6832,63 @@ "name": "Jira Issue Commented", "description": "Trigger workflow when a comment is added to a Jira issue" }, + { + "id": "jira_comment_updated", + "name": "Jira Comment Updated", + "description": "Trigger workflow when a comment is updated on a Jira issue" + }, + { + "id": "jira_comment_deleted", + "name": "Jira Comment Deleted", + "description": "Trigger workflow when a comment is deleted from a Jira issue" + }, { "id": "jira_worklog_created", "name": "Jira Worklog Created", "description": "Trigger workflow when time is logged on a Jira issue" }, + { + "id": "jira_worklog_updated", + "name": "Jira Worklog Updated", + "description": "Trigger workflow when a worklog entry is updated on a Jira issue" + }, + { + "id": "jira_worklog_deleted", + "name": "Jira Worklog Deleted", + "description": "Trigger workflow when a worklog entry is deleted from a Jira issue" + }, + { + "id": "jira_sprint_created", + "name": "Jira Sprint Created", + "description": "Trigger workflow when a sprint is created in Jira" + }, + { + "id": "jira_sprint_started", + "name": "Jira Sprint Started", + "description": "Trigger workflow when a sprint is started in Jira" + }, + { + "id": "jira_sprint_closed", + "name": "Jira Sprint Closed", + "description": "Trigger workflow when a sprint is closed in Jira" + }, + { + "id": "jira_project_created", + "name": "Jira Project Created", + "description": "Trigger workflow when a project is created in Jira" + }, + { + "id": "jira_version_released", + "name": "Jira Version Released", + "description": "Trigger workflow when a version is released in Jira" + }, { "id": "jira_webhook", "name": "Jira Webhook (All Events)", "description": "Trigger workflow on any Jira webhook event" } ], - "triggerCount": 6, + "triggerCount": 15, "authType": "oauth", "category": "tools", "integrationTypes": ["productivity", "customer-support"], @@ -6962,8 +7042,34 @@ } ], "operationCount": 34, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "jsm_request_created", + "name": "JSM Request Created", + "description": "Trigger workflow when a new service request is created in Jira Service Management" + }, + { + "id": "jsm_request_updated", + "name": "JSM Request Updated", + "description": "Trigger workflow when a service request is updated in Jira Service Management" + }, + { + "id": "jsm_request_commented", + "name": "JSM Request Commented", + "description": "Trigger workflow when a comment is added to a Jira Service Management request" + }, + { + "id": "jsm_request_resolved", + "name": "JSM Request Resolved", + "description": "Trigger workflow when a service request is resolved in Jira Service Management" + }, + { + "id": "jsm_webhook", + "name": "JSM Webhook (All Events)", + "description": "Trigger workflow on any Jira Service Management webhook event" + } + ], + "triggerCount": 5, "authType": "oauth", "category": "tools", "integrationTypes": ["customer-support", "developer-tools", "productivity"], @@ -8603,6 +8709,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/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index 5808f2cbb58..63fad11f3a6 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -89,6 +89,7 @@ vi.mock('@/lib/workflows/persistence/utils', () => ({ })) vi.mock('@/lib/workflows/orchestration', () => ({ performChatUndeploy: mockPerformChatUndeploy, + notifySocketDeploymentChanged: vi.fn().mockResolvedValue(undefined), })) vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })), diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index 8cf37410ae0..dc02fff4d26 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -9,7 +9,7 @@ import { getSession } from '@/lib/auth' import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' import { getEmailDomain } from '@/lib/core/utils/urls' -import { performChatUndeploy } from '@/lib/workflows/orchestration' +import { notifySocketDeploymentChanged, performChatUndeploy } from '@/lib/workflows/orchestration' import { deployWorkflow } from '@/lib/workflows/persistence/utils' import { checkChatAccess } from '@/app/api/chat/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' @@ -155,6 +155,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< logger.info( `Redeployed workflow ${existingChat[0].workflowId} for chat update (v${deployResult.version})` ) + await notifySocketDeploymentChanged(existingChat[0].workflowId) } let encryptedPassword diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts index db29c2759de..b42d804203e 100644 --- a/apps/sim/app/api/form/route.ts +++ b/apps/sim/app/api/form/route.ts @@ -10,6 +10,7 @@ import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' import { getEmailDomain } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' +import { notifySocketDeploymentChanged } from '@/lib/workflows/orchestration' import { deployWorkflow } from '@/lib/workflows/persistence/utils' import { checkWorkflowAccessForFormCreation, @@ -152,6 +153,8 @@ export async function POST(request: NextRequest) { `${workflowRecord.isDeployed ? 'Redeployed' : 'Auto-deployed'} workflow ${workflowId} for form (v${result.version})` ) + await notifySocketDeploymentChanged(workflowId) + let encryptedPassword = null if (authType === 'password' && password) { const { encrypted } = await encryptSecret(password) diff --git a/apps/sim/app/api/providers/ollama/models/route.ts b/apps/sim/app/api/providers/ollama/models/route.ts index 44434eadca4..4a676f7e7d9 100644 --- a/apps/sim/app/api/providers/ollama/models/route.ts +++ b/apps/sim/app/api/providers/ollama/models/route.ts @@ -1,11 +1,11 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { env } from '@/lib/core/config/env' +import { getOllamaUrl } from '@/lib/core/utils/urls' import type { ModelsObject } from '@/providers/ollama/types' import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils' const logger = createLogger('OllamaModelsAPI') -const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434' +const OLLAMA_HOST = getOllamaUrl() /** * Get available Ollama models 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/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 81ef1a01ccf..164344640a3 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -53,6 +53,7 @@ vi.mock('@/lib/core/utils/request', () => ({ vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'), + getOllamaUrl: vi.fn().mockReturnValue('http://localhost:11434'), })) vi.mock('@/lib/execution/call-chain', () => ({ diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index 26a63ecdd81..b9b7e91a79a 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -7,6 +7,7 @@ import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence' import { loadWorkflowFromNormalizedTables, @@ -305,8 +306,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`) try { - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - const notifyResponse = await fetch(`${socketUrl}/api/workflow-updated`, { + const notifyResponse = await fetch(`${getSocketServerUrl()}/api/workflow-updated`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 9709beba0c5..8e64e1203fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -157,7 +157,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { } if (block.type === 'text') { - if (!block.content?.trim()) continue + if (!block.content) continue if (block.subagent) { if (group && group.agentName === block.subagent) { group.isDelegating = false diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx index 8534cb8cdb2..83d11ad5a5c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx @@ -293,7 +293,7 @@ function LineChartComponent({ className='inline-block h-[6px] w-[6px] rounded-xs' style={{ backgroundColor: resolvedColors[s.id || ''] || s.color }} /> - {s.label} + {s.label} ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx index 518ad67654b..501d208fb87 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx @@ -224,6 +224,7 @@ export const LogsToolbar = memo(function LogsToolbar({ const [datePickerOpen, setDatePickerOpen] = useState(false) const [previousTimeRange, setPreviousTimeRange] = useState(timeRange) + const dateRangeAppliedRef = useRef(false) const { data: folders = {} } = useFolderMap(workspaceId) const { data: allWorkflowList = [] } = useWorkflows(workspaceId) @@ -249,11 +250,13 @@ export const LogsToolbar = memo(function LogsToolbar({ const statusOptions: ComboboxOption[] = useMemo( () => - (Object.keys(STATUS_CONFIG) as LogStatus[]).map((status) => ({ - value: status, - label: STATUS_CONFIG[status].label, - icon: getColorIcon(STATUS_CONFIG[status].color), - })), + (Object.keys(STATUS_CONFIG) as LogStatus[]) + .filter((status) => STATUS_CONFIG[status].filterable) + .map((status) => ({ + value: status, + label: STATUS_CONFIG[status].label, + icon: getColorIcon(STATUS_CONFIG[status].color), + })), [] ) @@ -305,34 +308,29 @@ export const LogsToolbar = memo(function LogsToolbar({ [setTriggers, workspaceId] ) - const statusDisplayLabel = useMemo(() => { - if (selectedStatuses.length === 0) return 'Status' - if (selectedStatuses.length === 1) { - const status = statusOptions.find((s) => s.value === selectedStatuses[0]) - return status?.label || '1 selected' - } - return `${selectedStatuses.length} selected` - }, [selectedStatuses, statusOptions]) + const statusDisplayLabel = + selectedStatuses.length === 0 + ? 'Status' + : selectedStatuses.length === 1 + ? (statusOptions.find((s) => s.value === selectedStatuses[0])?.label ?? '1 selected') + : `${selectedStatuses.length} selected` - const selectedStatusColor = useMemo(() => { - if (selectedStatuses.length !== 1) return null - const status = selectedStatuses[0] as LogStatus - return STATUS_CONFIG[status]?.color ?? null - }, [selectedStatuses]) + const selectedStatusColor = + selectedStatuses.length === 1 + ? (STATUS_CONFIG[selectedStatuses[0] as LogStatus]?.color ?? null) + : null const workflowOptions: ComboboxOption[] = useMemo( () => workflows.map((w) => ({ value: w.id, label: w.name, icon: getColorIcon(w.color, true) })), [workflows] ) - const workflowDisplayLabel = useMemo(() => { - if (workflowIds.length === 0) return 'Workflow' - if (workflowIds.length === 1) { - const workflow = workflows.find((w) => w.id === workflowIds[0]) - return workflow?.name || '1 selected' - } - return `${workflowIds.length} workflows` - }, [workflowIds, workflows]) + const workflowDisplayLabel = + workflowIds.length === 0 + ? 'Workflow' + : workflowIds.length === 1 + ? (workflows.find((w) => w.id === workflowIds[0])?.name ?? '1 selected') + : `${workflowIds.length} workflows` const selectedWorkflow = workflowIds.length === 1 ? workflows.find((w) => w.id === workflowIds[0]) : null @@ -342,14 +340,12 @@ export const LogsToolbar = memo(function LogsToolbar({ [folderList] ) - const folderDisplayLabel = useMemo(() => { - if (folderIds.length === 0) return 'Folder' - if (folderIds.length === 1) { - const folder = folderList.find((f) => f.id === folderIds[0]) - return folder?.name || '1 selected' - } - return `${folderIds.length} folders` - }, [folderIds, folderList]) + const folderDisplayLabel = + folderIds.length === 0 + ? 'Folder' + : folderIds.length === 1 + ? (folderList.find((f) => f.id === folderIds[0])?.name ?? '1 selected') + : `${folderIds.length} folders` const triggerOptions: ComboboxOption[] = useMemo( () => @@ -361,23 +357,21 @@ export const LogsToolbar = memo(function LogsToolbar({ [] ) - const triggerDisplayLabel = useMemo(() => { - if (triggers.length === 0) return 'Trigger' - if (triggers.length === 1) { - const trigger = triggerOptions.find((t) => t.value === triggers[0]) - return trigger?.label || '1 selected' - } - return `${triggers.length} triggers` - }, [triggers, triggerOptions]) - - const timeDisplayLabel = useMemo(() => { - if (timeRange === 'All time') return 'Time' - if (timeRange === 'Custom range' && startDate && endDate) { - return `${formatDateShort(startDate)} - ${formatDateShort(endDate)}` - } - if (timeRange === 'Custom range') return 'Custom range' - return timeRange - }, [timeRange, startDate, endDate]) + const triggerDisplayLabel = + triggers.length === 0 + ? 'Trigger' + : triggers.length === 1 + ? (triggerOptions.find((t) => t.value === triggers[0])?.label ?? '1 selected') + : `${triggers.length} triggers` + + const timeDisplayLabel = + timeRange === 'All time' + ? 'Time' + : timeRange === 'Custom range' && startDate && endDate + ? `${formatDateShort(startDate)} - ${formatDateShort(endDate)}` + : timeRange === 'Custom range' + ? 'Custom range' + : timeRange /** * Handles time range selection from combobox. @@ -405,6 +399,7 @@ export const LogsToolbar = memo(function LogsToolbar({ */ const handleDateRangeApply = useCallback( (start: string, end: string) => { + dateRangeAppliedRef.current = true setDateRange(start, end) setDatePickerOpen(false) captureEvent(posthogRef.current, 'logs_filter_applied', { @@ -792,42 +787,38 @@ export const LogsToolbar = memo(function LogsToolbar({ /> {/* Timeline Filter */} - - -
- - {timeDisplayLabel} - - } - size='sm' - align='end' - className='h-[32px] w-[120px] rounded-md' - /> -
-
- + {timeDisplayLabel} + } + size='sm' align='end' - sideOffset={4} - collisionPadding={16} - className='w-auto p-0' - > - - -
+ className='h-[32px] w-[120px] rounded-md' + /> + { + if (!isOpen) { + if (dateRangeAppliedRef.current) { + dateRangeAppliedRef.current = false + } else { + handleDatePickerCancel() + } + } + }} + startDate={startDate} + endDate={endDate} + onRangeChange={handleDateRangeApply} + onCancel={handleDatePickerCancel} + /> + diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 90c24c5ee59..fa0373e4444 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -10,9 +10,6 @@ import { Combobox, type ComboboxOption, Download, - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, Library, RefreshCw, } from '@/components/emcn' @@ -361,8 +358,7 @@ export default function Logs() { }) const logs = useMemo(() => { - if (!logsQuery.data?.pages) return [] - return logsQuery.data.pages.flatMap((page) => page.logs) + return logsQuery.data?.pages?.flatMap((page) => page.logs) ?? [] }, [logsQuery.data?.pages]) const sortedLogs = useMemo(() => { @@ -570,18 +566,22 @@ export default function Logs() { const effectiveSidebarOpen = isSidebarOpen && selectedLogIndex !== -1 - const handleRefresh = useCallback(() => { + const triggerVisualRefresh = useCallback(() => { setIsVisuallyRefreshing(true) const timerId = window.setTimeout(() => { setIsVisuallyRefreshing(false) refreshTimersRef.current.delete(timerId) }, REFRESH_SPINNER_DURATION_MS) refreshTimersRef.current.add(timerId) + }, []) + + const handleRefresh = useCallback(() => { + triggerVisualRefresh() logsRefetchRef.current() if (selectedLogIdRef.current) { activeLogRefetchRef.current() } - }, []) + }, [triggerVisualRefresh]) const prevIsFetchingRef = useRef(logsQuery.isFetching) useEffect(() => { @@ -590,14 +590,9 @@ export default function Logs() { prevIsFetchingRef.current = isFetching if (isLive && !wasFetching && isFetching) { - setIsVisuallyRefreshing(true) - const timerId = window.setTimeout(() => { - setIsVisuallyRefreshing(false) - refreshTimersRef.current.delete(timerId) - }, REFRESH_SPINNER_DURATION_MS) - refreshTimersRef.current.add(timerId) + triggerVisualRefresh() } - }, [logsQuery.isFetching, isLive]) + }, [logsQuery.isFetching, isLive, triggerVisualRefresh]) const handleExport = useCallback(async () => { setIsExporting(true) @@ -898,6 +893,11 @@ export default function Logs() { setSearchQuery(fullQuery) }, []) + const getSuggestions = useCallback( + (input: string) => suggestionEngine.getSuggestions(input), + [suggestionEngine] + ) + const { appliedFilters, currentInput, @@ -920,7 +920,7 @@ export default function Logs() { initializeFromQuery, } = useSearchState({ onFiltersChange: handleFiltersChange, - getSuggestions: (input) => suggestionEngine.getSuggestions(input), + getSuggestions, }) const lastExternalSearchValue = useRef(searchQuery) @@ -1259,6 +1259,7 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr const [datePickerOpen, setDatePickerOpen] = useState(false) const [previousTimeRange, setPreviousTimeRange] = useState(timeRange) + const dateRangeAppliedRef = useRef(false) const { data: folders = {} } = useFolderMap(workspaceId) const { data: allWorkflowList = [] } = useWorkflows(workspaceId) @@ -1269,11 +1270,13 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr const statusOptions: ComboboxOption[] = useMemo( () => - (Object.keys(STATUS_CONFIG) as LogStatus[]).map((status) => ({ - value: status, - label: STATUS_CONFIG[status].label, - icon: getColorIcon(STATUS_CONFIG[status].color), - })), + (Object.keys(STATUS_CONFIG) as LogStatus[]) + .filter((status) => STATUS_CONFIG[status].filterable) + .map((status) => ({ + value: status, + label: STATUS_CONFIG[status].label, + icon: getColorIcon(STATUS_CONFIG[status].color), + })), [] ) @@ -1355,6 +1358,7 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr } const handleDateRangeApply = (start: string, end: string) => { + dateRangeAppliedRef.current = true setDateRange(start, end) setDatePickerOpen(false) } @@ -1482,39 +1486,37 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
Time Range - - -
- {timeDisplayLabel} +
+ {timeDisplayLabel} + } + size='sm' + className='h-[32px] w-full rounded-md' + /> + { + if (!isOpen) { + if (dateRangeAppliedRef.current) { + dateRangeAppliedRef.current = false + } else { + handleDatePickerCancel() } - size='sm' - className='h-[32px] w-full rounded-md' - /> -
- - - - - + } + }} + startDate={startDate} + endDate={endDate} + onRangeChange={handleDateRangeApply} + onCancel={handleDatePickerCancel} + /> +
{filtersActive && ( diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index 1f77a590435..27cc823a195 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -55,14 +55,25 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus { export const STATUS_CONFIG: Record< LogStatus, - { variant: React.ComponentProps['variant']; label: string; color: string } + { + variant: React.ComponentProps['variant'] + label: string + color: string + /** Whether this status appears as a filter option. Intermediary states (e.g. cancelling) are excluded. */ + filterable: boolean + } > = { - error: { variant: 'red', label: 'Error', color: 'var(--text-error)' }, - pending: { variant: 'amber', label: 'Pending', color: '#f59e0b' }, - running: { variant: 'amber', label: 'Running', color: '#f59e0b' }, - cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b' }, - cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316' }, - info: { variant: 'gray', label: 'Info', color: 'var(--terminal-status-info-color)' }, + error: { variant: 'red', label: 'Error', color: 'var(--text-error)', filterable: true }, + pending: { variant: 'amber', label: 'Pending', color: '#f59e0b', filterable: true }, + running: { variant: 'amber', label: 'Running', color: '#f59e0b', filterable: true }, + cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b', filterable: false }, + cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316', filterable: true }, + info: { + variant: 'gray', + label: 'Info', + color: 'var(--terminal-status-info-color)', + filterable: true, + }, } const TRIGGER_VARIANT_MAP: Record['variant']> = { @@ -87,16 +98,14 @@ interface StatusBadgeProps { * @param props - Component props containing the status * @returns A Badge with dot indicator and status label */ -export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => { +export function StatusBadge({ status }: StatusBadgeProps) { const config = STATUS_CONFIG[status] return React.createElement( Badge, { variant: config.variant, dot: true, size: 'sm' }, config.label ) -}) - -StatusBadge.displayName = 'StatusBadge' +} interface TriggerBadgeProps { trigger: string @@ -108,7 +117,7 @@ interface TriggerBadgeProps { * @param props - Component props containing the trigger type * @returns A Badge with appropriate styling for the trigger type */ -export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => { +export function TriggerBadge({ trigger }: TriggerBadgeProps) { const metadata = getIntegrationMetadata(trigger) const isIntegration = !(CORE_TRIGGER_TYPES as readonly string[]).includes(trigger) const block = isIntegration ? getBlock(trigger) : null @@ -116,21 +125,32 @@ export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => { const coreVariant = TRIGGER_VARIANT_MAP[trigger] if (coreVariant) { - return React.createElement(Badge, { variant: coreVariant, size: 'sm' }, metadata.label) + return React.createElement( + Badge, + { variant: coreVariant, size: 'sm', className: 'whitespace-nowrap' }, + metadata.label + ) } if (IconComponent) { return React.createElement( Badge, - { variant: 'gray-secondary', size: 'sm', icon: IconComponent }, + { + variant: 'gray-secondary', + size: 'sm', + icon: IconComponent, + className: 'whitespace-nowrap', + }, metadata.label ) } - return React.createElement(Badge, { variant: 'gray-secondary', size: 'sm' }, metadata.label) -}) - -TriggerBadge.displayName = 'TriggerBadge' + return React.createElement( + Badge, + { variant: 'gray-secondary', size: 'sm', className: 'whitespace-nowrap' }, + metadata.label + ) +} interface LogWithDuration { totalDurationMs?: number | string diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx index 6760a73fd7a..c74249354e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx @@ -244,16 +244,15 @@ export function ImportCsvDialog({ } }, [mapping, parsed?.headers, table.schema.columns]) - const appendCapacityDeficit = useMemo(() => { - if (!parsed || mode !== 'append') return 0 - const projected = table.rowCount + parsed.totalRows - return projected > table.maxRows ? projected - table.maxRows : 0 - }, [mode, parsed, table.maxRows, table.rowCount]) + const appendCapacityDeficit = + parsed && mode === 'append' && table.rowCount + parsed.totalRows > table.maxRows + ? table.rowCount + parsed.totalRows - table.maxRows + : 0 - const replaceCapacityDeficit = useMemo(() => { - if (!parsed || mode !== 'replace') return 0 - return parsed.totalRows > table.maxRows ? parsed.totalRows - table.maxRows : 0 - }, [mode, parsed, table.maxRows]) + const replaceCapacityDeficit = + parsed && mode === 'replace' && parsed.totalRows > table.maxRows + ? parsed.totalRows - table.maxRows + : 0 const canSubmit = parsed !== null && @@ -264,12 +263,11 @@ export function ImportCsvDialog({ appendCapacityDeficit === 0 && replaceCapacityDeficit === 0 - const importCsv = importMutation.mutateAsync const handleSubmit = useCallback(async () => { if (!parsed || !canSubmit) return setSubmitError(null) try { - const result = await importCsv({ + const result = await importMutation.mutateAsync({ workspaceId, tableId: table.id, file: parsed.file, @@ -294,9 +292,9 @@ export function ImportCsvDialog({ setSubmitError(summarizeImportError(message)) logger.error('CSV import into existing table failed', err) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ canSubmit, - importCsv, mapping, mode, onImported, @@ -363,7 +361,7 @@ export function ImportCsvDialog({
- + {parsed.file.name} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 43b66cde526..b9dc27ba82a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -604,9 +604,9 @@ const SubBlockRow = memo(function SubBlockRow({ const { data: workflowMapForLookup = {} } = useWorkflowMap(workspaceId) const workflowSelectionName = useMemo(() => { - if (subBlock?.id !== 'workflowId' || typeof rawValue !== 'string') return null + if (subBlock?.type !== 'workflow-selector' || typeof rawValue !== 'string') return null return workflowMapForLookup[rawValue]?.name ?? null - }, [workflowMapForLookup, subBlock?.id, rawValue]) + }, [workflowMapForLookup, subBlock?.type, rawValue]) const { data: mcpServers = [] } = useMcpServers(workspaceId || '') const mcpServerDisplayName = useMemo(() => { @@ -632,12 +632,12 @@ const SubBlockRow = memo(function SubBlockRow({ const { data: tables = [] } = useTablesList(workspaceId || '') const tableDisplayName = useMemo(() => { - if (subBlock?.id !== 'tableId' || typeof rawValue !== 'string') { + if (subBlock?.type !== 'table-selector' || typeof rawValue !== 'string') { return null } const table = tables.find((t) => t.id === rawValue) return table?.name ?? null - }, [subBlock?.id, rawValue, tables]) + }, [subBlock?.type, rawValue, tables]) const webhookUrlDisplayValue = useMemo(() => { if (!subBlock?.id?.startsWith('webhookUrlDisplay') || !blockId) { diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index 638e0cda063..7af310952bd 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -13,7 +13,7 @@ import { import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import type { Socket } from 'socket.io-client' -import { getEnv } from '@/lib/core/config/env' +import { getSocketUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { type SocketJoinCommand, @@ -102,6 +102,7 @@ interface SocketContextType { onWorkflowDeleted: (handler: (data: any) => void) => void onWorkflowReverted: (handler: (data: any) => void) => void onWorkflowUpdated: (handler: (data: any) => void) => void + onWorkflowDeployed: (handler: (data: any) => void) => void onOperationConfirmed: (handler: (data: any) => void) => void onOperationFailed: (handler: (data: any) => void) => void } @@ -132,6 +133,7 @@ const SocketContext = createContext({ onWorkflowDeleted: () => {}, onWorkflowReverted: () => {}, onWorkflowUpdated: () => {}, + onWorkflowDeployed: () => {}, onOperationConfirmed: () => {}, onOperationFailed: () => {}, }) @@ -176,6 +178,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { workflowDeleted?: (data: any) => void workflowReverted?: (data: any) => void workflowUpdated?: (data: any) => void + workflowDeployed?: (data: any) => void operationConfirmed?: (data: any) => void operationFailed?: (data: any) => void }>({}) @@ -337,7 +340,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { const initializeSocket = async () => { try { const { io } = await import('socket.io-client') - const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002' + const socketUrl = getSocketUrl() logger.info('Attempting to connect to Socket.IO server', { url: socketUrl, @@ -550,6 +553,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) { eventHandlers.current.workflowUpdated?.(data) }) + socketInstance.on('workflow-deployed', (data) => { + logger.info(`Workflow ${data.workflowId} deployment state changed`) + eventHandlers.current.workflowDeployed?.(data) + }) + const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => { const [ { useOperationQueueStore }, @@ -994,6 +1002,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) { eventHandlers.current.workflowUpdated = handler }, []) + const onWorkflowDeployed = useCallback((handler: (data: any) => void) => { + eventHandlers.current.workflowDeployed = handler + }, []) + const onOperationConfirmed = useCallback((handler: (data: any) => void) => { eventHandlers.current.operationConfirmed = handler }, []) @@ -1029,6 +1041,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, }), @@ -1058,6 +1071,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, ] diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index e66efd70a08..81ab5c02c7a 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -979,17 +979,24 @@ export const ConfluenceV2Block: BlockConfig = { ...getTrigger('confluence_page_updated').subBlocks, ...getTrigger('confluence_page_removed').subBlocks, ...getTrigger('confluence_page_moved').subBlocks, + ...getTrigger('confluence_page_restored').subBlocks, + ...getTrigger('confluence_page_permissions_updated').subBlocks, ...getTrigger('confluence_comment_created').subBlocks, ...getTrigger('confluence_comment_removed').subBlocks, + ...getTrigger('confluence_comment_updated').subBlocks, ...getTrigger('confluence_blog_created').subBlocks, ...getTrigger('confluence_blog_updated').subBlocks, ...getTrigger('confluence_blog_removed').subBlocks, + ...getTrigger('confluence_blog_restored').subBlocks, ...getTrigger('confluence_attachment_created').subBlocks, ...getTrigger('confluence_attachment_removed').subBlocks, + ...getTrigger('confluence_attachment_updated').subBlocks, ...getTrigger('confluence_space_created').subBlocks, ...getTrigger('confluence_space_updated').subBlocks, + ...getTrigger('confluence_space_removed').subBlocks, ...getTrigger('confluence_label_added').subBlocks, ...getTrigger('confluence_label_removed').subBlocks, + ...getTrigger('confluence_user_created').subBlocks, ...getTrigger('confluence_webhook').subBlocks, ], triggers: { @@ -999,17 +1006,24 @@ export const ConfluenceV2Block: BlockConfig = { 'confluence_page_updated', 'confluence_page_removed', 'confluence_page_moved', + 'confluence_page_restored', + 'confluence_page_permissions_updated', 'confluence_comment_created', 'confluence_comment_removed', + 'confluence_comment_updated', 'confluence_blog_created', 'confluence_blog_updated', 'confluence_blog_removed', + 'confluence_blog_restored', 'confluence_attachment_created', 'confluence_attachment_removed', + 'confluence_attachment_updated', 'confluence_space_created', 'confluence_space_updated', + 'confluence_space_removed', 'confluence_label_added', 'confluence_label_removed', + 'confluence_user_created', 'confluence_webhook', ], }, diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index b1eaf339d14..e1d8e6d206b 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -702,7 +702,16 @@ Return ONLY the comment text - no explanations.`, ...getTrigger('jira_issue_updated').subBlocks, ...getTrigger('jira_issue_deleted').subBlocks, ...getTrigger('jira_issue_commented').subBlocks, + ...getTrigger('jira_comment_updated').subBlocks, + ...getTrigger('jira_comment_deleted').subBlocks, ...getTrigger('jira_worklog_created').subBlocks, + ...getTrigger('jira_worklog_updated').subBlocks, + ...getTrigger('jira_worklog_deleted').subBlocks, + ...getTrigger('jira_sprint_created').subBlocks, + ...getTrigger('jira_sprint_started').subBlocks, + ...getTrigger('jira_sprint_closed').subBlocks, + ...getTrigger('jira_project_created').subBlocks, + ...getTrigger('jira_version_released').subBlocks, ...getTrigger('jira_webhook').subBlocks, ], tools: { @@ -1268,6 +1277,9 @@ Return ONLY the comment text - no explanations.`, time_spent: { type: 'string', description: 'Time spent (for worklog events)' }, changelog: { type: 'json', description: 'Changelog object (for update events)' }, issue: { type: 'json', description: 'Complete issue object from webhook' }, + sprint: { type: 'json', description: 'Sprint object (for sprint events)' }, + project: { type: 'json', description: 'Project object (for project events)' }, + version: { type: 'json', description: 'Version object (for version events)' }, jira: { type: 'json', description: 'Complete webhook payload' }, user: { type: 'json', description: 'User object who triggered the event' }, webhook: { type: 'json', description: 'Webhook metadata' }, @@ -1279,7 +1291,16 @@ Return ONLY the comment text - no explanations.`, 'jira_issue_updated', 'jira_issue_deleted', 'jira_issue_commented', + 'jira_comment_updated', + 'jira_comment_deleted', 'jira_worklog_created', + 'jira_worklog_updated', + 'jira_worklog_deleted', + 'jira_sprint_created', + 'jira_sprint_started', + 'jira_sprint_closed', + 'jira_project_created', + 'jira_version_released', 'jira_webhook', ], }, diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 9c75d4c0e4b..3a9ed8c07a1 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -3,6 +3,7 @@ import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import type { JsmResponse } from '@/tools/jsm/types' +import { getTrigger } from '@/triggers' export const JiraServiceManagementBlock: BlockConfig = { type: 'jira_service_management', @@ -564,6 +565,11 @@ Return ONLY the comment text - no explanations.`, ], }, }, + ...getTrigger('jsm_request_created').subBlocks, + ...getTrigger('jsm_request_updated').subBlocks, + ...getTrigger('jsm_request_commented').subBlocks, + ...getTrigger('jsm_request_resolved').subBlocks, + ...getTrigger('jsm_webhook').subBlocks, ], tools: { access: [ @@ -1246,4 +1252,14 @@ Return ONLY the comment text - no explanations.`, sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' }, targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' }, }, + triggers: { + enabled: true, + available: [ + 'jsm_request_created', + 'jsm_request_updated', + 'jsm_request_commented', + 'jsm_request_resolved', + 'jsm_webhook', + ], + }, } 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/executor/utils/block-reference.test.ts b/apps/sim/executor/utils/block-reference.test.ts index 470522b772e..72ec514765f 100644 --- a/apps/sim/executor/utils/block-reference.test.ts +++ b/apps/sim/executor/utils/block-reference.test.ts @@ -162,7 +162,11 @@ describe('resolveBlockReference', () => { expect(result).toEqual({ value: undefined, blockId: 'block-1' }) }) - it('should validate path when block has no output yet', () => { + it('should not validate path when block has no output yet', () => { + // Blocks with no output typically live on a branched path that wasn't + // taken this run. We resolve such references to undefined (which the + // caller maps to RESOLVED_EMPTY) rather than throwing on every nested + // path the schema doesn't pre-declare. const ctx = createContext({ blockData: {}, blockOutputSchemas: { @@ -170,7 +174,8 @@ describe('resolveBlockReference', () => { }, }) - expect(() => resolveBlockReference('start', ['invalid'], ctx)).toThrow(InvalidFieldError) + const result = resolveBlockReference('start', ['invalid'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) }) it('should return undefined for valid field when block has no output', () => { @@ -184,6 +189,57 @@ describe('resolveBlockReference', () => { const result = resolveBlockReference('start', ['input'], ctx) expect(result).toEqual({ value: undefined, blockId: 'block-1' }) }) + + it('should return undefined for nested path under json field when block has no output', () => { + // Repro for the branched-path bug: a function block with a dynamic + // `json` result that never ran should resolve to undefined regardless + // of the nested path, not throw. + const ctx = createContext({ + blockData: {}, + blockOutputSchemas: { + 'block-1': { + result: { type: 'json' }, + stdout: { type: 'string' }, + }, + }, + }) + + const result = resolveBlockReference('start', ['result', 'summary'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + + it('should not throw for nested path under json field on executed block', () => { + // A `json` field declares dynamic shape, so drilling into it must be + // permitted even when the runtime data doesn't happen to include that + // key on this run. + const ctx = createContext({ + blockData: { 'block-1': { result: { foo: 1 } } }, + blockOutputSchemas: { + 'block-1': { + result: { type: 'json' }, + stdout: { type: 'string' }, + }, + }, + }) + + const result = resolveBlockReference('start', ['result', 'summary'], ctx) + expect(result).toEqual({ value: undefined, blockId: 'block-1' }) + }) + + it('should resolve values nested under json field on executed block', () => { + const ctx = createContext({ + blockData: { 'block-1': { result: { summary: 'hello' } } }, + blockOutputSchemas: { + 'block-1': { + result: { type: 'json' }, + stdout: { type: 'string' }, + }, + }, + }) + + const result = resolveBlockReference('start', ['result', 'summary'], ctx) + expect(result).toEqual({ value: 'hello', blockId: 'block-1' }) + }) }) describe('without schema (pass-through mode)', () => { diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 4ae41a2b122..e1a8b2200ce 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -2,7 +2,18 @@ import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types' import { normalizeName } from '@/executor/constants' import { navigatePath } from '@/executor/variables/resolvers/reference' -export type OutputSchema = Record +/** + * A single schema node encountered while walking an `OutputSchema`. Captures + * only the fields this module inspects — not a full schema type. + */ +interface SchemaNode { + type?: string + description?: string + properties?: unknown + items?: unknown +} + +export type OutputSchema = Record export interface BlockReferenceContext { blockNameMapping: Record @@ -29,25 +40,26 @@ export class InvalidFieldError extends Error { } } +function asSchemaNode(value: unknown): SchemaNode | undefined { + if (typeof value !== 'object' || value === null) return undefined + return value as SchemaNode +} + function isFileType(value: unknown): boolean { - if (typeof value !== 'object' || value === null) return false - const typed = value as { type?: string } - return typed.type === 'file' || typed.type === 'file[]' + const node = asSchemaNode(value) + return node?.type === 'file' || node?.type === 'file[]' } function isArrayType(value: unknown): value is { type: 'array'; items?: unknown } { - if (typeof value !== 'object' || value === null) return false - return (value as { type?: string }).type === 'array' + return asSchemaNode(value)?.type === 'array' } function getArrayItems(schema: unknown): unknown { - if (typeof schema !== 'object' || schema === null) return undefined - return (schema as { items?: unknown }).items + return asSchemaNode(schema)?.items } function getProperties(schema: unknown): Record | undefined { - if (typeof schema !== 'object' || schema === null) return undefined - const props = (schema as { properties?: unknown }).properties + const props = asSchemaNode(schema)?.properties return typeof props === 'object' && props !== null ? (props as Record) : undefined @@ -69,6 +81,19 @@ function lookupField(schema: unknown, fieldName: string): unknown | undefined { return undefined } +function isOpaqueSchemaNode(value: unknown): boolean { + const node = asSchemaNode(value) + if (!node) return false + // A schema node whose nested shape isn't enumerated. Any path beneath it + // is accepted because there's no declared structure to validate against. + // `object` / `json` with declared `properties` are walked via lookupField. + if (node.type === 'any') return true + if ((node.type === 'json' || node.type === 'object') && node.properties === undefined) { + return true + } + return false +} + function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): boolean { if (!schema || pathParts.length === 0) { return true @@ -83,6 +108,10 @@ function isPathInSchema(schema: OutputSchema | undefined, pathParts: string[]): return false } + if (isOpaqueSchemaNode(current)) { + return true + } + if (/^\d+$/.test(part)) { if (isFileType(current)) { const nextPart = pathParts[i + 1] @@ -183,14 +212,12 @@ export function resolveBlockReference( } const blockOutput = context.blockData[blockId] - const schema = context.blockOutputSchemas?.[blockId] + // When the block has not produced any output (e.g. it lives on a branched + // path that wasn't taken), resolve the reference to undefined without + // validating against the declared schema. Callers map this to an empty + // value so that references to skipped blocks don't fail the workflow. if (blockOutput === undefined) { - if (schema && pathParts.length > 0) { - if (!isPathInSchema(schema, pathParts)) { - throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema)) - } - } return { value: undefined, blockId } } @@ -200,6 +227,7 @@ export function resolveBlockReference( const value = navigatePath(blockOutput, pathParts) + const schema = context.blockOutputSchemas?.[blockId] if (value === undefined && schema) { if (!isPathInSchema(schema, pathParts)) { throw new InvalidFieldError(blockName, pathParts.join('.'), getSchemaFieldNames(schema)) diff --git a/apps/sim/executor/utils/subflow-utils.test.ts b/apps/sim/executor/utils/subflow-utils.test.ts new file mode 100644 index 00000000000..18f7e2097d8 --- /dev/null +++ b/apps/sim/executor/utils/subflow-utils.test.ts @@ -0,0 +1,86 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { ExecutionContext } from '@/executor/types' +import type { VariableResolver } from '@/executor/variables/resolver' +import { resolveArrayInput } from './subflow-utils' + +describe('resolveArrayInput', () => { + const fakeCtx = {} as unknown as ExecutionContext + + it('returns arrays as-is', () => { + expect(resolveArrayInput(fakeCtx, [1, 2, 3], null)).toEqual([1, 2, 3]) + }) + + it('converts plain objects to entries', () => { + expect(resolveArrayInput(fakeCtx, { a: 1, b: 2 }, null)).toEqual([ + ['a', 1], + ['b', 2], + ]) + }) + + it('returns empty array when a pure reference resolves to null (skipped block)', () => { + // `resolveSingleReference` returns `null` for a reference that points at a + // block that exists in the workflow but did not execute on this path. + // A loop/parallel over such a reference should run zero iterations rather + // than fail the workflow. + const resolver = { + resolveSingleReference: vi.fn().mockReturnValue(null), + } as unknown as VariableResolver + + const result = resolveArrayInput(fakeCtx, '', resolver) + + expect(result).toEqual([]) + expect(resolver.resolveSingleReference).toHaveBeenCalled() + }) + + it('returns the array from a pure reference that resolved to an array', () => { + const resolver = { + resolveSingleReference: vi.fn().mockReturnValue([1, 2, 3]), + } as unknown as VariableResolver + + expect(resolveArrayInput(fakeCtx, '', resolver)).toEqual([1, 2, 3]) + }) + + it('converts resolved objects to entries', () => { + const resolver = { + resolveSingleReference: vi.fn().mockReturnValue({ x: 1, y: 2 }), + } as unknown as VariableResolver + + expect(resolveArrayInput(fakeCtx, '', resolver)).toEqual([ + ['x', 1], + ['y', 2], + ]) + }) + + it('throws when a pure reference resolves to a non-array, non-object, non-null value', () => { + const resolver = { + resolveSingleReference: vi.fn().mockReturnValue(42), + } as unknown as VariableResolver + + expect(() => resolveArrayInput(fakeCtx, '', resolver)).toThrow( + /did not resolve to an array or object/ + ) + }) + + it('throws when a pure reference resolves to undefined (unknown block)', () => { + // `undefined` means the reference could not be matched to any block at + // all (typo / deleted block). This must still fail loudly. + const resolver = { + resolveSingleReference: vi.fn().mockReturnValue(undefined), + } as unknown as VariableResolver + + expect(() => resolveArrayInput(fakeCtx, '', resolver)).toThrow( + /did not resolve to an array or object/ + ) + }) + + it('parses a JSON array string', () => { + expect(resolveArrayInput(fakeCtx, '[1, 2, 3]', null)).toEqual([1, 2, 3]) + }) + + it('throws on a string that is neither a reference nor valid JSON array/object', () => { + expect(() => resolveArrayInput(fakeCtx, 'not json', null)).toThrow() + }) +}) diff --git a/apps/sim/executor/utils/subflow-utils.ts b/apps/sim/executor/utils/subflow-utils.ts index 89a1e7b6948..99570329bf5 100644 --- a/apps/sim/executor/utils/subflow-utils.ts +++ b/apps/sim/executor/utils/subflow-utils.ts @@ -216,6 +216,9 @@ export function resolveArrayInput( if (typeof resolved === 'object' && resolved !== null) { return Object.entries(resolved) } + if (resolved === null) { + return [] + } throw new Error(`Reference "${items}" did not resolve to an array or object`) } catch (error) { if (error instanceof Error && error.message.startsWith('Reference "')) { diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index 2529e6bb228..4c943f297ab 100644 --- a/apps/sim/executor/variables/resolvers/block.test.ts +++ b/apps/sim/executor/variables/resolvers/block.test.ts @@ -383,6 +383,47 @@ describe('BlockResolver', () => { expect(resolver.resolve('', ctx)).toBe(RESOLVED_EMPTY) }) + it.concurrent( + 'should return RESOLVED_EMPTY for nested json path on function block that did not execute', + () => { + // Repro for the branched-trigger CRM workflow bug: a function block + // on an untaken branch is referenced via a nested path under its + // `result` (declared `type: 'json'`). The resolver must not validate + // the path against the declared top-level schema keys in this case. + const workflow = createTestWorkflow([ + { id: 'normalize-email', name: 'NormalizeEmail', type: 'function' }, + { id: 'normalize-calendar', name: 'NormalizeCalendar', type: 'function' }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', { + 'normalize-email': { result: { summary: 'email summary' }, stdout: '' }, + }) + + expect(resolver.resolve('', ctx)).toBe('email summary') + expect(resolver.resolve('', ctx)).toBe(RESOLVED_EMPTY) + expect(resolver.resolve('', ctx)).toBe(RESOLVED_EMPTY) + } + ) + + it.concurrent( + 'should return RESOLVED_EMPTY for nested json path on executed block when data is missing', + () => { + // Even for a block that ran, drilling into a `json`-typed field that + // the runtime output didn't include should resolve to empty rather + // than throw — `json` explicitly means dynamic shape. + const workflow = createTestWorkflow([ + { id: 'normalize-email', name: 'NormalizeEmail', type: 'function' }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', { + 'normalize-email': { result: { subject: 'hi' }, stdout: '' }, + }) + + expect(resolver.resolve('', ctx)).toBe('hi') + expect(resolver.resolve('', ctx)).toBe(RESOLVED_EMPTY) + } + ) + it.concurrent('should fall back to context blockStates', () => { const workflow = createTestWorkflow([{ id: 'source' }]) const resolver = new BlockResolver(workflow) diff --git a/apps/sim/hooks/queries/deployments.ts b/apps/sim/hooks/queries/deployments.ts index a0a559ce802..6e7498f13b6 100644 --- a/apps/sim/hooks/queries/deployments.ts +++ b/apps/sim/hooks/queries/deployments.ts @@ -42,6 +42,8 @@ export function invalidateDeploymentQueries(queryClient: QueryClient, workflowId queryClient.invalidateQueries({ queryKey: deploymentKeys.info(workflowId) }), queryClient.invalidateQueries({ queryKey: deploymentKeys.deployedState(workflowId) }), queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) }), + queryClient.invalidateQueries({ queryKey: deploymentKeys.chatStatus(workflowId) }), + queryClient.invalidateQueries({ queryKey: deploymentKeys.formStatus(workflowId) }), ]) } diff --git a/apps/sim/hooks/queries/tables.ts b/apps/sim/hooks/queries/tables.ts index 9f67d50e9a3..282761df980 100644 --- a/apps/sim/hooks/queries/tables.ts +++ b/apps/sim/hooks/queries/tables.ts @@ -5,7 +5,15 @@ import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from '@/components/emcn' -import type { Filter, RowData, Sort, TableDefinition, TableMetadata, TableRow } from '@/lib/table' +import type { + CsvHeaderMapping, + Filter, + RowData, + Sort, + TableDefinition, + TableMetadata, + TableRow, +} from '@/lib/table' const logger = createLogger('TableQueries') @@ -780,7 +788,6 @@ export function useUploadCsvToTable() { }) } -export type CsvHeaderMapping = Record export type CsvImportMode = 'append' | 'replace' interface ImportCsvIntoTableParams { 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/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index aa1433f7ae6..ae1b2fe90f6 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' import { createLogger } from '@sim/logger' +import { useQueryClient } from '@tanstack/react-query' import type { Edge } from 'reactflow' import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' @@ -7,6 +8,7 @@ import { generateId } from '@/lib/core/utils/uuid' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' +import { invalidateDeploymentQueries } from '@/hooks/queries/deployments' import { useUndoRedo } from '@/hooks/use-undo-redo' import { BLOCK_OPERATIONS, @@ -34,6 +36,7 @@ import { findAllDescendantNodes, isBlockProtected } from '@/stores/workflows/wor const logger = createLogger('CollaborativeWorkflow') export function useCollaborativeWorkflow() { + const queryClient = useQueryClient() const undoRedo = useUndoRedo() const isUndoRedoInProgress = useRef(false) const lastDiffOperationId = useRef(null) @@ -125,6 +128,7 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, } = useSocket() @@ -645,6 +649,15 @@ export function useCollaborativeWorkflow() { } } + const handleWorkflowDeployed = (data: any) => { + const { workflowId } = data + logger.info(`Workflow ${workflowId} deployment state changed`) + + if (workflowId !== activeWorkflowId) return + + invalidateDeploymentQueries(queryClient, workflowId) + } + const handleOperationConfirmed = (data: any) => { const { operationId } = data logger.debug('Operation confirmed', { operationId }) @@ -664,6 +677,7 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted(handleWorkflowDeleted) onWorkflowReverted(handleWorkflowReverted) onWorkflowUpdated(handleWorkflowUpdated) + onWorkflowDeployed(handleWorkflowDeployed) onOperationConfirmed(handleOperationConfirmed) onOperationFailed(handleOperationFailed) }, [ @@ -673,9 +687,11 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, activeWorkflowId, + queryClient, confirmOperation, failOperation, emitWorkflowOperation, 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/billing/constants.ts b/apps/sim/lib/billing/constants.ts index 1838e7da1e5..d9a3c391540 100644 --- a/apps/sim/lib/billing/constants.ts +++ b/apps/sim/lib/billing/constants.ts @@ -32,7 +32,7 @@ export const SEARCH_TOOL_COST = 0.01 * Default threshold (in dollars) for incremental overage billing * When unbilled overage reaches this amount, an invoice item is created */ -export const DEFAULT_OVERAGE_THRESHOLD = 50 +export const DEFAULT_OVERAGE_THRESHOLD = 100 /** * Available credit tiers. Each tier maps a credit amount to the underlying dollar cost. diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 95538651008..8a64b57a766 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['context_write']: { + context_write: { parameters: { type: 'object', properties: { @@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_job']: { + create_job: { parameters: { type: 'object', properties: { @@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['debug']: { + debug: { parameters: { properties: { context: { @@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_visualization']: { + generate_visualization: { parameters: { type: 'object', properties: { @@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_version']: { + get_deployment_version: { parameters: { type: 'object', properties: { @@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_execution_summary']: { + get_execution_summary: { parameters: { type: 'object', properties: { @@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_logs']: { + get_workflow_logs: { parameters: { type: 'object', properties: { @@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['revert_to_version']: { + revert_to_version: { parameters: { type: 'object', properties: { @@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['tool_search_tool_regex']: { + tool_search_tool_regex: { parameters: { properties: { case_insensitive: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 4168e681bf5..bb408783036 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -6,6 +6,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow' import { @@ -147,8 +148,7 @@ function findDescendants(containerId: string, blocksById: Record logger.info('Workflow state persisted to database', { workflowId }) - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - fetch(`${socketUrl}/api/workflow-updated`, { + fetch(`${getSocketServerUrl()}/api/workflow-updated`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 2c7ef8bb691..2d3ca878044 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -72,7 +72,7 @@ export const env = createEnv({ STRIPE_PRICE_TEAM_25_YR: z.string().min(1).optional(), // Team Pro: $255/seat/yr STRIPE_PRICE_TEAM_100_MO: z.string().min(1).optional(), // Team Max: $100/seat/mo STRIPE_PRICE_TEAM_100_YR: z.string().min(1).optional(), // Team Max: $1,020/seat/yr - OVERAGE_THRESHOLD_DOLLARS: z.number().optional().default(50), // Dollar threshold for incremental overage billing (default: $50) + OVERAGE_THRESHOLD_DOLLARS: z.number().optional().default(100), // Dollar threshold for incremental overage billing (default: $100) // Email & Communication EMAIL_VERIFICATION_ENABLED: z.boolean().optional(), // Enable email verification for user registration and login (defaults to false) @@ -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/idempotency/service.ts b/apps/sim/lib/core/idempotency/service.ts index b9920f49e81..5f19d4c89de 100644 --- a/apps/sim/lib/core/idempotency/service.ts +++ b/apps/sim/lib/core/idempotency/service.ts @@ -454,6 +454,7 @@ export class IdempotencyService { normalizedHeaders?.['linear-delivery'] || normalizedHeaders?.['greenhouse-event-id'] || normalizedHeaders?.['x-zm-request-id'] || + normalizedHeaders?.['x-atlassian-webhook-identifier'] || normalizedHeaders?.['idempotency-key'] if (webhookIdHeader) { diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index 9420878e6d5..073a0c35f97 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -3,8 +3,19 @@ import { isDev, isHosted, isReactGrabEnabled } from '../config/feature-flags' /** * Content Security Policy (CSP) configuration builder + * + * NOTE: This file is loaded by next.config.ts at build time, before @/ path + * aliases are resolved. Do NOT import from ../utils/urls (which uses @/ imports). + * Keep all URL constants local to this file. */ +const DEFAULT_SOCKET_URL = 'http://localhost:3002' +const DEFAULT_OLLAMA_URL = 'http://localhost:11434' + +function toWebSocketUrl(httpUrl: string): string { + return httpUrl.replace('http://', 'ws://').replace('https://', 'wss://') +} + function getHostnameFromUrl(url: string | undefined): string[] { if (!url) return [] try { @@ -156,14 +167,11 @@ export const buildTimeCSPDirectives: CSPDirectives = { 'connect-src': [ ...STATIC_CONNECT_SRC, env.NEXT_PUBLIC_APP_URL || '', - ...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? ['http://localhost:11434'] : []), + ...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? [DEFAULT_OLLAMA_URL] : []), ...(env.NEXT_PUBLIC_SOCKET_URL - ? [ - env.NEXT_PUBLIC_SOCKET_URL, - env.NEXT_PUBLIC_SOCKET_URL.replace('http://', 'ws://').replace('https://', 'wss://'), - ] + ? [env.NEXT_PUBLIC_SOCKET_URL, toWebSocketUrl(env.NEXT_PUBLIC_SOCKET_URL)] : isDev - ? ['http://localhost:3002', 'ws://localhost:3002'] + ? [DEFAULT_SOCKET_URL, toWebSocketUrl(DEFAULT_SOCKET_URL)] : []), ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL), ...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL), @@ -201,13 +209,9 @@ export function buildCSPString(directives: CSPDirectives): string { export function generateRuntimeCSP(): string { const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || '' - const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? 'http://localhost:3002' : '') - const socketWsUrl = socketUrl - ? socketUrl.replace('http://', 'ws://').replace('https://', 'wss://') - : isDev - ? 'ws://localhost:3002' - : '' - const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? 'http://localhost:11434' : '') + const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? DEFAULT_SOCKET_URL : '') + const socketWsUrl = socketUrl ? toWebSocketUrl(socketUrl) : '' + const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? DEFAULT_OLLAMA_URL : '') const brandLogoDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL')) const brandFaviconDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL')) 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/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 15712176a8d..8381fe58ca0 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -1,4 +1,4 @@ -import { getEnv } from '@/lib/core/config/env' +import { env, getEnv } from '@/lib/core/config/env' import { isProd } from '@/lib/core/config/feature-flags' /** Canonical base URL for the public-facing marketing site. No trailing slash. */ @@ -100,3 +100,30 @@ export function getEmailDomain(): string { return isProd ? 'sim.ai' : 'localhost:3000' } } + +const DEFAULT_SOCKET_URL = 'http://localhost:3002' +const DEFAULT_OLLAMA_URL = 'http://localhost:11434' + +/** + * Returns the socket server URL for server-side internal API calls. + * Reads from SOCKET_SERVER_URL with a localhost fallback for development. + */ +export function getSocketServerUrl(): string { + return env.SOCKET_SERVER_URL || DEFAULT_SOCKET_URL +} + +/** + * Returns the socket server URL for client-side Socket.IO connections. + * Reads from NEXT_PUBLIC_SOCKET_URL with a localhost fallback for development. + */ +export function getSocketUrl(): string { + return getEnv('NEXT_PUBLIC_SOCKET_URL') || DEFAULT_SOCKET_URL +} + +/** + * Returns the Ollama server URL. + * Reads from OLLAMA_URL with a localhost fallback for development. + */ +export function getOllamaUrl(): string { + return env.OLLAMA_URL || DEFAULT_OLLAMA_URL +} 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/table/query-builder/use-query-builder.ts b/apps/sim/lib/table/query-builder/use-query-builder.ts index baa76d6cc7f..1bab3a82f28 100644 --- a/apps/sim/lib/table/query-builder/use-query-builder.ts +++ b/apps/sim/lib/table/query-builder/use-query-builder.ts @@ -2,7 +2,7 @@ * Hooks for query builder UI state management (filters and sorting). */ -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { generateShortId } from '@/lib/core/utils/uuid' import type { ColumnOption } from '../types' import { @@ -15,6 +15,21 @@ import { export type { ColumnOption } +const comparisonOptions: ColumnOption[] = COMPARISON_OPERATORS.map((op) => ({ + value: op.value, + label: op.label, +})) + +const logicalOptions: ColumnOption[] = LOGICAL_OPERATORS.map((op) => ({ + value: op.value, + label: op.label, +})) + +const sortDirectionOptions: ColumnOption[] = SORT_DIRECTIONS.map((d) => ({ + value: d.value, + label: d.label, +})) + /** Manages filter rule state with add/remove/update operations. */ export function useFilterBuilder({ columns, @@ -22,21 +37,6 @@ export function useFilterBuilder({ setRules, isReadOnly = false, }: UseFilterBuilderProps): UseFilterBuilderReturn { - const comparisonOptions = useMemo( - () => COMPARISON_OPERATORS.map((op) => ({ value: op.value, label: op.label })), - [] - ) - - const logicalOptions = useMemo( - () => LOGICAL_OPERATORS.map((op) => ({ value: op.value, label: op.label })), - [] - ) - - const sortDirectionOptions = useMemo( - () => SORT_DIRECTIONS.map((d) => ({ value: d.value, label: d.label })), - [] - ) - const createDefaultRule = useCallback((): FilterRule => { return { id: generateShortId(), @@ -85,11 +85,6 @@ export function useSortBuilder({ sortRule, setSortRule, }: UseSortBuilderProps): UseSortBuilderReturn { - const sortDirectionOptions = useMemo( - () => SORT_DIRECTIONS.map((d) => ({ value: d.value, label: d.label })), - [] - ) - const addSort = useCallback(() => { setSortRule({ id: generateShortId(), 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/confluence.ts b/apps/sim/lib/webhooks/providers/confluence.ts index 79fda7c0529..cfb35bf920f 100644 --- a/apps/sim/lib/webhooks/providers/confluence.ts +++ b/apps/sim/lib/webhooks/providers/confluence.ts @@ -27,6 +27,8 @@ export const confluenceHandler: WebhookProviderHandler = { extractAttachmentData, extractSpaceData, extractLabelData, + extractPagePermissionsData, + extractUserData, } = await import('@/triggers/confluence/utils') const providerConfig = (webhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined @@ -45,6 +47,12 @@ export const confluenceHandler: WebhookProviderHandler = { if (triggerId?.startsWith('confluence_label_')) { return { input: extractLabelData(body) } } + if (triggerId === 'confluence_page_permissions_updated') { + return { input: extractPagePermissionsData(body as Record) } + } + if (triggerId === 'confluence_user_created') { + return { input: extractUserData(body as Record) } + } if (triggerId === 'confluence_webhook') { const b = body as Record return { @@ -59,12 +67,35 @@ export const confluenceHandler: WebhookProviderHandler = { space: b.space || null, label: b.label || null, content: b.content || null, + user: b.user || null, }, } } return { input: extractPageData(body) } }, + extractIdempotencyId(body: unknown) { + const obj = body as Record + const event = obj.event as string | undefined + const timestamp = obj.timestamp ?? '' + const page = obj.page as Record | undefined + const comment = obj.comment as Record | undefined + const attachment = obj.attachment as Record | undefined + const blog = (obj.blog || obj.blogpost) as Record | undefined + const space = obj.space as Record | undefined + const user = obj.user as Record | undefined + + const entityId = + comment?.id || attachment?.id || blog?.id || page?.id || space?.id || user?.accountId + if (event && entityId) { + return `confluence:${event}:${entityId}:${timestamp}` + } + if (event && timestamp) { + return `confluence:${event}:${timestamp}` + } + return null + }, + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined const obj = body as Record diff --git a/apps/sim/lib/webhooks/providers/jira.ts b/apps/sim/lib/webhooks/providers/jira.ts index 1520238d11e..0b5a80f073f 100644 --- a/apps/sim/lib/webhooks/providers/jira.ts +++ b/apps/sim/lib/webhooks/providers/jira.ts @@ -30,11 +30,8 @@ export function validateJiraSignature(secret: string, signature: string, body: s const providedSignature = signature.substring(7) const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') logger.debug('Jira signature comparison', { - computedSignature: `${computedHash.substring(0, 10)}...`, - providedSignature: `${providedSignature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: providedSignature.length, - match: computedHash === providedSignature, }) return safeCompare(computedHash, providedSignature) } catch (error) { @@ -52,17 +49,64 @@ export const jiraHandler: WebhookProviderHandler = { }), async formatInput({ body, webhook }: FormatInputContext): Promise { - const { extractIssueData, extractCommentData, extractWorklogData } = await import( - '@/triggers/jira/utils' - ) + const { + extractIssueData, + extractCommentData, + extractWorklogData, + extractSprintData, + extractProjectData, + extractVersionData, + } = await import('@/triggers/jira/utils') const providerConfig = (webhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined - if (triggerId === 'jira_issue_commented') { + + if ( + triggerId === 'jira_issue_commented' || + triggerId === 'jira_comment_updated' || + triggerId === 'jira_comment_deleted' + ) { return { input: extractCommentData(body) } } - if (triggerId === 'jira_worklog_created') { + if ( + triggerId === 'jira_worklog_created' || + triggerId === 'jira_worklog_updated' || + triggerId === 'jira_worklog_deleted' + ) { return { input: extractWorklogData(body) } } + if ( + triggerId === 'jira_sprint_created' || + triggerId === 'jira_sprint_started' || + triggerId === 'jira_sprint_closed' + ) { + return { input: extractSprintData(body) } + } + if (triggerId === 'jira_project_created') { + return { input: extractProjectData(body) } + } + if (triggerId === 'jira_version_released') { + return { input: extractVersionData(body) } + } + + if (!triggerId || triggerId === 'jira_webhook') { + const obj = body as Record + return { + input: { + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + issue_event_type_name: obj.issue_event_type_name, + issue: obj.issue || {}, + changelog: obj.changelog, + comment: obj.comment, + worklog: obj.worklog, + sprint: obj.sprint, + project: obj.project, + version: obj.version, + }, + } + } + return { input: extractIssueData(body) } }, @@ -95,9 +139,16 @@ export const jiraHandler: WebhookProviderHandler = { extractIdempotencyId(body: unknown) { const obj = body as Record const issue = obj.issue as Record | undefined + const comment = obj.comment as Record | undefined + const worklog = obj.worklog as Record | undefined const project = obj.project as Record | undefined - if (obj.webhookEvent && (issue?.id || project?.id)) { - return `${obj.webhookEvent}:${issue?.id || project?.id}` + const sprint = obj.sprint as Record | undefined + const version = obj.version as Record | undefined + const entityId = + comment?.id || worklog?.id || issue?.id || project?.id || sprint?.id || version?.id + if (obj.webhookEvent && entityId) { + const ts = obj.timestamp ?? '' + return `${obj.webhookEvent}:${entityId}:${ts}` } return null }, diff --git a/apps/sim/lib/webhooks/providers/jsm.ts b/apps/sim/lib/webhooks/providers/jsm.ts new file mode 100644 index 00000000000..9d1a785f904 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/jsm.ts @@ -0,0 +1,96 @@ +import { createLogger } from '@sim/logger' +import { validateJiraSignature } from '@/lib/webhooks/providers/jira' +import type { + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { createHmacVerifier } from '@/lib/webhooks/providers/utils' + +const logger = createLogger('WebhookProvider:JSM') + +/** + * Jira Service Management webhook handler. + * + * JSM uses the Jira webhook infrastructure. The handler reuses the same HMAC + * signature validation as Jira and adds JSM-specific event matching logic + * to route events to the correct trigger based on event type and changelog context. + */ +export const jsmHandler: WebhookProviderHandler = { + verifyAuth: createHmacVerifier({ + configKey: 'webhookSecret', + headerName: 'X-Hub-Signature', + validateFn: validateJiraSignature, + providerLabel: 'JSM', + }), + + async formatInput({ body, webhook }: FormatInputContext): Promise { + const { extractRequestData, extractCommentData } = await import('@/triggers/jsm/utils') + const providerConfig = (webhook.providerConfig as Record) || {} + const triggerId = providerConfig.triggerId as string | undefined + + if (triggerId === 'jsm_request_commented') { + return { input: extractCommentData(body as Record) } + } + + // For the generic webhook, pass through the full payload so no data is lost + if (!triggerId || triggerId === 'jsm_webhook') { + const obj = body as Record + return { + input: { + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + issue_event_type_name: obj.issue_event_type_name, + issue: obj.issue || {}, + changelog: obj.changelog, + comment: obj.comment, + }, + } + } + + return { input: extractRequestData(body as Record) } + }, + + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { + const triggerId = providerConfig.triggerId as string | undefined + const obj = body as Record + + if (triggerId && triggerId !== 'jsm_webhook') { + const webhookEvent = obj.webhookEvent as string | undefined + const issueEventTypeName = obj.issue_event_type_name as string | undefined + const changelog = obj.changelog as + | { items?: Array<{ field?: string; toString?: string }> } + | undefined + + const { isJsmEventMatch } = await import('@/triggers/jsm/utils') + if (!isJsmEventMatch(triggerId, webhookEvent || '', issueEventTypeName, changelog)) { + logger.debug( + `[${requestId}] JSM event mismatch for trigger ${triggerId}. Event: ${webhookEvent}. Skipping execution.`, + { + webhookId: webhook.id, + workflowId: workflow.id, + triggerId, + receivedEvent: webhookEvent, + } + ) + return false + } + } + + return true + }, + + extractIdempotencyId(body: unknown) { + const obj = body as Record + const comment = obj.comment as Record | undefined + const issue = obj.issue as Record | undefined + const entityId = comment?.id || issue?.id + if (obj.webhookEvent && entityId) { + const ts = obj.timestamp ?? '' + return `jsm:${obj.webhookEvent}:${entityId}:${ts}` + } + return null + }, +} 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..a9d559a77dc 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -20,9 +20,11 @@ import { hubspotHandler } from '@/lib/webhooks/providers/hubspot' import { imapHandler } from '@/lib/webhooks/providers/imap' import { intercomHandler } from '@/lib/webhooks/providers/intercom' import { jiraHandler } from '@/lib/webhooks/providers/jira' +import { jsmHandler } from '@/lib/webhooks/providers/jsm' 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' @@ -65,8 +67,10 @@ const PROVIDER_HANDLERS: Record = { imap: imapHandler, intercom: intercomHandler, jira: jiraHandler, + jsm: jsmHandler, lemlist: lemlistHandler, linear: linearHandler, + monday: mondayHandler, resend: resendHandler, 'microsoft-teams': microsoftTeamsHandler, notion: notionHandler, diff --git a/apps/sim/lib/workflows/lifecycle.test.ts b/apps/sim/lib/workflows/lifecycle.test.ts index 473ff68a3c3..28c3b2386ee 100644 --- a/apps/sim/lib/workflows/lifecycle.test.ts +++ b/apps/sim/lib/workflows/lifecycle.test.ts @@ -56,6 +56,11 @@ vi.mock('@/lib/core/config/env', () => ({ SOCKET_SERVER_URL: 'http://socket.test', INTERNAL_API_SECRET: 'secret', }, + getEnv: vi.fn(), +})) + +vi.mock('@/lib/core/utils/urls', () => ({ + getSocketServerUrl: vi.fn().mockReturnValue('http://socket.test'), })) vi.mock('@/lib/core/telemetry', () => ({ diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index c9ff0a9bcfa..d42dca12ca2 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -18,6 +18,7 @@ import { env } from '@/lib/core/config/env' import { getRedisClient } from '@/lib/core/config/redis' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { mcpPubSub } from '@/lib/mcp/pubsub' import { getWorkflowById } from '@/lib/workflows/utils' @@ -31,8 +32,7 @@ interface ArchiveWorkflowOptions { async function notifyWorkflowArchived(workflowId: string, requestId: string): Promise { try { - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - const socketResponse = await fetch(`${socketUrl}/api/workflow-deleted`, { + const socketResponse = await fetch(`${getSocketServerUrl()}/api/workflow-deleted`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/workflows/operations/import-export.test.ts b/apps/sim/lib/workflows/operations/import-export.test.ts new file mode 100644 index 00000000000..7d913327274 --- /dev/null +++ b/apps/sim/lib/workflows/operations/import-export.test.ts @@ -0,0 +1,33 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { sanitizePathSegment } from '@/lib/workflows/operations/import-export' + +describe('sanitizePathSegment', () => { + it('should preserve ASCII alphanumeric characters', () => { + expect(sanitizePathSegment('workflow-123_abc')).toBe('workflow-123_abc') + }) + + it('should replace spaces with dashes', () => { + expect(sanitizePathSegment('my workflow')).toBe('my-workflow') + }) + + it('should replace special characters with dashes', () => { + expect(sanitizePathSegment('workflow!@#')).toBe('workflow-') + }) + + it('should preserve Korean characters (BUG REPRODUCTION)', () => { + expect(sanitizePathSegment('한글')).toBe('한글') + }) + + it('should preserve other Unicode characters', () => { + expect(sanitizePathSegment('日本語')).toBe('日本語') + }) + + it('should remove filesystem unsafe characters', () => { + expect(sanitizePathSegment('work/flow?name*')).not.toContain('/') + expect(sanitizePathSegment('work/flow?name*')).not.toContain('?') + expect(sanitizePathSegment('work/flow?name*')).not.toContain('*') + }) +}) diff --git a/apps/sim/lib/workflows/operations/import-export.ts b/apps/sim/lib/workflows/operations/import-export.ts index fdac248f134..093f3617a9f 100644 --- a/apps/sim/lib/workflows/operations/import-export.ts +++ b/apps/sim/lib/workflows/operations/import-export.ts @@ -48,7 +48,7 @@ export interface WorkspaceExportStructure { * Sanitizes a string for use as a path segment in a ZIP file. */ export function sanitizePathSegment(name: string): string { - return name.replace(/[^a-z0-9-_]/gi, '-') + return name.replace(/[^\p{L}\p{N}\-_]/gu, '-').replace(/-+/g, '-') } /** diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts index 56717aa877d..4f81701b558 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.ts @@ -5,7 +5,7 @@ import { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBaseUrl, getSocketServerUrl } from '@/lib/core/utils/urls' import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync' import { captureServerEvent } from '@/lib/posthog/server' import { @@ -31,6 +31,30 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployOrchestration') +/** + * Notifies the socket server that a workflow's deployment state has changed, + * so all connected clients can refresh their deployment queries. + */ +export async function notifySocketDeploymentChanged(workflowId: string): Promise { + try { + const response = await fetch(`${getSocketServerUrl()}/api/workflow-deployed`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': env.INTERNAL_API_SECRET, + }, + body: JSON.stringify({ workflowId }), + }) + if (!response.ok) { + logger.warn( + `Socket deployment notification failed (${response.status}) for workflow ${workflowId}` + ) + } + } catch (error) { + logger.error('Error sending workflow deployed event to socket server', error) + } +} + export interface PerformFullDeployParams { workflowId: string userId: string @@ -222,6 +246,8 @@ export async function performFullDeploy( request, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true, deployedAt, @@ -296,6 +322,8 @@ export async function performFullUndeploy( description: `Undeployed workflow "${(workflowData.name as string) || workflowId}"`, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true } } @@ -509,6 +537,8 @@ export async function performActivateVersion( }, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true, deployedAt: result.deployedAt, @@ -596,8 +626,7 @@ export async function performRevertToVersion( .where(eq(workflowTable.id, workflowId)) try { - const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - await fetch(`${socketServerUrl}/api/workflow-reverted`, { + await fetch(`${getSocketServerUrl()}/api/workflow-reverted`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/workflows/orchestration/index.ts b/apps/sim/lib/workflows/orchestration/index.ts index a4aeeec42e2..a055f6a3532 100644 --- a/apps/sim/lib/workflows/orchestration/index.ts +++ b/apps/sim/lib/workflows/orchestration/index.ts @@ -7,6 +7,7 @@ export { performChatUndeploy, } from './chat-deploy' export { + notifySocketDeploymentChanged, type PerformActivateVersionParams, type PerformActivateVersionResult, type PerformFullDeployParams, 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/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index b6cb8dd3234..c09bf88977b 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' -import { env } from '@/lib/core/config/env' +import { getOllamaUrl } from '@/lib/core/utils/urls' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import type { ModelsObject } from '@/providers/ollama/types' @@ -18,7 +18,7 @@ import { useProvidersStore } from '@/stores/providers' import { executeTool } from '@/tools' const logger = createLogger('OllamaProvider') -const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434' +const OLLAMA_HOST = getOllamaUrl() export const ollamaProvider: ProviderConfig = { id: 'ollama', diff --git a/apps/sim/socket/rooms/memory-manager.ts b/apps/sim/socket/rooms/memory-manager.ts index fa631ff6898..1e14c9c7df1 100644 --- a/apps/sim/socket/rooms/memory-manager.ts +++ b/apps/sim/socket/rooms/memory-manager.ts @@ -238,4 +238,21 @@ export class MemoryRoomManager implements IRoomManager { logger.info(`Notified ${room.users.size} users about workflow update: ${workflowId}`) } + + async handleWorkflowDeployed(workflowId: string): Promise { + logger.info(`Handling workflow deployed notification for ${workflowId}`) + + const room = this.workflowRooms.get(workflowId) + if (!room) { + logger.debug(`No active room found for deployed workflow ${workflowId}`) + return + } + + this._io.to(workflowId).emit('workflow-deployed', { + workflowId, + timestamp: Date.now(), + }) + + logger.info(`Notified ${room.users.size} users about workflow deployment change: ${workflowId}`) + } } diff --git a/apps/sim/socket/rooms/redis-manager.ts b/apps/sim/socket/rooms/redis-manager.ts index fb0d0d1042b..adfe6720485 100644 --- a/apps/sim/socket/rooms/redis-manager.ts +++ b/apps/sim/socket/rooms/redis-manager.ts @@ -439,4 +439,22 @@ export class RedisRoomManager implements IRoomManager { const userCount = await this.getUniqueUserCount(workflowId) logger.info(`Notified ${userCount} users about workflow update: ${workflowId}`) } + + async handleWorkflowDeployed(workflowId: string): Promise { + logger.info(`Handling workflow deployed notification for ${workflowId}`) + + const hasRoom = await this.hasWorkflowRoom(workflowId) + if (!hasRoom) { + logger.debug(`No active room found for deployed workflow ${workflowId}`) + return + } + + this._io.to(workflowId).emit('workflow-deployed', { + workflowId, + timestamp: Date.now(), + }) + + const userCount = await this.getUniqueUserCount(workflowId) + logger.info(`Notified ${userCount} users about workflow deployment change: ${workflowId}`) + } } diff --git a/apps/sim/socket/rooms/types.ts b/apps/sim/socket/rooms/types.ts index 5c755a739e0..9553a427e1e 100644 --- a/apps/sim/socket/rooms/types.ts +++ b/apps/sim/socket/rooms/types.ts @@ -138,4 +138,9 @@ export interface IRoomManager { * Handle workflow update - notify users */ handleWorkflowUpdate(workflowId: string): Promise + + /** + * Handle workflow deployment change - notify users to refresh deployment state + */ + handleWorkflowDeployed(workflowId: string): Promise } diff --git a/apps/sim/socket/routes/http.ts b/apps/sim/socket/routes/http.ts index ea2eb3cde76..5c555e92843 100644 --- a/apps/sim/socket/routes/http.ts +++ b/apps/sim/socket/routes/http.ts @@ -122,6 +122,20 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) { return } + // Handle workflow deployment change notifications from the main API + if (req.method === 'POST' && req.url === '/api/workflow-deployed') { + try { + const body = await readRequestBody(req) + const { workflowId } = JSON.parse(body) + await roomManager.handleWorkflowDeployed(workflowId) + sendSuccess(res) + } catch (error) { + logger.error('Error handling workflow deployed notification:', error) + sendError(res, 'Failed to process deployment notification') + } + return + } + // Handle workflow revert notifications from the main API if (req.method === 'POST' && req.url === '/api/workflow-reverted') { try { 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/confluence/attachment_updated.ts b/apps/sim/triggers/confluence/attachment_updated.ts new file mode 100644 index 00000000000..47487554bc8 --- /dev/null +++ b/apps/sim/triggers/confluence/attachment_updated.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildAttachmentOutputs, + buildConfluenceAttachmentExtraFields, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Attachment Updated Trigger + * + * Triggers when an attachment is updated (new version uploaded) in Confluence. + */ +export const confluenceAttachmentUpdatedTrigger: TriggerConfig = { + id: 'confluence_attachment_updated', + name: 'Confluence Attachment Updated', + provider: 'confluence', + description: 'Trigger workflow when an attachment is updated in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_attachment_updated', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('attachment_updated'), + extraFields: buildConfluenceAttachmentExtraFields('confluence_attachment_updated'), + }), + + outputs: buildAttachmentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/blog_restored.ts b/apps/sim/triggers/confluence/blog_restored.ts new file mode 100644 index 00000000000..6c66649407b --- /dev/null +++ b/apps/sim/triggers/confluence/blog_restored.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildBlogOutputs, + buildConfluenceExtraFields, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Blog Post Restored Trigger + * + * Triggers when a blog post is restored from trash in Confluence. + */ +export const confluenceBlogRestoredTrigger: TriggerConfig = { + id: 'confluence_blog_restored', + name: 'Confluence Blog Post Restored', + provider: 'confluence', + description: 'Trigger workflow when a blog post is restored from trash in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_blog_restored', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('blog_restored'), + extraFields: buildConfluenceExtraFields('confluence_blog_restored'), + }), + + outputs: buildBlogOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/comment_updated.ts b/apps/sim/triggers/confluence/comment_updated.ts new file mode 100644 index 00000000000..84c499fe106 --- /dev/null +++ b/apps/sim/triggers/confluence/comment_updated.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildCommentOutputs, + buildConfluenceExtraFields, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Comment Updated Trigger + * + * Triggers when a comment on a page or blog post is updated/edited in Confluence. + */ +export const confluenceCommentUpdatedTrigger: TriggerConfig = { + id: 'confluence_comment_updated', + name: 'Confluence Comment Updated', + provider: 'confluence', + description: 'Trigger workflow when a comment is updated in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_comment_updated', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('comment_updated'), + extraFields: buildConfluenceExtraFields('confluence_comment_updated'), + }), + + outputs: buildCommentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/index.ts b/apps/sim/triggers/confluence/index.ts index ea5c16fa97d..4efe33e7cfd 100644 --- a/apps/sim/triggers/confluence/index.ts +++ b/apps/sim/triggers/confluence/index.ts @@ -5,17 +5,24 @@ export { confluenceAttachmentCreatedTrigger } from './attachment_created' export { confluenceAttachmentRemovedTrigger } from './attachment_removed' +export { confluenceAttachmentUpdatedTrigger } from './attachment_updated' export { confluenceBlogCreatedTrigger } from './blog_created' export { confluenceBlogRemovedTrigger } from './blog_removed' +export { confluenceBlogRestoredTrigger } from './blog_restored' export { confluenceBlogUpdatedTrigger } from './blog_updated' export { confluenceCommentCreatedTrigger } from './comment_created' export { confluenceCommentRemovedTrigger } from './comment_removed' +export { confluenceCommentUpdatedTrigger } from './comment_updated' export { confluenceLabelAddedTrigger } from './label_added' export { confluenceLabelRemovedTrigger } from './label_removed' export { confluencePageCreatedTrigger } from './page_created' export { confluencePageMovedTrigger } from './page_moved' +export { confluencePagePermissionsUpdatedTrigger } from './page_permissions_updated' export { confluencePageRemovedTrigger } from './page_removed' +export { confluencePageRestoredTrigger } from './page_restored' export { confluencePageUpdatedTrigger } from './page_updated' export { confluenceSpaceCreatedTrigger } from './space_created' +export { confluenceSpaceRemovedTrigger } from './space_removed' export { confluenceSpaceUpdatedTrigger } from './space_updated' +export { confluenceUserCreatedTrigger } from './user_created' export { confluenceWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/confluence/page_permissions_updated.ts b/apps/sim/triggers/confluence/page_permissions_updated.ts new file mode 100644 index 00000000000..a078c5c2c17 --- /dev/null +++ b/apps/sim/triggers/confluence/page_permissions_updated.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildConfluenceExtraFields, + buildPagePermissionsOutputs, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Page Permissions Updated Trigger + * + * Triggers when page permissions are changed in Confluence. + */ +export const confluencePagePermissionsUpdatedTrigger: TriggerConfig = { + id: 'confluence_page_permissions_updated', + name: 'Confluence Page Permissions Updated', + provider: 'confluence', + description: 'Trigger workflow when page permissions are changed in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_page_permissions_updated', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('content_permissions_updated'), + extraFields: buildConfluenceExtraFields('confluence_page_permissions_updated'), + }), + + outputs: buildPagePermissionsOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/page_restored.ts b/apps/sim/triggers/confluence/page_restored.ts new file mode 100644 index 00000000000..d0676d52c7e --- /dev/null +++ b/apps/sim/triggers/confluence/page_restored.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildConfluenceExtraFields, + buildPageOutputs, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Page Restored Trigger + * + * Triggers when a page is restored from trash in Confluence. + */ +export const confluencePageRestoredTrigger: TriggerConfig = { + id: 'confluence_page_restored', + name: 'Confluence Page Restored', + provider: 'confluence', + description: 'Trigger workflow when a page is restored from trash in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_page_restored', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('page_restored'), + extraFields: buildConfluenceExtraFields('confluence_page_restored'), + }), + + outputs: buildPageOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/space_removed.ts b/apps/sim/triggers/confluence/space_removed.ts new file mode 100644 index 00000000000..ccbb6205872 --- /dev/null +++ b/apps/sim/triggers/confluence/space_removed.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildConfluenceExtraFields, + buildSpaceOutputs, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence Space Removed Trigger + * + * Triggers when a space is deleted/removed in Confluence. + */ +export const confluenceSpaceRemovedTrigger: TriggerConfig = { + id: 'confluence_space_removed', + name: 'Confluence Space Removed', + provider: 'confluence', + description: 'Trigger workflow when a space is removed in Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_space_removed', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('space_removed'), + extraFields: buildConfluenceExtraFields('confluence_space_removed'), + }), + + outputs: buildSpaceOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/user_created.ts b/apps/sim/triggers/confluence/user_created.ts new file mode 100644 index 00000000000..753beb7e862 --- /dev/null +++ b/apps/sim/triggers/confluence/user_created.ts @@ -0,0 +1,41 @@ +import { ConfluenceIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildConfluenceExtraFields, + buildUserOutputs, + confluenceSetupInstructions, + confluenceTriggerOptions, +} from '@/triggers/confluence/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Confluence User Created Trigger + * + * Triggers when a new user is added to Confluence. + */ +export const confluenceUserCreatedTrigger: TriggerConfig = { + id: 'confluence_user_created', + name: 'Confluence User Created', + provider: 'confluence', + description: 'Trigger workflow when a new user is added to Confluence', + version: '1.0.0', + icon: ConfluenceIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'confluence_user_created', + triggerOptions: confluenceTriggerOptions, + setupInstructions: confluenceSetupInstructions('user_created'), + extraFields: buildConfluenceExtraFields('confluence_user_created'), + }), + + outputs: buildUserOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/confluence/utils.ts b/apps/sim/triggers/confluence/utils.ts index 95f0caf87ea..cc722457a97 100644 --- a/apps/sim/triggers/confluence/utils.ts +++ b/apps/sim/triggers/confluence/utils.ts @@ -17,6 +17,13 @@ export const confluenceTriggerOptions = [ { label: 'Space Updated', id: 'confluence_space_updated' }, { label: 'Label Added', id: 'confluence_label_added' }, { label: 'Label Removed', id: 'confluence_label_removed' }, + { label: 'Comment Updated', id: 'confluence_comment_updated' }, + { label: 'Attachment Updated', id: 'confluence_attachment_updated' }, + { label: 'Page Restored', id: 'confluence_page_restored' }, + { label: 'Blog Post Restored', id: 'confluence_blog_restored' }, + { label: 'Space Removed', id: 'confluence_space_removed' }, + { label: 'Page Permissions Updated', id: 'confluence_page_permissions_updated' }, + { label: 'User Created', id: 'confluence_user_created' }, { label: 'Generic Webhook (All Events)', id: 'confluence_webhook' }, ] @@ -234,6 +241,39 @@ export function buildLabelOutputs(): Record { } } +/** Page permissions outputs for permission change events. */ +export function buildPagePermissionsOutputs(): Record { + return { + ...buildBaseWebhookOutputs(), + page: { + ...buildContentEntityFields(), + permissions: { + type: 'json', + description: 'Updated permissions object for the page', + }, + }, + } +} + +/** User-related outputs for user events. */ +export function buildUserOutputs(): Record { + return { + ...buildBaseWebhookOutputs(), + user: { + accountId: { type: 'string', description: 'Account ID of the new user' }, + accountType: { type: 'string', description: 'Account type (e.g., atlassian, app)' }, + displayName: { type: 'string', description: 'Display name of the user' }, + emailAddress: { + type: 'string', + description: + 'Email address of the user (may not be available due to GDPR/privacy settings)', + }, + publicName: { type: 'string', description: 'Public name of the user' }, + self: { type: 'string', description: 'URL link to the user profile' }, + }, + } +} + /** Combined outputs for the generic webhook trigger (all events). */ export function buildGenericWebhookOutputs(): Record { return { @@ -245,6 +285,7 @@ export function buildGenericWebhookOutputs(): Record { space: { type: 'json', description: 'Space object (present in space events)' }, label: { type: 'json', description: 'Label object (present in label events)' }, content: { type: 'json', description: 'Content object (present in label events)' }, + user: { type: 'json', description: 'User object (present in user events)' }, files: { type: 'file[]', description: @@ -308,6 +349,24 @@ export function extractLabelData(body: any) { } } +export function extractPagePermissionsData(body: Record) { + return { + timestamp: body.timestamp, + userAccountId: body.userAccountId, + accountType: body.accountType, + page: body.page || {}, + } +} + +export function extractUserData(body: Record) { + return { + timestamp: body.timestamp, + userAccountId: body.userAccountId, + accountType: body.accountType, + user: body.user || {}, + } +} + /** * Maps trigger IDs to the exact Confluence event strings they accept. * Admin REST API webhooks include an `event` field (e.g. `"event": "page_created"`). @@ -329,6 +388,13 @@ const TRIGGER_EVENT_MAP: Record = { confluence_space_updated: ['space_updated'], confluence_label_added: ['label_added', 'label_created'], confluence_label_removed: ['label_removed', 'label_deleted'], + confluence_comment_updated: ['comment_updated'], + confluence_attachment_updated: ['attachment_updated'], + confluence_page_restored: ['page_restored'], + confluence_blog_restored: ['blog_restored'], + confluence_space_removed: ['space_removed'], + confluence_page_permissions_updated: ['content_permissions_updated'], + confluence_user_created: ['user_created', 'user_reactivated'], } const TRIGGER_CATEGORY_MAP: Record = { @@ -347,6 +413,13 @@ const TRIGGER_CATEGORY_MAP: Record = { confluence_space_updated: 'space', confluence_label_added: 'label', confluence_label_removed: 'label', + confluence_comment_updated: 'comment', + confluence_attachment_updated: 'attachment', + confluence_page_restored: 'page', + confluence_blog_restored: 'blog', + confluence_space_removed: 'space', + confluence_page_permissions_updated: 'page', + confluence_user_created: 'user', } /** @@ -360,6 +433,7 @@ function inferEntityCategory(body: Record): string | null { if (body.label) return 'label' if (body.page) return 'page' if (body.space) return 'space' + if (body.user) return 'user' return null } diff --git a/apps/sim/triggers/jira/comment_deleted.ts b/apps/sim/triggers/jira/comment_deleted.ts new file mode 100644 index 00000000000..4988edfbfbc --- /dev/null +++ b/apps/sim/triggers/jira/comment_deleted.ts @@ -0,0 +1,83 @@ +import { JiraIcon } from '@/components/icons' +import { buildCommentOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Comment Deleted Trigger + * Triggers when a comment on an issue is deleted + */ +export const jiraCommentDeletedTrigger: TriggerConfig = { + id: 'jira_comment_deleted', + name: 'Jira Comment Deleted', + provider: 'jira', + description: 'Trigger workflow when a comment is deleted from a Jira issue', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_deleted', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_deleted', + }, + }, + { + id: 'jqlFilter', + title: 'JQL Filter', + type: 'long-input', + placeholder: 'project = PROJ AND issuetype = Bug', + description: 'Filter which comment deletions trigger this workflow using JQL', + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_deleted', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('comment_deleted'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_deleted', + }, + }, + ], + + outputs: buildCommentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/comment_updated.ts b/apps/sim/triggers/jira/comment_updated.ts new file mode 100644 index 00000000000..0d5ffa279c4 --- /dev/null +++ b/apps/sim/triggers/jira/comment_updated.ts @@ -0,0 +1,83 @@ +import { JiraIcon } from '@/components/icons' +import { buildCommentOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Comment Updated Trigger + * Triggers when a comment on an issue is updated + */ +export const jiraCommentUpdatedTrigger: TriggerConfig = { + id: 'jira_comment_updated', + name: 'Jira Comment Updated', + provider: 'jira', + description: 'Trigger workflow when a comment is updated on a Jira issue', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_updated', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_updated', + }, + }, + { + id: 'jqlFilter', + title: 'JQL Filter', + type: 'long-input', + placeholder: 'project = PROJ AND issuetype = Bug', + description: 'Filter which comment updates trigger this workflow using JQL', + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_updated', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('comment_updated'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_comment_updated', + }, + }, + ], + + outputs: buildCommentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/index.ts b/apps/sim/triggers/jira/index.ts index 06527d8620a..865792e9053 100644 --- a/apps/sim/triggers/jira/index.ts +++ b/apps/sim/triggers/jira/index.ts @@ -3,9 +3,18 @@ * Export all Jira webhook triggers */ +export { jiraCommentDeletedTrigger } from './comment_deleted' +export { jiraCommentUpdatedTrigger } from './comment_updated' export { jiraIssueCommentedTrigger } from './issue_commented' export { jiraIssueCreatedTrigger } from './issue_created' export { jiraIssueDeletedTrigger } from './issue_deleted' export { jiraIssueUpdatedTrigger } from './issue_updated' +export { jiraProjectCreatedTrigger } from './project_created' +export { jiraSprintClosedTrigger } from './sprint_closed' +export { jiraSprintCreatedTrigger } from './sprint_created' +export { jiraSprintStartedTrigger } from './sprint_started' +export { jiraVersionReleasedTrigger } from './version_released' export { jiraWebhookTrigger } from './webhook' export { jiraWorklogCreatedTrigger } from './worklog_created' +export { jiraWorklogDeletedTrigger } from './worklog_deleted' +export { jiraWorklogUpdatedTrigger } from './worklog_updated' diff --git a/apps/sim/triggers/jira/project_created.ts b/apps/sim/triggers/jira/project_created.ts new file mode 100644 index 00000000000..634adc2b3e6 --- /dev/null +++ b/apps/sim/triggers/jira/project_created.ts @@ -0,0 +1,70 @@ +import { JiraIcon } from '@/components/icons' +import { buildProjectCreatedOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Project Created Trigger + * Triggers when a project is created + */ +export const jiraProjectCreatedTrigger: TriggerConfig = { + id: 'jira_project_created', + name: 'Jira Project Created', + provider: 'jira', + description: 'Trigger workflow when a project is created in Jira', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_project_created', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_project_created', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('project_created'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_project_created', + }, + }, + ], + + outputs: buildProjectCreatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/sprint_closed.ts b/apps/sim/triggers/jira/sprint_closed.ts new file mode 100644 index 00000000000..45a25680891 --- /dev/null +++ b/apps/sim/triggers/jira/sprint_closed.ts @@ -0,0 +1,70 @@ +import { JiraIcon } from '@/components/icons' +import { buildSprintOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Sprint Closed Trigger + * Triggers when a sprint is closed/completed + */ +export const jiraSprintClosedTrigger: TriggerConfig = { + id: 'jira_sprint_closed', + name: 'Jira Sprint Closed', + provider: 'jira', + description: 'Trigger workflow when a sprint is closed in Jira', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_closed', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_closed', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('sprint_closed'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_closed', + }, + }, + ], + + outputs: buildSprintOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/sprint_created.ts b/apps/sim/triggers/jira/sprint_created.ts new file mode 100644 index 00000000000..c46d0db477d --- /dev/null +++ b/apps/sim/triggers/jira/sprint_created.ts @@ -0,0 +1,70 @@ +import { JiraIcon } from '@/components/icons' +import { buildSprintOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Sprint Created Trigger + * Triggers when a sprint is created + */ +export const jiraSprintCreatedTrigger: TriggerConfig = { + id: 'jira_sprint_created', + name: 'Jira Sprint Created', + provider: 'jira', + description: 'Trigger workflow when a sprint is created in Jira', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_created', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_created', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('sprint_created'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_created', + }, + }, + ], + + outputs: buildSprintOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/sprint_started.ts b/apps/sim/triggers/jira/sprint_started.ts new file mode 100644 index 00000000000..a12f83e78d0 --- /dev/null +++ b/apps/sim/triggers/jira/sprint_started.ts @@ -0,0 +1,70 @@ +import { JiraIcon } from '@/components/icons' +import { buildSprintOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Sprint Started Trigger + * Triggers when a sprint is started + */ +export const jiraSprintStartedTrigger: TriggerConfig = { + id: 'jira_sprint_started', + name: 'Jira Sprint Started', + provider: 'jira', + description: 'Trigger workflow when a sprint is started in Jira', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_started', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_started', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('sprint_started'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_sprint_started', + }, + }, + ], + + outputs: buildSprintOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/utils.ts b/apps/sim/triggers/jira/utils.ts index 5face582998..3cdcd1ef87e 100644 --- a/apps/sim/triggers/jira/utils.ts +++ b/apps/sim/triggers/jira/utils.ts @@ -1,4 +1,3 @@ -import type { SubBlockConfig } from '@/blocks/types' import type { TriggerOutput } from '@/triggers/types' /** @@ -9,74 +8,19 @@ export const jiraTriggerOptions = [ { label: 'Issue Updated', id: 'jira_issue_updated' }, { label: 'Issue Deleted', id: 'jira_issue_deleted' }, { label: 'Issue Commented', id: 'jira_issue_commented' }, + { label: 'Comment Updated', id: 'jira_comment_updated' }, + { label: 'Comment Deleted', id: 'jira_comment_deleted' }, { label: 'Worklog Created', id: 'jira_worklog_created' }, + { label: 'Worklog Updated', id: 'jira_worklog_updated' }, + { label: 'Worklog Deleted', id: 'jira_worklog_deleted' }, + { label: 'Sprint Created', id: 'jira_sprint_created' }, + { label: 'Sprint Started', id: 'jira_sprint_started' }, + { label: 'Sprint Closed', id: 'jira_sprint_closed' }, + { label: 'Project Created', id: 'jira_project_created' }, + { label: 'Version Released', id: 'jira_version_released' }, { label: 'Generic Webhook (All Events)', id: 'jira_webhook' }, ] -/** - * Common webhook subBlocks for Jira triggers - * Used across all Jira webhook-based triggers - */ -export const jiraWebhookSubBlocks: SubBlockConfig[] = [ - { - id: 'triggerCredentials', - title: 'Jira Credentials', - type: 'oauth-input', - serviceId: 'jira', - requiredScopes: [ - 'read:jira-work', - 'read:jira-user', - 'manage:jira-webhook', - 'read:webhook:jira', - 'write:webhook:jira', - 'delete:webhook:jira', - 'read:issue-event:jira', - 'read:issue:jira', - 'read:issue.changelog:jira', - 'read:comment:jira', - 'read:comment.property:jira', - 'read:issue.property:jira', - 'read:issue-worklog:jira', - 'read:project:jira', - 'read:field:jira', - 'read:jql:jira', - ], - placeholder: 'Select Jira account', - required: true, - mode: 'trigger', - }, - { - id: 'webhookUrlDisplay', - title: 'Webhook URL', - type: 'short-input', - readOnly: true, - showCopyButton: true, - useWebhookUrl: true, - placeholder: 'Webhook URL will be generated after saving', - mode: 'trigger', - description: 'Copy this URL and use it when configuring the webhook in Jira', - }, - { - id: 'webhookSecret', - title: 'Webhook Secret', - type: 'short-input', - placeholder: 'Enter webhook secret for validation', - description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', - password: true, - required: false, - mode: 'trigger', - }, - { - id: 'jiraDomain', - title: 'Jira Domain', - type: 'short-input', - placeholder: 'your-company.atlassian.net', - description: 'Your Jira Cloud domain', - required: false, - mode: 'trigger', - }, -] - /** * Generates setup instructions for Jira webhooks */ @@ -114,6 +58,20 @@ function buildBaseWebhookOutputs(): Record { type: 'number', description: 'Timestamp of the webhook event', }, + user: { + displayName: { + type: 'string', + description: 'Display name of the user who triggered the event', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who triggered the event', + }, + emailAddress: { + type: 'string', + description: 'Email address of the user who triggered the event', + }, + }, issue: { id: { @@ -166,7 +124,8 @@ function buildBaseWebhookOutputs(): Record { }, emailAddress: { type: 'string', - description: 'Creator email address', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', }, }, duedate: { @@ -191,6 +150,11 @@ function buildBaseWebhookOutputs(): Record { type: 'string', description: 'Issue summary/title', }, + description: { + type: 'json', + description: + 'Issue description in Atlassian Document Format (ADF). On Jira Server this may be a plain string.', + }, updated: { type: 'string', description: 'Last updated date (ISO format)', @@ -210,7 +174,8 @@ function buildBaseWebhookOutputs(): Record { }, emailAddress: { type: 'string', - description: 'Assignee email address', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', }, }, priority: { @@ -238,7 +203,8 @@ function buildBaseWebhookOutputs(): Record { }, emailAddress: { type: 'string', - description: 'Reporter email address', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', }, }, security: { @@ -263,6 +229,24 @@ function buildBaseWebhookOutputs(): Record { description: 'Issue type ID', }, }, + resolution: { + name: { + type: 'string', + description: 'Resolution name (e.g., Done, Fixed)', + }, + id: { + type: 'string', + description: 'Resolution ID', + }, + }, + components: { + type: 'array', + description: 'Array of component objects associated with this issue', + }, + fixVersions: { + type: 'array', + description: 'Array of fix version objects for this issue', + }, }, }, } @@ -309,8 +293,9 @@ export function buildCommentOutputs(): Record { description: 'Comment ID', }, body: { - type: 'string', - description: 'Comment text/body', + type: 'json', + description: + 'Comment body in Atlassian Document Format (ADF). On Jira Server this may be a plain string.', }, author: { displayName: { @@ -326,6 +311,16 @@ export function buildCommentOutputs(): Record { description: 'Comment author email address', }, }, + updateAuthor: { + displayName: { + type: 'string', + description: 'Display name of the user who last updated the comment', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who last updated the comment', + }, + }, created: { type: 'string', description: 'Comment creation date (ISO format)', @@ -334,6 +329,10 @@ export function buildCommentOutputs(): Record { type: 'string', description: 'Comment last updated date (ISO format)', }, + self: { + type: 'string', + description: 'REST API URL for this comment', + }, }, } } @@ -361,6 +360,16 @@ export function buildWorklogOutputs(): Record { description: 'Worklog author email address', }, }, + updateAuthor: { + displayName: { + type: 'string', + description: 'Display name of the user who last updated the worklog', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who last updated the worklog', + }, + }, timeSpent: { type: 'string', description: 'Time spent (e.g., "2h 30m")', @@ -377,6 +386,22 @@ export function buildWorklogOutputs(): Record { type: 'string', description: 'When the work was started (ISO format)', }, + created: { + type: 'string', + description: 'When the worklog entry was created (ISO format)', + }, + updated: { + type: 'string', + description: 'When the worklog entry was last updated (ISO format)', + }, + issueId: { + type: 'string', + description: 'ID of the issue this worklog belongs to', + }, + self: { + type: 'string', + description: 'REST API URL for this worklog entry', + }, }, } } @@ -391,9 +416,16 @@ export function isJiraEventMatch( jira_issue_updated: ['jira:issue_updated', 'issue_updated', 'issue_generic'], jira_issue_deleted: ['jira:issue_deleted', 'issue_deleted'], jira_issue_commented: ['comment_created'], + jira_comment_updated: ['comment_updated'], + jira_comment_deleted: ['comment_deleted'], jira_worklog_created: ['worklog_created'], jira_worklog_updated: ['worklog_updated'], jira_worklog_deleted: ['worklog_deleted'], + jira_sprint_created: ['sprint_created'], + jira_sprint_started: ['sprint_started'], + jira_sprint_closed: ['sprint_closed'], + jira_project_created: ['project_created'], + jira_version_released: ['jira:version_released'], // Generic webhook accepts all events jira_webhook: ['*'], } @@ -415,30 +447,273 @@ export function isJiraEventMatch( ) } -export function extractIssueData(body: any) { +export function extractIssueData(body: unknown) { + const obj = body as Record + return { + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + issue_event_type_name: obj.issue_event_type_name, + issue: obj.issue || {}, + changelog: obj.changelog, + } +} + +export function extractCommentData(body: unknown) { + const obj = body as Record return { - webhookEvent: body.webhookEvent, - timestamp: body.timestamp, - issue_event_type_name: body.issue_event_type_name, - issue: body.issue || {}, - changelog: body.changelog, + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + issue: obj.issue || {}, + comment: obj.comment || {}, } } -export function extractCommentData(body: any) { +export function extractWorklogData(body: unknown) { + const obj = body as Record return { - webhookEvent: body.webhookEvent, - timestamp: body.timestamp, - issue: body.issue || {}, - comment: body.comment || {}, + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + issue: obj.issue || {}, + worklog: obj.worklog || {}, } } -export function extractWorklogData(body: any) { +/** + * Builds output schema for sprint-related webhook events + */ +export function buildSprintOutputs(): Record { + return { + webhookEvent: { + type: 'string', + description: 'The webhook event type (e.g., sprint_started, sprint_closed)', + }, + timestamp: { + type: 'number', + description: 'Timestamp of the webhook event', + }, + user: { + displayName: { + type: 'string', + description: 'Display name of the user who triggered the event', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who triggered the event', + }, + emailAddress: { + type: 'string', + description: 'Email address of the user who triggered the event', + }, + }, + sprint: { + id: { + type: 'number', + description: 'Sprint ID', + }, + self: { + type: 'string', + description: 'REST API URL for this sprint', + }, + state: { + type: 'string', + description: 'Sprint state (future, active, closed)', + }, + name: { + type: 'string', + description: 'Sprint name', + }, + startDate: { + type: 'string', + description: 'Sprint start date (ISO format)', + }, + endDate: { + type: 'string', + description: 'Sprint end date (ISO format)', + }, + completeDate: { + type: 'string', + description: 'Sprint completion date (ISO format)', + }, + originBoardId: { + type: 'number', + description: 'Board ID the sprint belongs to', + }, + goal: { + type: 'string', + description: 'Sprint goal', + }, + createdDate: { + type: 'string', + description: 'Sprint creation date (ISO format)', + }, + }, + } +} + +/** + * Builds output schema for project_created webhook events + */ +export function buildProjectCreatedOutputs(): Record { + return { + webhookEvent: { + type: 'string', + description: 'The webhook event type (project_created)', + }, + timestamp: { + type: 'number', + description: 'Timestamp of the webhook event', + }, + user: { + displayName: { + type: 'string', + description: 'Display name of the user who triggered the event', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who triggered the event', + }, + emailAddress: { + type: 'string', + description: 'Email address of the user who triggered the event', + }, + }, + project: { + id: { + type: 'string', + description: 'Project ID', + }, + key: { + type: 'string', + description: 'Project key', + }, + name: { + type: 'string', + description: 'Project name', + }, + self: { + type: 'string', + description: 'REST API URL for this project', + }, + projectTypeKey: { + type: 'string', + description: 'Project type (e.g., software, business)', + }, + lead: { + displayName: { + type: 'string', + description: 'Project lead display name', + }, + accountId: { + type: 'string', + description: 'Project lead account ID', + }, + }, + }, + } +} + +/** + * Builds output schema for version_released webhook events + */ +export function buildVersionReleasedOutputs(): Record { + return { + webhookEvent: { + type: 'string', + description: 'The webhook event type (jira:version_released)', + }, + timestamp: { + type: 'number', + description: 'Timestamp of the webhook event', + }, + user: { + displayName: { + type: 'string', + description: 'Display name of the user who triggered the event', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who triggered the event', + }, + emailAddress: { + type: 'string', + description: 'Email address of the user who triggered the event', + }, + }, + version: { + id: { + type: 'string', + description: 'Version ID', + }, + name: { + type: 'string', + description: 'Version name', + }, + self: { + type: 'string', + description: 'REST API URL for this version', + }, + released: { + type: 'boolean', + description: 'Whether the version is released', + }, + releaseDate: { + type: 'string', + description: 'Release date (ISO format)', + }, + projectId: { + type: 'number', + description: 'Project ID the version belongs to', + }, + description: { + type: 'string', + description: 'Version description', + }, + archived: { + type: 'boolean', + description: 'Whether the version is archived', + }, + }, + } +} + +/** + * Extracts sprint data from a Jira webhook payload + */ +export function extractSprintData(body: unknown) { + const obj = body as Record + return { + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + sprint: obj.sprint || {}, + } +} + +/** + * Extracts project data from a Jira webhook payload + */ +export function extractProjectData(body: unknown) { + const obj = body as Record + return { + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + project: obj.project || {}, + } +} + +/** + * Extracts version data from a Jira webhook payload + */ +export function extractVersionData(body: unknown) { + const obj = body as Record return { - webhookEvent: body.webhookEvent, - timestamp: body.timestamp, - issue: body.issue || {}, - worklog: body.worklog || {}, + webhookEvent: obj.webhookEvent, + timestamp: obj.timestamp, + user: obj.user || null, + version: obj.version || {}, } } diff --git a/apps/sim/triggers/jira/version_released.ts b/apps/sim/triggers/jira/version_released.ts new file mode 100644 index 00000000000..8085bb8e2ff --- /dev/null +++ b/apps/sim/triggers/jira/version_released.ts @@ -0,0 +1,70 @@ +import { JiraIcon } from '@/components/icons' +import { buildVersionReleasedOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Version Released Trigger + * Triggers when a version/release is released + */ +export const jiraVersionReleasedTrigger: TriggerConfig = { + id: 'jira_version_released', + name: 'Jira Version Released', + provider: 'jira', + description: 'Trigger workflow when a version is released in Jira', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_version_released', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_version_released', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('jira:version_released'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_version_released', + }, + }, + ], + + outputs: buildVersionReleasedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/worklog_deleted.ts b/apps/sim/triggers/jira/worklog_deleted.ts new file mode 100644 index 00000000000..d2a1c17d14c --- /dev/null +++ b/apps/sim/triggers/jira/worklog_deleted.ts @@ -0,0 +1,83 @@ +import { JiraIcon } from '@/components/icons' +import { buildWorklogOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Worklog Deleted Trigger + * Triggers when a worklog entry is deleted from an issue + */ +export const jiraWorklogDeletedTrigger: TriggerConfig = { + id: 'jira_worklog_deleted', + name: 'Jira Worklog Deleted', + provider: 'jira', + description: 'Trigger workflow when a worklog entry is deleted from a Jira issue', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_deleted', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_deleted', + }, + }, + { + id: 'jqlFilter', + title: 'JQL Filter', + type: 'long-input', + placeholder: 'project = PROJ', + description: 'Filter which worklog deletions trigger this workflow using JQL', + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_deleted', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('worklog_deleted'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_deleted', + }, + }, + ], + + outputs: buildWorklogOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jira/worklog_updated.ts b/apps/sim/triggers/jira/worklog_updated.ts new file mode 100644 index 00000000000..5a26c7f7e33 --- /dev/null +++ b/apps/sim/triggers/jira/worklog_updated.ts @@ -0,0 +1,83 @@ +import { JiraIcon } from '@/components/icons' +import { buildWorklogOutputs, jiraSetupInstructions } from '@/triggers/jira/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Jira Worklog Updated Trigger + * Triggers when a worklog entry is updated on an issue + */ +export const jiraWorklogUpdatedTrigger: TriggerConfig = { + id: 'jira_worklog_updated', + name: 'Jira Worklog Updated', + provider: 'jira', + description: 'Trigger workflow when a worklog entry is updated on a Jira issue', + version: '1.0.0', + icon: JiraIcon, + + subBlocks: [ + { + id: 'webhookUrlDisplay', + title: 'Webhook URL', + type: 'short-input', + readOnly: true, + showCopyButton: true, + useWebhookUrl: true, + placeholder: 'Webhook URL will be generated', + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_updated', + }, + }, + { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_updated', + }, + }, + { + id: 'jqlFilter', + title: 'JQL Filter', + type: 'long-input', + placeholder: 'project = PROJ', + description: 'Filter which worklog updates trigger this workflow using JQL', + required: false, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_updated', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: jiraSetupInstructions('worklog_updated'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'jira_worklog_updated', + }, + }, + ], + + outputs: buildWorklogOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jsm/index.ts b/apps/sim/triggers/jsm/index.ts new file mode 100644 index 00000000000..c76b4d0597d --- /dev/null +++ b/apps/sim/triggers/jsm/index.ts @@ -0,0 +1,10 @@ +/** + * Jira Service Management Triggers + * Export all JSM webhook triggers + */ + +export { jsmRequestCommentedTrigger } from './request_commented' +export { jsmRequestCreatedTrigger } from './request_created' +export { jsmRequestResolvedTrigger } from './request_resolved' +export { jsmRequestUpdatedTrigger } from './request_updated' +export { jsmWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/jsm/request_commented.ts b/apps/sim/triggers/jsm/request_commented.ts new file mode 100644 index 00000000000..4487c0c7e34 --- /dev/null +++ b/apps/sim/triggers/jsm/request_commented.ts @@ -0,0 +1,41 @@ +import { JiraServiceManagementIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJsmCommentOutputs, + buildJsmExtraFields, + jsmSetupInstructions, + jsmTriggerOptions, +} from '@/triggers/jsm/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * JSM Request Commented Trigger + * + * Triggers when a comment is added to a service request (public or internal). + */ +export const jsmRequestCommentedTrigger: TriggerConfig = { + id: 'jsm_request_commented', + name: 'JSM Request Commented', + provider: 'jsm', + description: 'Trigger workflow when a comment is added to a Jira Service Management request', + version: '1.0.0', + icon: JiraServiceManagementIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'jsm_request_commented', + triggerOptions: jsmTriggerOptions, + setupInstructions: jsmSetupInstructions('comment_created'), + extraFields: buildJsmExtraFields('jsm_request_commented'), + }), + + outputs: buildJsmCommentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jsm/request_created.ts b/apps/sim/triggers/jsm/request_created.ts new file mode 100644 index 00000000000..6a4691709f9 --- /dev/null +++ b/apps/sim/triggers/jsm/request_created.ts @@ -0,0 +1,43 @@ +import { JiraServiceManagementIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJsmExtraFields, + buildJsmRequestOutputs, + jsmSetupInstructions, + jsmTriggerOptions, +} from '@/triggers/jsm/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * JSM Request Created Trigger + * + * Primary trigger — includes the dropdown for selecting trigger type. + * Triggers when a new service request is created in Jira Service Management. + */ +export const jsmRequestCreatedTrigger: TriggerConfig = { + id: 'jsm_request_created', + name: 'JSM Request Created', + provider: 'jsm', + description: 'Trigger workflow when a new service request is created in Jira Service Management', + version: '1.0.0', + icon: JiraServiceManagementIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'jsm_request_created', + triggerOptions: jsmTriggerOptions, + includeDropdown: true, + setupInstructions: jsmSetupInstructions('jira:issue_created'), + extraFields: buildJsmExtraFields('jsm_request_created'), + }), + + outputs: buildJsmRequestOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jsm/request_resolved.ts b/apps/sim/triggers/jsm/request_resolved.ts new file mode 100644 index 00000000000..2dd216efb64 --- /dev/null +++ b/apps/sim/triggers/jsm/request_resolved.ts @@ -0,0 +1,45 @@ +import { JiraServiceManagementIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJsmExtraFields, + buildJsmRequestUpdatedOutputs, + jsmSetupInstructions, + jsmTriggerOptions, +} from '@/triggers/jsm/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * JSM Request Resolved Trigger + * + * Triggers when a service request is resolved (status changed to Resolved, Done, or Closed). + * This is a specialized issue_updated event filtered by changelog status changes. + */ +export const jsmRequestResolvedTrigger: TriggerConfig = { + id: 'jsm_request_resolved', + name: 'JSM Request Resolved', + provider: 'jsm', + description: 'Trigger workflow when a service request is resolved in Jira Service Management', + version: '1.0.0', + icon: JiraServiceManagementIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'jsm_request_resolved', + triggerOptions: jsmTriggerOptions, + setupInstructions: jsmSetupInstructions( + 'jira:issue_updated', + 'This trigger fires when a request status changes to Resolved, Done, or Closed.' + ), + extraFields: buildJsmExtraFields('jsm_request_resolved'), + }), + + outputs: buildJsmRequestUpdatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jsm/request_updated.ts b/apps/sim/triggers/jsm/request_updated.ts new file mode 100644 index 00000000000..fe078b9d5fc --- /dev/null +++ b/apps/sim/triggers/jsm/request_updated.ts @@ -0,0 +1,41 @@ +import { JiraServiceManagementIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJsmExtraFields, + buildJsmRequestUpdatedOutputs, + jsmSetupInstructions, + jsmTriggerOptions, +} from '@/triggers/jsm/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * JSM Request Updated Trigger + * + * Triggers when a service request is updated in Jira Service Management. + */ +export const jsmRequestUpdatedTrigger: TriggerConfig = { + id: 'jsm_request_updated', + name: 'JSM Request Updated', + provider: 'jsm', + description: 'Trigger workflow when a service request is updated in Jira Service Management', + version: '1.0.0', + icon: JiraServiceManagementIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'jsm_request_updated', + triggerOptions: jsmTriggerOptions, + setupInstructions: jsmSetupInstructions('jira:issue_updated'), + extraFields: buildJsmExtraFields('jsm_request_updated'), + }), + + outputs: buildJsmRequestUpdatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} diff --git a/apps/sim/triggers/jsm/utils.ts b/apps/sim/triggers/jsm/utils.ts new file mode 100644 index 00000000000..984784bf9c6 --- /dev/null +++ b/apps/sim/triggers/jsm/utils.ts @@ -0,0 +1,412 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Shared trigger dropdown options for all JSM triggers + */ +export const jsmTriggerOptions = [ + { label: 'Request Created', id: 'jsm_request_created' }, + { label: 'Request Updated', id: 'jsm_request_updated' }, + { label: 'Request Commented', id: 'jsm_request_commented' }, + { label: 'Request Resolved', id: 'jsm_request_resolved' }, + { label: 'Generic Webhook (All Events)', id: 'jsm_webhook' }, +] + +/** + * Generates setup instructions for JSM webhooks. + * JSM uses the Jira webhook infrastructure with service desk context. + */ +export function jsmSetupInstructions(eventType: string, additionalNotes?: string): string { + const instructions = [ + 'Note: You must have admin permissions in your Jira workspace to create webhooks. JSM uses the Jira webhook system. See the Jira webhook documentation for details.', + 'In Jira, navigate to Settings > System > WebHooks.', + 'Click "Create a WebHook" to add a new webhook.', + 'Paste the Webhook URL from above into the URL field.', + 'Optionally, enter the Webhook Secret from above into the secret field for added security.', + `Select the events you want to trigger this workflow. For this trigger, select ${eventType}.`, + 'Optionally add a JQL filter to restrict webhooks to your service desk project (e.g., project = SD).', + 'Click "Create" to activate the webhook.', + ] + + if (additionalNotes) { + instructions.push(additionalNotes) + } + + return instructions + .map( + (instruction, index) => + `
${index === 0 ? instruction : `${index}. ${instruction}`}
` + ) + .join('') +} + +/** + * Webhook secret field for JSM triggers + */ +function jsmWebhookSecretField(triggerId: string): SubBlockConfig { + return { + id: 'webhookSecret', + title: 'Webhook Secret', + type: 'short-input', + placeholder: 'Enter a strong secret', + description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature', + password: true, + required: false, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + } +} + +/** + * Extra fields for JSM triggers (webhook secret + JQL filter) + */ +export function buildJsmExtraFields(triggerId: string): SubBlockConfig[] { + return [ + jsmWebhookSecretField(triggerId), + { + id: 'jqlFilter', + title: 'JQL Filter', + type: 'long-input', + placeholder: 'project = SD AND issuetype = "Service Request"', + description: + 'Filter which service desk requests trigger this workflow using JQL (Jira Query Language)', + required: false, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Base webhook output fields shared across all JSM triggers + */ +function buildBaseWebhookOutputs(): Record { + return { + webhookEvent: { + type: 'string', + description: + 'The webhook event type (e.g., jira:issue_created, jira:issue_updated, comment_created)', + }, + timestamp: { + type: 'number', + description: 'Timestamp of the webhook event', + }, + user: { + displayName: { + type: 'string', + description: 'Display name of the user who triggered the event', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who triggered the event', + }, + }, + + issue: { + id: { + type: 'string', + description: 'Jira issue ID', + }, + key: { + type: 'string', + description: 'Issue key (e.g., SD-123)', + }, + self: { + type: 'string', + description: 'REST API URL for this issue', + }, + fields: { + summary: { + type: 'string', + description: 'Request summary/title', + }, + status: { + name: { + type: 'string', + description: 'Current status name', + }, + id: { + type: 'string', + description: 'Status ID', + }, + statusCategory: { + type: 'json', + description: 'Status category information', + }, + }, + priority: { + name: { + type: 'string', + description: 'Priority name', + }, + id: { + type: 'string', + description: 'Priority ID', + }, + }, + issuetype: { + name: { + type: 'string', + description: 'Issue type name (e.g., Service Request, Incident)', + }, + id: { + type: 'string', + description: 'Issue type ID', + }, + }, + project: { + key: { + type: 'string', + description: 'Project key', + }, + name: { + type: 'string', + description: 'Project name', + }, + id: { + type: 'string', + description: 'Project ID', + }, + }, + reporter: { + displayName: { + type: 'string', + description: 'Reporter display name', + }, + accountId: { + type: 'string', + description: 'Reporter account ID', + }, + emailAddress: { + type: 'string', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', + }, + }, + assignee: { + displayName: { + type: 'string', + description: 'Assignee display name', + }, + accountId: { + type: 'string', + description: 'Assignee account ID', + }, + emailAddress: { + type: 'string', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', + }, + }, + creator: { + displayName: { + type: 'string', + description: 'Creator display name', + }, + accountId: { + type: 'string', + description: 'Creator account ID', + }, + emailAddress: { + type: 'string', + description: + 'Email address (Jira Server only — not available in Jira Cloud webhook payloads)', + }, + }, + created: { + type: 'string', + description: 'Request creation date (ISO format)', + }, + updated: { + type: 'string', + description: 'Last updated date (ISO format)', + }, + duedate: { + type: 'string', + description: 'Due date for the request', + }, + labels: { + type: 'array', + description: 'Array of labels applied to this request', + }, + resolution: { + name: { + type: 'string', + description: 'Resolution name (e.g., Done, Fixed)', + }, + id: { + type: 'string', + description: 'Resolution ID', + }, + }, + }, + }, + } +} + +/** + * Outputs for request created triggers + */ +export function buildJsmRequestOutputs(): Record { + return { + ...buildBaseWebhookOutputs(), + issue_event_type_name: { + type: 'string', + description: 'Issue event type name from Jira', + }, + } +} + +/** + * Outputs for request updated/resolved triggers (includes changelog) + */ +export function buildJsmRequestUpdatedOutputs(): Record { + return { + ...buildBaseWebhookOutputs(), + issue_event_type_name: { + type: 'string', + description: 'Issue event type name from Jira', + }, + changelog: { + id: { + type: 'string', + description: 'Changelog ID', + }, + items: { + type: 'array', + description: + 'Array of changed items. Each item contains field, fieldtype, from, fromString, to, toString', + }, + }, + } +} + +/** + * Outputs for comment triggers + */ +export function buildJsmCommentOutputs(): Record { + return { + ...buildBaseWebhookOutputs(), + + comment: { + id: { + type: 'string', + description: 'Comment ID', + }, + body: { + type: 'json', + description: + 'Comment body in Atlassian Document Format (ADF). On Jira Server this may be a plain string.', + }, + author: { + displayName: { + type: 'string', + description: 'Comment author display name', + }, + accountId: { + type: 'string', + description: 'Comment author account ID', + }, + emailAddress: { + type: 'string', + description: 'Comment author email address', + }, + }, + updateAuthor: { + displayName: { + type: 'string', + description: 'Display name of the user who last updated the comment', + }, + accountId: { + type: 'string', + description: 'Account ID of the user who last updated the comment', + }, + }, + created: { + type: 'string', + description: 'Comment creation date (ISO format)', + }, + updated: { + type: 'string', + description: 'Comment last updated date (ISO format)', + }, + }, + } +} + +/** + * Checks whether a JSM webhook event matches the configured trigger. + * + * JSM events come through Jira's webhook system. The matching logic considers: + * - The webhook event type (jira:issue_created, jira:issue_updated, comment_created) + * - The issue_event_type_name for finer-grained matching + * - Changelog items for approval, SLA, and resolution events + */ +export function isJsmEventMatch( + triggerId: string, + webhookEvent: string, + issueEventTypeName?: string, + changelog?: { items?: Array<{ field?: string; toString?: string }> } +): boolean { + switch (triggerId) { + case 'jsm_request_created': + return webhookEvent === 'jira:issue_created' || issueEventTypeName === 'issue_created' + + case 'jsm_request_updated': + return ( + webhookEvent === 'jira:issue_updated' || + issueEventTypeName === 'issue_updated' || + issueEventTypeName === 'issue_generic' + ) + + case 'jsm_request_commented': + return webhookEvent === 'comment_created' + + case 'jsm_request_resolved': { + if (webhookEvent !== 'jira:issue_updated' && issueEventTypeName !== 'issue_updated') { + return false + } + const resolvedItems = changelog?.items ?? [] + return resolvedItems.some( + (item) => + item.field === 'status' && + (item.toString?.toLowerCase() === 'resolved' || + item.toString?.toLowerCase() === 'done' || + item.toString?.toLowerCase() === 'closed') + ) + } + + case 'jsm_webhook': + return true + + default: + return false + } +} + +/** + * Extracts request data from a JSM webhook payload + */ +export function extractRequestData(body: Record) { + return { + webhookEvent: body.webhookEvent, + timestamp: body.timestamp, + user: body.user || null, + issue_event_type_name: body.issue_event_type_name, + issue: body.issue || {}, + changelog: body.changelog, + } +} + +/** + * Extracts comment data from a JSM webhook payload + */ +export function extractCommentData(body: Record) { + return { + webhookEvent: body.webhookEvent, + timestamp: body.timestamp, + user: body.user || null, + issue: body.issue || {}, + comment: body.comment || {}, + } +} diff --git a/apps/sim/triggers/jsm/webhook.ts b/apps/sim/triggers/jsm/webhook.ts new file mode 100644 index 00000000000..3ef23743178 --- /dev/null +++ b/apps/sim/triggers/jsm/webhook.ts @@ -0,0 +1,87 @@ +import { JiraServiceManagementIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJsmExtraFields, + buildJsmRequestOutputs, + jsmSetupInstructions, + jsmTriggerOptions, +} from '@/triggers/jsm/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Generic JSM Webhook Trigger + * + * Captures all Jira Service Management webhook events. + */ +export const jsmWebhookTrigger: TriggerConfig = { + id: 'jsm_webhook', + name: 'JSM Webhook (All Events)', + provider: 'jsm', + description: 'Trigger workflow on any Jira Service Management webhook event', + version: '1.0.0', + icon: JiraServiceManagementIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'jsm_webhook', + triggerOptions: jsmTriggerOptions, + setupInstructions: jsmSetupInstructions('All Events'), + extraFields: buildJsmExtraFields('jsm_webhook'), + }), + + outputs: { + ...buildJsmRequestOutputs(), + changelog: { + id: { + type: 'string', + description: 'Changelog ID', + }, + items: { + type: 'array', + description: + 'Array of changed items. Each item contains field, fieldtype, from, fromString, to, toString', + }, + }, + comment: { + id: { + type: 'string', + description: 'Comment ID', + }, + body: { + type: 'json', + description: + 'Comment body in Atlassian Document Format (ADF). On Jira Server this may be a plain string.', + }, + author: { + displayName: { + type: 'string', + description: 'Comment author display name', + }, + accountId: { + type: 'string', + description: 'Comment author account ID', + }, + emailAddress: { + type: 'string', + description: 'Comment author email address', + }, + }, + created: { + type: 'string', + description: 'Comment creation date (ISO format)', + }, + updated: { + type: 'string', + description: 'Comment last updated date (ISO format)', + }, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hub-Signature': 'sha256=...', + 'X-Atlassian-Webhook-Identifier': 'unique-webhook-id', + }, + }, +} 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..62502116307 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -56,19 +56,26 @@ import { import { confluenceAttachmentCreatedTrigger, confluenceAttachmentRemovedTrigger, + confluenceAttachmentUpdatedTrigger, confluenceBlogCreatedTrigger, confluenceBlogRemovedTrigger, + confluenceBlogRestoredTrigger, confluenceBlogUpdatedTrigger, confluenceCommentCreatedTrigger, confluenceCommentRemovedTrigger, + confluenceCommentUpdatedTrigger, confluenceLabelAddedTrigger, confluenceLabelRemovedTrigger, confluencePageCreatedTrigger, confluencePageMovedTrigger, + confluencePagePermissionsUpdatedTrigger, confluencePageRemovedTrigger, + confluencePageRestoredTrigger, confluencePageUpdatedTrigger, confluenceSpaceCreatedTrigger, + confluenceSpaceRemovedTrigger, confluenceSpaceUpdatedTrigger, + confluenceUserCreatedTrigger, confluenceWebhookTrigger, } from '@/triggers/confluence' import { fathomNewMeetingTrigger, fathomWebhookTrigger } from '@/triggers/fathom' @@ -153,13 +160,29 @@ import { intercomWebhookTrigger, } from '@/triggers/intercom' import { + jiraCommentDeletedTrigger, + jiraCommentUpdatedTrigger, jiraIssueCommentedTrigger, jiraIssueCreatedTrigger, jiraIssueDeletedTrigger, jiraIssueUpdatedTrigger, + jiraProjectCreatedTrigger, + jiraSprintClosedTrigger, + jiraSprintCreatedTrigger, + jiraSprintStartedTrigger, + jiraVersionReleasedTrigger, jiraWebhookTrigger, jiraWorklogCreatedTrigger, + jiraWorklogDeletedTrigger, + jiraWorklogUpdatedTrigger, } from '@/triggers/jira' +import { + jsmRequestCommentedTrigger, + jsmRequestCreatedTrigger, + jsmRequestResolvedTrigger, + jsmRequestUpdatedTrigger, + jsmWebhookTrigger, +} from '@/triggers/jsm' import { lemlistEmailBouncedTrigger, lemlistEmailClickedTrigger, @@ -207,6 +230,17 @@ import { microsoftTeamsChatSubscriptionTrigger, microsoftTeamsWebhookTrigger, } from '@/triggers/microsoftteams' +import { + mondayColumnChangedTrigger, + mondayItemArchivedTrigger, + mondayItemCreatedTrigger, + mondayItemDeletedTrigger, + mondayItemMovedTrigger, + mondayItemNameChangedTrigger, + mondayStatusChangedTrigger, + mondaySubitemCreatedTrigger, + mondayUpdateCreatedTrigger, +} from '@/triggers/monday' import { notionCommentCreatedTrigger, notionDatabaseCreatedTrigger, @@ -337,6 +371,13 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { confluence_space_updated: confluenceSpaceUpdatedTrigger, confluence_label_added: confluenceLabelAddedTrigger, confluence_label_removed: confluenceLabelRemovedTrigger, + confluence_comment_updated: confluenceCommentUpdatedTrigger, + confluence_attachment_updated: confluenceAttachmentUpdatedTrigger, + confluence_page_restored: confluencePageRestoredTrigger, + confluence_blog_restored: confluenceBlogRestoredTrigger, + confluence_space_removed: confluenceSpaceRemovedTrigger, + confluence_page_permissions_updated: confluencePagePermissionsUpdatedTrigger, + confluence_user_created: confluenceUserCreatedTrigger, generic_webhook: genericWebhookTrigger, greenhouse_candidate_hired: greenhouseCandidateHiredTrigger, greenhouse_new_application: greenhouseNewApplicationTrigger, @@ -383,7 +424,21 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { jira_issue_updated: jiraIssueUpdatedTrigger, jira_issue_deleted: jiraIssueDeletedTrigger, jira_issue_commented: jiraIssueCommentedTrigger, + jira_comment_updated: jiraCommentUpdatedTrigger, + jira_comment_deleted: jiraCommentDeletedTrigger, jira_worklog_created: jiraWorklogCreatedTrigger, + jira_worklog_updated: jiraWorklogUpdatedTrigger, + jira_worklog_deleted: jiraWorklogDeletedTrigger, + jira_sprint_created: jiraSprintCreatedTrigger, + jira_sprint_started: jiraSprintStartedTrigger, + jira_sprint_closed: jiraSprintClosedTrigger, + jira_project_created: jiraProjectCreatedTrigger, + jira_version_released: jiraVersionReleasedTrigger, + jsm_request_created: jsmRequestCreatedTrigger, + jsm_request_updated: jsmRequestUpdatedTrigger, + jsm_request_commented: jsmRequestCommentedTrigger, + jsm_request_resolved: jsmRequestResolvedTrigger, + jsm_webhook: jsmWebhookTrigger, lemlist_webhook: lemlistWebhookTrigger, lemlist_email_replied: lemlistEmailRepliedTrigger, lemlist_email_opened: lemlistEmailOpenedTrigger, @@ -423,6 +478,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, diff --git a/apps/sim/triggers/types.ts b/apps/sim/triggers/types.ts index 69e5a5d2fbd..2fb5498c92b 100644 --- a/apps/sim/triggers/types.ts +++ b/apps/sim/triggers/types.ts @@ -1,6 +1,6 @@ export interface TriggerOutput { type?: string - description?: string + description?: string | TriggerOutput [key: string]: TriggerOutput | string | undefined }