v0.6.49: deploy sockets event, resolver, logs improvements, monday.com integration, atlassian triggers

This commit is contained in:
Waleed
2026-04-16 20:31:23 -07:00
committed by GitHub
135 changed files with 9014 additions and 465 deletions

View File

@@ -3602,6 +3602,29 @@ export function OpenRouterIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function MondayIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 -50 256 256'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid'
>
<g>
<path
d='M31.8458633,153.488694 C20.3244423,153.513586 9.68073708,147.337265 3.98575204,137.321731 C-1.62714067,127.367831 -1.29055839,115.129325 4.86093879,105.498969 L62.2342919,15.4033556 C68.2125882,5.54538256 79.032489,-0.333585033 90.5563073,0.0146553508 C102.071737,0.290611552 112.546041,6.74705604 117.96667,16.9106216 C123.315033,27.0238906 122.646488,39.1914174 116.240607,48.6847625 L58.9037201,138.780375 C52.9943022,147.988884 42.7873202,153.537154 31.8458633,153.488694 L31.8458633,153.488694 Z'
fill='#F62B54'
/>
<path
d='M130.25575,153.488484 C118.683837,153.488484 108.035731,147.301291 102.444261,137.358197 C96.8438154,127.431292 97.1804475,115.223704 103.319447,105.620522 L160.583402,15.7315506 C166.47539,5.73210989 177.327374,-0.284878136 188.929728,0.0146553508 C200.598885,0.269918151 211.174058,6.7973526 216.522421,17.0078646 C221.834319,27.2183766 221.056375,39.4588356 214.456008,48.9278699 L157.204209,138.816842 C151.313487,147.985468 141.153618,153.5168 130.25575,153.488484 Z'
fill='#FFCC00'
/>
<ellipse fill='#00CA72' cx='226.465527' cy='125.324379' rx='29.5375538' ry='28.9176274' />
</g>
</svg>
)
}
export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'>

View File

@@ -119,6 +119,7 @@ import {
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MistralIcon,
MondayIcon,
MongoDBIcon,
MySQLIcon,
Neo4jIcon,
@@ -327,6 +328,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse: MistralIcon,
mistral_parse_v3: MistralIcon,
monday: MondayIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,

View File

@@ -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." },
]} />

View File

@@ -115,6 +115,7 @@
"microsoft_planner",
"microsoft_teams",
"mistral_parse",
"monday",
"mongodb",
"mysql",
"neo4j",

View File

@@ -0,0 +1,387 @@
---
title: Monday
description: Manage Monday.com boards, items, and groups
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="monday"
color="#FFFFFF"
/>
## 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 |

View File

@@ -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\) |

View File

@@ -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,10 @@ 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\) |
@@ -352,19 +743,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 +764,213 @@ 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 |
---
### 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\) |
| ↳ `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 |

View File

@@ -0,0 +1,314 @@
---
title: Jsm
description: Available Jsm triggers for automating workflows
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="jsm"
color="#6B7280"
/>
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\) |

View File

@@ -27,9 +27,11 @@
"imap",
"intercom",
"jira",
"jsm",
"lemlist",
"linear",
"microsoft-teams",
"monday",
"notion",
"outlook",
"resend",

View File

@@ -0,0 +1,215 @@
---
title: Monday
description: Available Monday triggers for automating workflows
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="monday"
color="#FFFFFF"
/>
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 |

View File

@@ -119,6 +119,7 @@ import {
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MistralIcon,
MondayIcon,
MongoDBIcon,
MySQLIcon,
Neo4jIcon,
@@ -312,6 +313,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse_v3: MistralIcon,
monday: MondayIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,

View File

@@ -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",

View File

@@ -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 })),

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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', () => ({

View File

@@ -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',

View File

@@ -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

View File

@@ -293,7 +293,7 @@ function LineChartComponent({
className='inline-block h-[6px] w-[6px] rounded-xs'
style={{ backgroundColor: resolvedColors[s.id || ''] || s.color }}
/>
<span style={{ color: 'hsl(var(--muted-foreground))' }}>{s.label}</span>
<span className='text-[var(--text-muted)]'>{s.label}</span>
</button>
)
})}

View File

@@ -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 triggerDisplayLabel =
triggers.length === 0
? 'Trigger'
: triggers.length === 1
? (triggerOptions.find((t) => t.value === triggers[0])?.label ?? '1 selected')
: `${triggers.length} triggers`
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 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 */}
<DropdownMenu open={datePickerOpen} onOpenChange={setDatePickerOpen}>
<DropdownMenuTrigger asChild>
<div>
<Combobox
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='Time'
overlayContent={
<span className='truncate text-[var(--text-primary)]'>
{timeDisplayLabel}
</span>
}
size='sm'
align='end'
className='h-[32px] w-[120px] rounded-md'
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
side='bottom'
<div className='relative'>
<Combobox
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='Time'
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{timeDisplayLabel}</span>
}
size='sm'
align='end'
sideOffset={4}
collisionPadding={16}
className='w-auto p-0'
>
<DatePicker
mode='range'
startDate={startDate}
endDate={endDate}
onRangeChange={handleDateRangeApply}
onCancel={handleDatePickerCancel}
inline
/>
</DropdownMenuContent>
</DropdownMenu>
className='h-[32px] w-[120px] rounded-md'
/>
<DatePicker
mode='range'
showTrigger={false}
open={datePickerOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
if (dateRangeAppliedRef.current) {
dateRangeAppliedRef.current = false
} else {
handleDatePickerCancel()
}
}
}}
startDate={startDate}
endDate={endDate}
onRangeChange={handleDateRangeApply}
onCancel={handleDatePickerCancel}
/>
</div>
</div>
</div>
</div>

View File

@@ -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
<div className='flex flex-col gap-1.5'>
<span className='font-medium text-[var(--text-secondary)] text-caption'>Time Range</span>
<DropdownMenu open={datePickerOpen} onOpenChange={setDatePickerOpen}>
<DropdownMenuTrigger asChild>
<div>
<Combobox
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='All time'
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{timeDisplayLabel}</span>
<div className='relative'>
<Combobox
options={TIME_RANGE_OPTIONS as unknown as ComboboxOption[]}
value={timeRange}
onChange={handleTimeRangeChange}
placeholder='All time'
overlayContent={
<span className='truncate text-[var(--text-primary)]'>{timeDisplayLabel}</span>
}
size='sm'
className='h-[32px] w-full rounded-md'
/>
<DatePicker
mode='range'
showTrigger={false}
open={datePickerOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
if (dateRangeAppliedRef.current) {
dateRangeAppliedRef.current = false
} else {
handleDatePickerCancel()
}
size='sm'
className='h-[32px] w-full rounded-md'
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
side='bottom'
align='end'
sideOffset={4}
collisionPadding={16}
className='w-auto p-0'
>
<DatePicker
mode='range'
startDate={startDate}
endDate={endDate}
onRangeChange={handleDateRangeApply}
onCancel={handleDatePickerCancel}
inline
/>
</DropdownMenuContent>
</DropdownMenu>
}
}}
startDate={startDate}
endDate={endDate}
onRangeChange={handleDateRangeApply}
onCancel={handleDatePickerCancel}
/>
</div>
</div>
{filtersActive && (

View File

@@ -55,14 +55,25 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus {
export const STATUS_CONFIG: Record<
LogStatus,
{ variant: React.ComponentProps<typeof Badge>['variant']; label: string; color: string }
{
variant: React.ComponentProps<typeof Badge>['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<string, React.ComponentProps<typeof Badge>['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

View File

@@ -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({
<div className='flex flex-col gap-4'>
<div className='flex items-center justify-between gap-3 rounded-sm border border-[var(--border)] p-2'>
<div className='flex min-w-0 flex-col'>
<span className='truncate text-caption text-[var(--text-primary)]'>
<span className='truncate text-[var(--text-primary)] text-caption'>
{parsed.file.name}
</span>
<span className='text-[var(--text-tertiary)] text-xs'>

View File

@@ -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) {

View File

@@ -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<SocketContextType>({
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,
]

View File

@@ -979,17 +979,24 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
...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<ConfluenceResponse> = {
'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',
],
},

View File

@@ -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',
],
},

View File

@@ -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<JsmResponse> = {
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',
],
},
}

View File

@@ -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<MondayResponse> = {
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<string, unknown> = {
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',
],
},
}

View File

@@ -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<string, BlockConfig> = {
mistral_parse: MistralParseBlock,
mistral_parse_v2: MistralParseV2Block,
mistral_parse_v3: MistralParseV3Block,
monday: MondayBlock,
mongodb: MongoDBBlock,
mothership: MothershipBlock,
mysql: MySQLBlock,

View File

@@ -3602,6 +3602,29 @@ export function OpenRouterIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function MondayIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 -50 256 256'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid'
>
<g>
<path
d='M31.8458633,153.488694 C20.3244423,153.513586 9.68073708,147.337265 3.98575204,137.321731 C-1.62714067,127.367831 -1.29055839,115.129325 4.86093879,105.498969 L62.2342919,15.4033556 C68.2125882,5.54538256 79.032489,-0.333585033 90.5563073,0.0146553508 C102.071737,0.290611552 112.546041,6.74705604 117.96667,16.9106216 C123.315033,27.0238906 122.646488,39.1914174 116.240607,48.6847625 L58.9037201,138.780375 C52.9943022,147.988884 42.7873202,153.537154 31.8458633,153.488694 L31.8458633,153.488694 Z'
fill='#F62B54'
/>
<path
d='M130.25575,153.488484 C118.683837,153.488484 108.035731,147.301291 102.444261,137.358197 C96.8438154,127.431292 97.1804475,115.223704 103.319447,105.620522 L160.583402,15.7315506 C166.47539,5.73210989 177.327374,-0.284878136 188.929728,0.0146553508 C200.598885,0.269918151 211.174058,6.7973526 216.522421,17.0078646 C221.834319,27.2183766 221.056375,39.4588356 214.456008,48.9278699 L157.204209,138.816842 C151.313487,147.985468 141.153618,153.5168 130.25575,153.488484 Z'
fill='#FFCC00'
/>
<ellipse fill='#00CA72' cx='226.465527' cy='125.324379' rx='29.5375538' ry='28.9176274' />
</g>
</svg>
)
}
export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'>

View File

@@ -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)', () => {

View File

@@ -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<string, { type?: string; description?: string } | unknown>
/**
* 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<string, SchemaNode | unknown>
export interface BlockReferenceContext {
blockNameMapping: Record<string, string>
@@ -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<string, unknown> | 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<string, unknown>)
: 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))

View File

@@ -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, '<SkippedBlock.result.items>', 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, '<Block.items>', 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, '<Block.obj>', 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, '<Block.count>', 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, '<Missing.items>', 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()
})
})

View File

@@ -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 "')) {

View File

@@ -383,6 +383,47 @@ describe('BlockResolver', () => {
expect(resolver.resolve('<start.input>', 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('<NormalizeEmail.result.summary>', ctx)).toBe('email summary')
expect(resolver.resolve('<NormalizeCalendar.result.summary>', ctx)).toBe(RESOLVED_EMPTY)
expect(resolver.resolve('<NormalizeCalendar.result>', 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('<NormalizeEmail.result.subject>', ctx)).toBe('hi')
expect(resolver.resolve('<NormalizeEmail.result.summary>', ctx)).toBe(RESOLVED_EMPTY)
}
)
it.concurrent('should fall back to context blockStates', () => {
const workflow = createTestWorkflow([{ id: 'source' }])
const resolver = new BlockResolver(workflow)

View File

@@ -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) }),
])
}

View File

@@ -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<string, string | null>
export type CsvImportMode = 'append' | 'replace'
interface ImportCsvIntoTableParams {

View File

@@ -1257,6 +1257,96 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
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,

View File

@@ -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

View File

@@ -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<string | null>(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,

View File

@@ -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',

View File

@@ -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.

View File

@@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry {
}
export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
['agent']: {
agent: {
parameters: {
properties: {
request: {
@@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['auth']: {
auth: {
parameters: {
properties: {
request: {
@@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['check_deployment_status']: {
check_deployment_status: {
parameters: {
type: 'object',
properties: {
@@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['complete_job']: {
complete_job: {
parameters: {
type: 'object',
properties: {
@@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['context_write']: {
context_write: {
parameters: {
type: 'object',
properties: {
@@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['crawl_website']: {
crawl_website: {
parameters: {
type: 'object',
properties: {
@@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['create_file']: {
create_file: {
parameters: {
type: 'object',
properties: {
@@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['create_folder']: {
create_folder: {
parameters: {
type: 'object',
properties: {
@@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['create_job']: {
create_job: {
parameters: {
type: 'object',
properties: {
@@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['create_workflow']: {
create_workflow: {
parameters: {
type: 'object',
properties: {
@@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['create_workspace_mcp_server']: {
create_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['debug']: {
debug: {
parameters: {
properties: {
context: {
@@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['delete_file']: {
delete_file: {
parameters: {
type: 'object',
properties: {
@@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['delete_folder']: {
delete_folder: {
parameters: {
type: 'object',
properties: {
@@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['delete_workflow']: {
delete_workflow: {
parameters: {
type: 'object',
properties: {
@@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['delete_workspace_mcp_server']: {
delete_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['deploy']: {
deploy: {
parameters: {
properties: {
request: {
@@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['deploy_api']: {
deploy_api: {
parameters: {
type: 'object',
properties: {
@@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
],
},
},
['deploy_chat']: {
deploy_chat: {
parameters: {
type: 'object',
properties: {
@@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
],
},
},
['deploy_mcp']: {
deploy_mcp: {
parameters: {
type: 'object',
properties: {
@@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
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<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['edit_content']: {
edit_content: {
parameters: {
type: 'object',
properties: {
@@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['edit_workflow']: {
edit_workflow: {
parameters: {
type: 'object',
properties: {
@@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
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<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['generate_api_key']: {
generate_api_key: {
parameters: {
type: 'object',
properties: {
@@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['generate_image']: {
generate_image: {
parameters: {
type: 'object',
properties: {
@@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['generate_visualization']: {
generate_visualization: {
parameters: {
type: 'object',
properties: {
@@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_block_outputs']: {
get_block_outputs: {
parameters: {
type: 'object',
properties: {
@@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_block_upstream_references']: {
get_block_upstream_references: {
parameters: {
type: 'object',
properties: {
@@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_deployed_workflow_state']: {
get_deployed_workflow_state: {
parameters: {
type: 'object',
properties: {
@@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_deployment_version']: {
get_deployment_version: {
parameters: {
type: 'object',
properties: {
@@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_execution_summary']: {
get_execution_summary: {
parameters: {
type: 'object',
properties: {
@@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_job_logs']: {
get_job_logs: {
parameters: {
type: 'object',
properties: {
@@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_page_contents']: {
get_page_contents: {
parameters: {
type: 'object',
properties: {
@@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
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<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['get_workflow_logs']: {
get_workflow_logs: {
parameters: {
type: 'object',
properties: {
@@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['glob']: {
glob: {
parameters: {
type: 'object',
properties: {
@@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['grep']: {
grep: {
parameters: {
type: 'object',
properties: {
@@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['job']: {
job: {
parameters: {
properties: {
request: {
@@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['knowledge']: {
knowledge: {
parameters: {
properties: {
request: {
@@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['knowledge_base']: {
knowledge_base: {
parameters: {
type: 'object',
properties: {
@@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['list_folders']: {
list_folders: {
parameters: {
type: 'object',
properties: {
@@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
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<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['manage_credential']: {
manage_credential: {
parameters: {
type: 'object',
properties: {
@@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['manage_custom_tool']: {
manage_custom_tool: {
parameters: {
type: 'object',
properties: {
@@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['manage_job']: {
manage_job: {
parameters: {
type: 'object',
properties: {
@@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['manage_mcp_tool']: {
manage_mcp_tool: {
parameters: {
type: 'object',
properties: {
@@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['manage_skill']: {
manage_skill: {
parameters: {
type: 'object',
properties: {
@@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['materialize_file']: {
materialize_file: {
parameters: {
type: 'object',
properties: {
@@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['move_folder']: {
move_folder: {
parameters: {
type: 'object',
properties: {
@@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['move_workflow']: {
move_workflow: {
parameters: {
type: 'object',
properties: {
@@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['oauth_get_auth_link']: {
oauth_get_auth_link: {
parameters: {
type: 'object',
properties: {
@@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['oauth_request_access']: {
oauth_request_access: {
parameters: {
type: 'object',
properties: {
@@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['open_resource']: {
open_resource: {
parameters: {
type: 'object',
properties: {
@@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['read']: {
read: {
parameters: {
type: 'object',
properties: {
@@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['redeploy']: {
redeploy: {
parameters: {
type: 'object',
properties: {
@@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
],
},
},
['rename_file']: {
rename_file: {
parameters: {
type: 'object',
properties: {
@@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['rename_workflow']: {
rename_workflow: {
parameters: {
type: 'object',
properties: {
@@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['research']: {
research: {
parameters: {
properties: {
topic: {
@@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['respond']: {
respond: {
parameters: {
additionalProperties: true,
properties: {
@@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['restore_resource']: {
restore_resource: {
parameters: {
type: 'object',
properties: {
@@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['revert_to_version']: {
revert_to_version: {
parameters: {
type: 'object',
properties: {
@@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['run']: {
run: {
parameters: {
properties: {
context: {
@@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['run_block']: {
run_block: {
parameters: {
type: 'object',
properties: {
@@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['run_from_block']: {
run_from_block: {
parameters: {
type: 'object',
properties: {
@@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['run_workflow']: {
run_workflow: {
parameters: {
type: 'object',
properties: {
@@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['run_workflow_until_block']: {
run_workflow_until_block: {
parameters: {
type: 'object',
properties: {
@@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['scrape_page']: {
scrape_page: {
parameters: {
type: 'object',
properties: {
@@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['search_documentation']: {
search_documentation: {
parameters: {
type: 'object',
properties: {
@@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['search_library_docs']: {
search_library_docs: {
parameters: {
type: 'object',
properties: {
@@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['search_online']: {
search_online: {
parameters: {
type: 'object',
properties: {
@@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['search_patterns']: {
search_patterns: {
parameters: {
type: 'object',
properties: {
@@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['set_block_enabled']: {
set_block_enabled: {
parameters: {
type: 'object',
properties: {
@@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['set_environment_variables']: {
set_environment_variables: {
parameters: {
type: 'object',
properties: {
@@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['set_global_workflow_variables']: {
set_global_workflow_variables: {
parameters: {
type: 'object',
properties: {
@@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['superagent']: {
superagent: {
parameters: {
properties: {
task: {
@@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['table']: {
table: {
parameters: {
properties: {
request: {
@@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['tool_search_tool_regex']: {
tool_search_tool_regex: {
parameters: {
properties: {
case_insensitive: {
@@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['update_job_history']: {
update_job_history: {
parameters: {
type: 'object',
properties: {
@@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['update_workspace_mcp_server']: {
update_workspace_mcp_server: {
parameters: {
type: 'object',
properties: {
@@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['user_memory']: {
user_memory: {
parameters: {
type: 'object',
properties: {
@@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
['user_table']: {
user_table: {
parameters: {
type: 'object',
properties: {
@@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
required: ['success', 'message'],
},
},
['workflow']: {
workflow: {
parameters: {
type: 'object',
},
resultSchema: undefined,
},
['workspace_file']: {
workspace_file: {
parameters: {
type: 'object',
properties: {

View File

@@ -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<string, BlockSt
}
function notifyWorkflowUpdated(workflowId: string): void {
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',

View File

@@ -6,9 +6,9 @@ import {
type ServerToolContext,
} from '@/lib/copilot/tools/server/base-tool'
import { generateId } from '@/lib/core/utils/uuid'
import { COLUMN_TYPES } from '@/lib/table/constants'
import {
buildAutoMapping,
COLUMN_TYPES,
CSV_MAX_BATCH_SIZE,
type CsvHeaderMapping,
CsvImportValidationError,
@@ -17,7 +17,7 @@ import {
parseCsvBuffer,
sanitizeName,
validateMapping,
} from '@/lib/table/csv-import'
} from '@/lib/table'
import {
addTableColumn,
batchInsertRows,

View File

@@ -9,6 +9,7 @@ import {
type ServerToolContext,
} from '@/lib/copilot/tools/server/base-tool'
import { env } from '@/lib/core/config/env'
import { getSocketServerUrl } from '@/lib/core/utils/urls'
import {
applyTargetedLayout,
getTargetedLayoutImpact,
@@ -284,8 +285,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
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',

View File

@@ -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

View File

@@ -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) {

View File

@@ -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'))

View File

@@ -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)
})
})
})

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -33,6 +33,7 @@ import {
MicrosoftPlannerIcon,
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MondayIcon,
NotionIcon,
OutlookIcon,
PipedriveIcon,
@@ -613,6 +614,29 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
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}`)
}

View File

@@ -101,6 +101,7 @@ export type OAuthService =
| 'calcom'
| 'docusign'
| 'github'
| 'monday'
export interface OAuthProviderConfig {
name: string

View File

@@ -337,7 +337,6 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
'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<string, string> = {
'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',
}
/**

View File

@@ -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(),

View File

@@ -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,

View File

@@ -27,6 +27,8 @@ export const confluenceHandler: WebhookProviderHandler = {
extractAttachmentData,
extractSpaceData,
extractLabelData,
extractPagePermissionsData,
extractUserData,
} = await import('@/triggers/confluence/utils')
const providerConfig = (webhook.providerConfig as Record<string, unknown>) || {}
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<string, unknown>) }
}
if (triggerId === 'confluence_user_created') {
return { input: extractUserData(body as Record<string, unknown>) }
}
if (triggerId === 'confluence_webhook') {
const b = body as Record<string, unknown>
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<string, unknown>
const event = obj.event as string | undefined
const timestamp = obj.timestamp ?? ''
const page = obj.page as Record<string, unknown> | undefined
const comment = obj.comment as Record<string, unknown> | undefined
const attachment = obj.attachment as Record<string, unknown> | undefined
const blog = (obj.blog || obj.blogpost) as Record<string, unknown> | undefined
const space = obj.space as Record<string, unknown> | undefined
const user = obj.user as Record<string, unknown> | 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<string, unknown>

View File

@@ -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<FormatInputResult> {
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<string, unknown>) || {}
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<string, unknown>
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<string, unknown>
const issue = obj.issue as Record<string, unknown> | undefined
const comment = obj.comment as Record<string, unknown> | undefined
const worklog = obj.worklog as Record<string, unknown> | undefined
const project = obj.project as Record<string, unknown> | undefined
if (obj.webhookEvent && (issue?.id || project?.id)) {
return `${obj.webhookEvent}:${issue?.id || project?.id}`
const sprint = obj.sprint as Record<string, unknown> | undefined
const version = obj.version as Record<string, unknown> | 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
},

View File

@@ -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<FormatInputResult> {
const { extractRequestData, extractCommentData } = await import('@/triggers/jsm/utils')
const providerConfig = (webhook.providerConfig as Record<string, unknown>) || {}
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId === 'jsm_request_commented') {
return { input: extractCommentData(body as Record<string, unknown>) }
}
// For the generic webhook, pass through the full payload so no data is lost
if (!triggerId || triggerId === 'jsm_webhook') {
const obj = body as Record<string, unknown>
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<string, unknown>) }
},
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined
const obj = body as Record<string, unknown>
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<string, unknown>
const comment = obj.comment as Record<string, unknown> | undefined
const issue = obj.issue as Record<string, unknown> | undefined
const entityId = comment?.id || issue?.id
if (obj.webhookEvent && entityId) {
const ts = obj.timestamp ?? ''
return `jsm:${obj.webhookEvent}:${entityId}:${ts}`
}
return null
},
}

View File

@@ -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<string, unknown>,
userId: string,
requestId: string
): Promise<string> {
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<string, unknown>
// 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<SubscriptionResult | undefined> {
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<void> {
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<FormatInputResult> {
const payload = body as Record<string, unknown>
const event = payload.event as Record<string, unknown> | undefined
if (!event) {
return {
input: payload,
}
}
const input: Record<string, unknown> = {
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<string, unknown>
const event = payload.event as Record<string, unknown> | undefined
if (event?.triggerUuid) {
return String(event.triggerUuid)
}
return null
},
}

View File

@@ -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<string, WebhookProviderHandler> = {
imap: imapHandler,
intercom: intercomHandler,
jira: jiraHandler,
jsm: jsmHandler,
lemlist: lemlistHandler,
linear: linearHandler,
monday: mondayHandler,
resend: resendHandler,
'microsoft-teams': microsoftTeamsHandler,
notion: notionHandler,

View File

@@ -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', () => ({

View File

@@ -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<void> {
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',

View File

@@ -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('*')
})
})

View File

@@ -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, '-')
}
/**

View File

@@ -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<void> {
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',

View File

@@ -7,6 +7,7 @@ export {
performChatUndeploy,
} from './chat-deploy'
export {
notifySocketDeploymentChanged,
type PerformActivateVersionParams,
type PerformActivateVersionResult,
type PerformFullDeployParams,

View File

@@ -23,6 +23,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
'datasetId',
'serviceDeskId',
'impersonateUserEmail',
'boardId',
'awsAccessKeyId',
'awsSecretAccessKey',
'awsRegion',

View File

@@ -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',

View File

@@ -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<void> {
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}`)
}
}

View File

@@ -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<void> {
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}`)
}
}

View File

@@ -138,4 +138,9 @@ export interface IRoomManager {
* Handle workflow update - notify users
*/
handleWorkflowUpdate(workflowId: string): Promise<void>
/**
* Handle workflow deployment change - notify users to refresh deployment state
*/
handleWorkflowDeployed(workflowId: string): Promise<void>
}

View File

@@ -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 {

View File

@@ -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<MondayArchiveItemParams, MondayArchiveItemResponse> =
{
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',
},
},
}

View File

@@ -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<MondayCreateGroupParams, MondayCreateGroupResponse> =
{
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' },
},
},
},
}

View File

@@ -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<MondayCreateItemParams, MondayCreateItemResponse> = {
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<string, unknown> | null
const group = raw.group as Record<string, unknown> | null
const columnValues = ((raw.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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 },
},
},
},
}

View File

@@ -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<string, unknown> | null
const group = raw.group as Record<string, unknown> | null
const columnValues = ((raw.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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 },
},
},
},
}

View File

@@ -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 },
},
},
},
}

View File

@@ -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<MondayDeleteItemParams, MondayDeleteItemResponse> = {
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',
},
},
}

View File

@@ -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<MondayGetBoardParams, MondayGetBoardResponse> = {
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<string, unknown>) => ({
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<string, unknown>) => ({
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' },
},
},
},
},
}

View File

@@ -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<MondayGetItemParams, MondayGetItemResponse> = {
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<string, unknown> | null
const group = raw.group as Record<string, unknown> | null
const columnValues = ((raw.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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 },
},
},
},
}

View File

@@ -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<string, unknown>): {
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<string, unknown> | null
const group = item.group as Record<string, unknown> | null
const columnValues = ((item.column_values as Record<string, unknown>[]) ?? []).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<MondayGetItemsParams, MondayGetItemsResponse> = {
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<string, unknown>[] = []
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',
},
},
}

View File

@@ -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'

View File

@@ -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<MondayListBoardsParams, MondayListBoardsResponse> = {
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<string, unknown>) => ({
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',
},
},
}

View File

@@ -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<string, unknown> | null
const group = raw.group as Record<string, unknown> | null
const columnValues = ((raw.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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 },
},
},
},
}

View File

@@ -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<MondaySearchItemsParams, MondaySearchItemsResponse> =
{
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<string, unknown>) => {
const board = item.board as Record<string, unknown> | null
const group = item.group as Record<string, unknown> | null
const columnValues = ((item.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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,
},
},
}

View File

@@ -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
}
}

View File

@@ -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<MondayUpdateItemParams, MondayUpdateItemResponse> = {
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<string, unknown> | null
const group = raw.group as Record<string, unknown> | null
const columnValues = ((raw.column_values as Record<string, unknown>[]) ?? []).map(
(cv: Record<string, unknown>) => ({
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 },
},
},
},
}

View File

@@ -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<string, string> {
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, unknown>): string | null {
if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
const messages = (data.errors as Array<Record<string, unknown>>)
.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
}

View File

@@ -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<string, ToolConfig> = {
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,

View File

@@ -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',
},
},
}

View File

@@ -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',
},
},
}

View File

@@ -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',
},
},
}

View File

@@ -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'

View File

@@ -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',
},
},
}

Some files were not shown because too many files have changed in this diff Show More