From 5bc2bd639b5db8e78ef38bcaf0d36a5a4598fb9d Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 09:59:41 -0700 Subject: [PATCH 1/7] =?UTF-8?q?fix(context=5Fdev):=20validation=20pass=20?= =?UTF-8?q?=E2=80=94=20add=20search=20numResults/country,=20accuracy=20fix?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive /validate-integration of all 22 context.dev tools against the live API docs found the integration clean (no correctness bugs). Applied the actionable items: - search: expose numResults (10-100) + country inputs (API supported them; users were silently capped at 10 results) - accuracy: scrape_html type description (+doc/docx), map meta description (+sitemapsSkipped), brand links description (+contact) - robustness: trim string query values in appendParam --- .../docs/en/integrations/context_dev.mdx | 16 +++++++++------- .../content/docs/en/integrations/slack.mdx | 3 +++ apps/sim/blocks/blocks/context_dev.ts | 18 ++++++++++++++++++ apps/sim/tools/context_dev/map.ts | 3 ++- apps/sim/tools/context_dev/scrape_html.ts | 3 ++- apps/sim/tools/context_dev/search.ts | 14 ++++++++++++++ apps/sim/tools/context_dev/types.ts | 7 ++++++- apps/sim/tools/context_dev/utils.ts | 4 +++- 8 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/context_dev.mdx b/apps/docs/content/docs/en/integrations/context_dev.mdx index e2b5898af95..3cd1687a0fc 100644 --- a/apps/docs/content/docs/en/integrations/context_dev.mdx +++ b/apps/docs/content/docs/en/integrations/context_dev.mdx @@ -65,7 +65,7 @@ Scrape any URL and return the raw HTML content of the page. | --------- | ---- | ----------- | | `html` | string | Raw HTML content of the page | | `url` | string | The scraped URL | -| `type` | string | Detected content type \(html, xml, json, text, csv, markdown, svg, pdf\) | +| `type` | string | Detected content type \(html, xml, json, text, csv, markdown, svg, pdf, doc, docx\) | ### `context_dev_scrape_images` @@ -177,7 +177,7 @@ Build a sitemap of a domain and return every discovered page URL. | --------- | ---- | ----------- | | `domain` | string | The domain that was mapped | | `urls` | array | All page URLs discovered from the sitemap | -| `meta` | object | Sitemap discovery stats \(sitemapsDiscovered, sitemapsFetched, errors\) | +| `meta` | object | Sitemap discovery stats \(sitemapsDiscovered, sitemapsFetched, sitemapsSkipped, errors\) | ### `context_dev_search` @@ -191,6 +191,8 @@ Search the web with natural language and optionally scrape results to markdown. | `includeDomains` | array | No | Only return results from these domains | | `excludeDomains` | array | No | Exclude results from these domains | | `freshness` | string | No | Recency filter \(last_24_hours, last_week, last_month, last_year\) | +| `numResults` | number | No | Number of results to return \(10-100, default 10\) | +| `country` | string | No | Restrict results to a country \(ISO 3166-1 alpha-2 code, e.g. US\) | | `queryFanout` | boolean | No | Expand the query into parallel variants for broader coverage | | `markdownEnabled` | boolean | No | Scrape each result page to markdown \(default: false\) | | `timeoutMS` | number | No | Request timeout in milliseconds \(1000-300000\) | @@ -449,7 +451,7 @@ Retrieve brand data for a domain: logos, colors, backdrops, socials, address, an | ↳ `email` | string | Brand contact email | | ↳ `phone` | string | Brand contact phone | | ↳ `industries` | json | Industry taxonomy \(eic industry/subindustry pairs\) | -| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing\) | +| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing, contact\) | | ↳ `primary_language` | string | Primary language of the brand site | ### `context_dev_get_brand_by_name` @@ -488,7 +490,7 @@ Retrieve brand data by company name: logos, colors, socials, address, and indust | ↳ `email` | string | Brand contact email | | ↳ `phone` | string | Brand contact phone | | ↳ `industries` | json | Industry taxonomy \(eic industry/subindustry pairs\) | -| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing\) | +| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing, contact\) | | ↳ `primary_language` | string | Primary language of the brand site | ### `context_dev_get_brand_by_email` @@ -526,7 +528,7 @@ Retrieve brand data from a work email address. Free/disposable emails are reject | ↳ `email` | string | Brand contact email | | ↳ `phone` | string | Brand contact phone | | ↳ `industries` | json | Industry taxonomy \(eic industry/subindustry pairs\) | -| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing\) | +| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing, contact\) | | ↳ `primary_language` | string | Primary language of the brand site | ### `context_dev_get_brand_by_ticker` @@ -565,7 +567,7 @@ Retrieve brand data for a public company by its stock ticker symbol. | ↳ `email` | string | Brand contact email | | ↳ `phone` | string | Brand contact phone | | ↳ `industries` | json | Industry taxonomy \(eic industry/subindustry pairs\) | -| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing\) | +| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing, contact\) | | ↳ `primary_language` | string | Primary language of the brand site | ### `context_dev_get_brand_simplified` @@ -632,7 +634,7 @@ Identify the brand behind a raw bank/card transaction descriptor and return its | ↳ `email` | string | Brand contact email | | ↳ `phone` | string | Brand contact phone | | ↳ `industries` | json | Industry taxonomy \(eic industry/subindustry pairs\) | -| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing\) | +| ↳ `links` | json | Key brand links \(careers, privacy, terms, blog, pricing, contact\) | | ↳ `primary_language` | string | Primary language of the brand site | ### `context_dev_prefetch_domain` diff --git a/apps/docs/content/docs/en/integrations/slack.mdx b/apps/docs/content/docs/en/integrations/slack.mdx index a01cafe3b3b..851d2b92bdc 100644 --- a/apps/docs/content/docs/en/integrations/slack.mdx +++ b/apps/docs/content/docs/en/integrations/slack.mdx @@ -1734,6 +1734,9 @@ Trigger workflow from Slack events like mentions, messages, and reactions | ↳ `callback_id` | string | Callback ID of the shortcut or view. Present for shortcuts and modal submissions | | ↳ `api_app_id` | string | Slack app ID. Present for interactivity and slash commands | | ↳ `message_ts` | string | Timestamp of the message the interaction originated from. Present for block_actions | +| ↳ `view` | json | Full Slack view object for modal interactions: state.values \(submitted input values\), private_metadata, id, callback_id, and hash. Present for view_submission/view_closed; null otherwise | +| ↳ `message` | json | Full source message object the interaction came from, including its blocks and text. Present for block_actions on a message; null otherwise | +| ↳ `state` | json | Current values of all stateful elements in the surface \(state.values\) at the time of a block action — e.g. inputs read on a button click. Present for block_actions; null otherwise | | ↳ `hasFiles` | boolean | Whether the message has file attachments | | ↳ `files` | file[] | File attachments downloaded from the message \(if includeFiles is enabled and bot token is provided\) | diff --git a/apps/sim/blocks/blocks/context_dev.ts b/apps/sim/blocks/blocks/context_dev.ts index 26298c41ef8..c96a0690d42 100644 --- a/apps/sim/blocks/blocks/context_dev.ts +++ b/apps/sim/blocks/blocks/context_dev.ts @@ -317,6 +317,22 @@ Do not include any explanations, markdown formatting, or other text outside the mode: 'advanced', condition: { field: 'operation', value: 'search' }, }, + { + id: 'numResults', + title: 'Number of Results', + type: 'short-input', + placeholder: '10 to 100 (default 10)', + mode: 'advanced', + condition: { field: 'operation', value: 'search' }, + }, + { + id: 'country', + title: 'Country', + type: 'short-input', + placeholder: 'ISO 3166-1 alpha-2 code (e.g., US)', + mode: 'advanced', + condition: { field: 'operation', value: 'search' }, + }, { id: 'factCheck', title: 'Fact Check', @@ -634,6 +650,8 @@ Do not include any explanations, markdown formatting, or other text outside the const exclude = toStringArray(params.excludeDomains) if (exclude?.length) result.excludeDomains = exclude setString('freshness') + setNumber('numResults') + setString('country') setBool('queryFanout') setBool('markdownEnabled') setNumber('timeoutMS') diff --git a/apps/sim/tools/context_dev/map.ts b/apps/sim/tools/context_dev/map.ts index c8491b02d5e..823d2de501b 100644 --- a/apps/sim/tools/context_dev/map.ts +++ b/apps/sim/tools/context_dev/map.ts @@ -83,7 +83,8 @@ export const contextDevMapTool: ToolConfig Date: Tue, 30 Jun 2026 10:22:06 -0700 Subject: [PATCH 2/7] =?UTF-8?q?feat(integrations):=20wave-4=20tool-depth?= =?UTF-8?q?=20=E2=80=94=20Slack,=20Asana,=20Jira,=20Google=20Docs,=20Trell?= =?UTF-8?q?o,=20Monday?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deepen six existing blocks with 38 new tools, no new OAuth scopes (all under already-granted scopes), additive/backwards-compatible: - Slack (7): schedule/list/delete scheduled messages; archive/rename/set-topic/set-purpose conversation - Asana (8): create/get project, list workspaces, create subtask, delete task, add followers, create/list sections (via internal routes + contracts) - Jira (5): list/get project, get transitions, list issue types, get fields - Google Docs (6): delete content range, named ranges, paragraph bullets, update paragraph style (documents.batchUpdate) - Trello (7): create board/list, get board/card, add checklist/label/member - Monday (5): change column value, create board/column, get groups, duplicate item Route baseline 873->881 for the 8 new Asana internal routes. --- .../content/docs/en/integrations/asana.mdx | 183 ++++++++++ .../docs/en/integrations/google_docs.mdx | 142 ++++++++ .../content/docs/en/integrations/jira.mdx | 149 ++++++++ .../content/docs/en/integrations/monday.mdx | 140 ++++++++ .../content/docs/en/integrations/slack.mdx | 180 ++++++++++ .../content/docs/en/integrations/trello.mdx | 157 +++++++++ .../api/tools/asana/add-followers/route.ts | 103 ++++++ .../api/tools/asana/create-project/route.ts | 100 ++++++ .../api/tools/asana/create-section/route.ts | 92 +++++ .../api/tools/asana/create-subtask/route.ts | 106 ++++++ .../app/api/tools/asana/delete-task/route.ts | 81 +++++ .../app/api/tools/asana/get-project/route.ts | 92 +++++ .../api/tools/asana/list-sections/route.ts | 93 +++++ .../api/tools/asana/list-workspaces/route.ts | 87 +++++ apps/sim/blocks/blocks/asana.ts | 212 +++++++++++ apps/sim/blocks/blocks/google_docs.ts | 139 +++++++- apps/sim/blocks/blocks/jira.ts | 124 ++++++- apps/sim/blocks/blocks/monday.ts | 238 ++++++++++++- apps/sim/blocks/blocks/slack.ts | 243 ++++++++++++- apps/sim/blocks/blocks/trello.ts | 328 +++++++++++++++++- apps/sim/lib/api/contracts/tools/asana.ts | 188 ++++++++++ apps/sim/lib/integrations/integrations.json | 166 ++++++++- apps/sim/tools/asana/add_followers.ts | 87 +++++ apps/sim/tools/asana/create_project.ts | 91 +++++ apps/sim/tools/asana/create_section.ts | 76 ++++ apps/sim/tools/asana/create_subtask.ts | 109 ++++++ apps/sim/tools/asana/delete_task.ts | 68 ++++ apps/sim/tools/asana/get_project.ts | 74 ++++ apps/sim/tools/asana/index.ts | 16 + apps/sim/tools/asana/list_sections.ts | 79 +++++ apps/sim/tools/asana/list_workspaces.ts | 74 ++++ apps/sim/tools/asana/types.ts | 118 +++++++ .../tools/google_docs/create-named-range.ts | 123 +++++++ .../google_docs/create-paragraph-bullets.ts | 143 ++++++++ .../tools/google_docs/delete-content-range.ts | 108 ++++++ .../tools/google_docs/delete-named-range.ts | 110 ++++++ .../google_docs/delete-paragraph-bullets.ts | 109 ++++++ apps/sim/tools/google_docs/index.ts | 12 + apps/sim/tools/google_docs/types.ts | 54 +++ .../google_docs/update-paragraph-style.ts | 167 +++++++++ apps/sim/tools/google_docs/utils.ts | 21 ++ apps/sim/tools/jira/get_fields.ts | 153 ++++++++ apps/sim/tools/jira/get_project.ts | 173 +++++++++ apps/sim/tools/jira/get_transitions.ts | 166 +++++++++ apps/sim/tools/jira/index.ts | 10 + apps/sim/tools/jira/list_issue_types.ts | 150 ++++++++ apps/sim/tools/jira/list_projects.ts | 206 +++++++++++ apps/sim/tools/jira/types.ts | 134 +++++++ apps/sim/tools/monday/change_column_value.ts | 160 +++++++++ apps/sim/tools/monday/create_board.ts | 134 +++++++ apps/sim/tools/monday/create_column.ts | 159 +++++++++ apps/sim/tools/monday/duplicate_item.ts | 142 ++++++++ apps/sim/tools/monday/get_groups.ts | 94 +++++ apps/sim/tools/monday/index.ts | 5 + apps/sim/tools/monday/types.ts | 70 ++++ apps/sim/tools/monday/utils.ts | 18 + apps/sim/tools/registry.ts | 76 ++++ apps/sim/tools/slack/archive_conversation.ts | 101 ++++++ .../tools/slack/delete_scheduled_message.ts | 104 ++++++ apps/sim/tools/slack/index.ts | 14 + .../tools/slack/list_scheduled_messages.ts | 144 ++++++++ apps/sim/tools/slack/rename_conversation.ts | 133 +++++++ apps/sim/tools/slack/schedule_message.ts | 138 ++++++++ .../tools/slack/set_conversation_purpose.ts | 105 ++++++ .../sim/tools/slack/set_conversation_topic.ts | 119 +++++++ apps/sim/tools/slack/types.ts | 114 ++++++ apps/sim/tools/trello/add_checklist.ts | 136 ++++++++ apps/sim/tools/trello/add_label.ts | 99 ++++++ apps/sim/tools/trello/add_member.ts | 99 ++++++ apps/sim/tools/trello/create_board.ts | 143 ++++++++ apps/sim/tools/trello/create_list.ts | 130 +++++++ apps/sim/tools/trello/get_board.ts | 116 +++++++ apps/sim/tools/trello/get_card.ts | 148 ++++++++ apps/sim/tools/trello/index.ts | 14 + apps/sim/tools/trello/shared.ts | 33 +- apps/sim/tools/trello/types.ts | 111 +++++- scripts/check-api-validation-contracts.ts | 4 +- 77 files changed, 8802 insertions(+), 35 deletions(-) create mode 100644 apps/sim/app/api/tools/asana/add-followers/route.ts create mode 100644 apps/sim/app/api/tools/asana/create-project/route.ts create mode 100644 apps/sim/app/api/tools/asana/create-section/route.ts create mode 100644 apps/sim/app/api/tools/asana/create-subtask/route.ts create mode 100644 apps/sim/app/api/tools/asana/delete-task/route.ts create mode 100644 apps/sim/app/api/tools/asana/get-project/route.ts create mode 100644 apps/sim/app/api/tools/asana/list-sections/route.ts create mode 100644 apps/sim/app/api/tools/asana/list-workspaces/route.ts create mode 100644 apps/sim/tools/asana/add_followers.ts create mode 100644 apps/sim/tools/asana/create_project.ts create mode 100644 apps/sim/tools/asana/create_section.ts create mode 100644 apps/sim/tools/asana/create_subtask.ts create mode 100644 apps/sim/tools/asana/delete_task.ts create mode 100644 apps/sim/tools/asana/get_project.ts create mode 100644 apps/sim/tools/asana/list_sections.ts create mode 100644 apps/sim/tools/asana/list_workspaces.ts create mode 100644 apps/sim/tools/google_docs/create-named-range.ts create mode 100644 apps/sim/tools/google_docs/create-paragraph-bullets.ts create mode 100644 apps/sim/tools/google_docs/delete-content-range.ts create mode 100644 apps/sim/tools/google_docs/delete-named-range.ts create mode 100644 apps/sim/tools/google_docs/delete-paragraph-bullets.ts create mode 100644 apps/sim/tools/google_docs/update-paragraph-style.ts create mode 100644 apps/sim/tools/jira/get_fields.ts create mode 100644 apps/sim/tools/jira/get_project.ts create mode 100644 apps/sim/tools/jira/get_transitions.ts create mode 100644 apps/sim/tools/jira/list_issue_types.ts create mode 100644 apps/sim/tools/jira/list_projects.ts create mode 100644 apps/sim/tools/monday/change_column_value.ts create mode 100644 apps/sim/tools/monday/create_board.ts create mode 100644 apps/sim/tools/monday/create_column.ts create mode 100644 apps/sim/tools/monday/duplicate_item.ts create mode 100644 apps/sim/tools/monday/get_groups.ts create mode 100644 apps/sim/tools/slack/archive_conversation.ts create mode 100644 apps/sim/tools/slack/delete_scheduled_message.ts create mode 100644 apps/sim/tools/slack/list_scheduled_messages.ts create mode 100644 apps/sim/tools/slack/rename_conversation.ts create mode 100644 apps/sim/tools/slack/schedule_message.ts create mode 100644 apps/sim/tools/slack/set_conversation_purpose.ts create mode 100644 apps/sim/tools/slack/set_conversation_topic.ts create mode 100644 apps/sim/tools/trello/add_checklist.ts create mode 100644 apps/sim/tools/trello/add_label.ts create mode 100644 apps/sim/tools/trello/add_member.ts create mode 100644 apps/sim/tools/trello/create_board.ts create mode 100644 apps/sim/tools/trello/create_list.ts create mode 100644 apps/sim/tools/trello/get_board.ts create mode 100644 apps/sim/tools/trello/get_card.ts diff --git a/apps/docs/content/docs/en/integrations/asana.mdx b/apps/docs/content/docs/en/integrations/asana.mdx index bd9a332e6d0..ac8dd885ecc 100644 --- a/apps/docs/content/docs/en/integrations/asana.mdx +++ b/apps/docs/content/docs/en/integrations/asana.mdx @@ -209,4 +209,187 @@ Add a comment (story) to an Asana task | ↳ `gid` | string | Author GID | | ↳ `name` | string | Author name | +### `asana_create_subtask` + +Create a subtask under an existing Asana task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskGid` | string | Yes | GID of the parent Asana task \(numeric string\) | +| `name` | string | Yes | Name of the subtask | +| `notes` | string | No | Notes or description for the subtask | +| `assignee` | string | No | User GID to assign the subtask to | +| `due_on` | string | No | Due date in YYYY-MM-DD format | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | Subtask globally unique identifier | +| `name` | string | Subtask name | +| `notes` | string | Subtask notes or description | +| `completed` | boolean | Whether the subtask is completed | +| `created_at` | string | Subtask creation timestamp | +| `permalink_url` | string | URL to the subtask in Asana | + +### `asana_delete_task` + +Delete an Asana task by its GID (moves it to the trash) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskGid` | string | Yes | GID of the Asana task to delete \(numeric string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | GID of the deleted task | +| `deleted` | boolean | Whether the task was deleted | + +### `asana_add_followers` + +Add one or more followers to an Asana task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskGid` | string | Yes | GID of the Asana task \(numeric string\) | +| `followers` | array | Yes | Array of user GIDs to add as followers to the task | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | Task globally unique identifier | +| `name` | string | Task name | +| `followers` | array | Current followers on the task after the update | +| ↳ `gid` | string | Follower GID | +| ↳ `name` | string | Follower name | + +### `asana_create_project` + +Create a new project in an Asana workspace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspace` | string | Yes | Asana workspace GID \(numeric string\) where the project will be created | +| `name` | string | Yes | Name of the project | +| `notes` | string | No | Notes or description for the project | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | Project globally unique identifier | +| `name` | string | Project name | +| `notes` | string | Project notes or description | +| `archived` | boolean | Whether the project is archived | +| `color` | string | Project color | +| `created_at` | string | Project creation timestamp | +| `modified_at` | string | Project last modified timestamp | +| `permalink_url` | string | URL to the project in Asana | + +### `asana_get_project` + +Retrieve a single Asana project by its GID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectGid` | string | Yes | Asana project GID \(numeric string\) to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | Project globally unique identifier | +| `name` | string | Project name | +| `notes` | string | Project notes or description | +| `archived` | boolean | Whether the project is archived | +| `color` | string | Project color | +| `created_at` | string | Project creation timestamp | +| `modified_at` | string | Project last modified timestamp | +| `permalink_url` | string | URL to the project in Asana | + +### `asana_list_workspaces` + +List all Asana workspaces and organizations the authenticated user belongs to + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `workspaces` | array | Array of workspaces | +| ↳ `gid` | string | Workspace GID | +| ↳ `name` | string | Workspace name | +| ↳ `resource_type` | string | Resource type \(workspace\) | + +### `asana_create_section` + +Create a new section in an Asana project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectGid` | string | Yes | GID of the Asana project \(numeric string\) to add the section to | +| `name` | string | Yes | Name of the section | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `gid` | string | Section globally unique identifier | +| `name` | string | Section name | +| `created_at` | string | Section creation timestamp | + +### `asana_list_sections` + +List all sections in an Asana project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectGid` | string | Yes | GID of the Asana project \(numeric string\) to list sections from | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of the response | +| `sections` | array | Array of sections in the project | +| ↳ `gid` | string | Section GID | +| ↳ `name` | string | Section name | +| ↳ `resource_type` | string | Resource type \(section\) | + diff --git a/apps/docs/content/docs/en/integrations/google_docs.mdx b/apps/docs/content/docs/en/integrations/google_docs.mdx index 9dc591ccb0a..cc0963d0155 100644 --- a/apps/docs/content/docs/en/integrations/google_docs.mdx +++ b/apps/docs/content/docs/en/integrations/google_docs.mdx @@ -245,4 +245,146 @@ Apply bold, italic, underline, and/or font size to a range of text in a Google D | ↳ `mimeType` | string | Document MIME type | | ↳ `url` | string | Document URL | +### `google_docs_update_paragraph_style` + +Apply a named paragraph style (such as a heading or title) and/or alignment to the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `startIndex` | number | Yes | The 1-based start character index of the range to style \(inclusive\) | +| `endIndex` | number | Yes | The end character index of the range to style \(exclusive\) | +| `namedStyleType` | string | No | The named paragraph style to apply. One of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6. | +| `alignment` | string | No | The paragraph alignment to apply. One of: LEFT, CENTER, RIGHT, JUSTIFY. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the paragraph style was applied successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + +### `google_docs_create_paragraph_bullets` + +Add bulleted or numbered list formatting to the paragraphs overlapping a range of text in a Google Docs document, using a chosen bullet glyph preset. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `startIndex` | number | Yes | The 1-based start character index of the range to bullet \(inclusive\) | +| `endIndex` | number | Yes | The end character index of the range to bullet \(exclusive\) | +| `bulletPreset` | string | No | The bullet glyph preset to apply. Defaults to BULLET_DISC_CIRCLE_SQUARE. Examples: BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the bullets were applied successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + +### `google_docs_delete_paragraph_bullets` + +Remove bullet or numbered list formatting from the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `startIndex` | number | Yes | The 1-based start character index of the range to clear bullets from \(inclusive\) | +| `endIndex` | number | Yes | The end character index of the range to clear bullets from \(exclusive\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the bullets were removed successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + +### `google_docs_delete_content_range` + +Delete all content between a start and end character index in a Google Docs document. The endIndex is exclusive and must be greater than the startIndex. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to delete content from | +| `startIndex` | number | Yes | The 1-based start character index of the range to delete \(inclusive\) | +| `endIndex` | number | Yes | The end character index of the range to delete \(exclusive\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the content range was deleted successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + +### `google_docs_create_named_range` + +Create a named range over a span of content in a Google Docs document so it can be referenced or deleted later. The name may be 1-256 characters and need not be unique. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `name` | string | Yes | The name of the range to create \(1-256 characters\) | +| `startIndex` | number | Yes | The 1-based start character index of the range \(inclusive\) | +| `endIndex` | number | Yes | The end character index of the range \(exclusive\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `namedRangeId` | string | The ID of the created named range | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + +### `google_docs_delete_named_range` + +Delete one or more named ranges from a Google Docs document by their ID or by name. Provide exactly one of namedRangeId or name; deleting by name removes all ranges sharing that name. The content itself is not removed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `namedRangeId` | string | No | The ID of the named range to delete. Provide exactly one of namedRangeId or namedRangeName. | +| `namedRangeName` | string | No | The name of the named range\(s\) to delete. All ranges sharing this name are removed. Provide exactly one of namedRangeId or namedRangeName. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the named range\(s\) were deleted successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | +| ↳ `documentId` | string | Google Docs document ID | +| ↳ `title` | string | Document title | +| ↳ `mimeType` | string | Document MIME type | +| ↳ `url` | string | Document URL | + diff --git a/apps/docs/content/docs/en/integrations/jira.mdx b/apps/docs/content/docs/en/integrations/jira.mdx index 86ba0172594..993f9f1d5e3 100644 --- a/apps/docs/content/docs/en/integrations/jira.mdx +++ b/apps/docs/content/docs/en/integrations/jira.mdx @@ -1046,6 +1046,155 @@ Search for Jira users by email address or display name. Returns matching users w | `startAt` | number | Pagination start index | | `maxResults` | number | Maximum results per page | +### `jira_list_projects` + +List Jira projects visible to the user, with optional name/key filtering and pagination. Returns each project with id, key, name, and type. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `query` | string | No | Filter projects by partial name or key match | +| `startAt` | number | No | The index of the first project to return \(for pagination, default: 0\) | +| `maxResults` | number | No | Maximum number of projects to return \(default: 50, max: 100\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `projects` | array | Array of Jira projects | +| ↳ `id` | string | Project ID | +| ↳ `key` | string | Project key \(e.g., PROJ\) | +| ↳ `name` | string | Project name | +| ↳ `projectTypeKey` | string | Project type key \(e.g., software, service_desk, business\) | +| ↳ `simplified` | boolean | Whether the project is a simplified \(team-managed\) project | +| ↳ `style` | string | Project style \(e.g., classic, next-gen\) | +| ↳ `isPrivate` | boolean | Whether the project is private | +| ↳ `url` | string | REST API URL for this project | +| ↳ `leadDisplayName` | string | Display name of the project lead | +| ↳ `leadAccountId` | string | Account ID of the project lead | +| `total` | number | Total number of matching projects | +| `startAt` | number | Pagination start index | +| `maxResults` | number | Maximum results per page | +| `isLast` | boolean | Whether this is the last page of results | + +### `jira_get_project` + +Get the details of a single Jira project by its ID or key, including its type, lead, components, issue types, and versions. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `projectId` | string | Yes | The project ID or key \(e.g., "PROJ" or "10000"\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `id` | string | Project ID | +| `key` | string | Project key \(e.g., PROJ\) | +| `name` | string | Project name | +| `description` | string | Project description | +| `projectTypeKey` | string | Project type key \(e.g., software, service_desk, business\) | +| `simplified` | boolean | Whether the project is a simplified \(team-managed\) project | +| `style` | string | Project style \(e.g., classic, next-gen\) | +| `isPrivate` | boolean | Whether the project is private | +| `url` | string | REST API URL for this project | +| `leadDisplayName` | string | Display name of the project lead | +| `leadAccountId` | string | Account ID of the project lead | +| `issueTypes` | array | Issue types available in this project | +| ↳ `id` | string | Issue type ID | +| ↳ `name` | string | Issue type name \(e.g., Task, Bug, Story\) | +| ↳ `subtask` | boolean | Whether this issue type is a subtask | + +### `jira_get_transitions` + +Get the workflow transitions available for an issue in its current status. Use the returned transition IDs with the Transition Issue operation. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | The issue key or ID \(e.g., PROJ-123\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `issueKey` | string | Issue key the transitions belong to | +| `transitions` | array | Available workflow transitions for the issue | +| ↳ `id` | string | Transition ID \(use with Transition Issue\) | +| ↳ `name` | string | Transition name \(e.g., "Start Progress"\) | +| ↳ `toStatusId` | string | ID of the status the issue moves to | +| ↳ `toStatusName` | string | Name of the status the issue moves to | +| ↳ `toStatusCategory` | string | Status category key of the target status \(new, indeterminate, done\) | +| ↳ `isAvailable` | boolean | Whether the transition can currently be performed | +| ↳ `hasScreen` | boolean | Whether the transition requires a screen with fields | +| `total` | number | Number of available transitions | + +### `jira_list_issue_types` + +List all issue types visible to the user across projects (e.g., Task, Bug, Story, Epic, Subtask). Useful for discovering valid issue types before creating an issue. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `issueTypes` | array | Array of issue types | +| ↳ `id` | string | Issue type ID | +| ↳ `name` | string | Issue type name \(e.g., Task, Bug, Story\) | +| ↳ `description` | string | Issue type description | +| ↳ `subtask` | boolean | Whether this issue type is a subtask | +| ↳ `hierarchyLevel` | number | Hierarchy level \(0 = standard, 1 = epic, -1 = subtask\) | +| ↳ `iconUrl` | string | URL of the issue type icon | +| ↳ `scope` | string | Project ID if this issue type is scoped to a team-managed project | +| `total` | number | Number of issue types returned | + +### `jira_get_fields` + +Get all system and custom fields defined in the Jira instance. Useful for discovering custom field IDs (e.g., customfield_10001) to use when writing or updating issues. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `fields` | array | Array of Jira fields \(system and custom\) | +| ↳ `id` | string | Field ID \(e.g., summary, customfield_10001\) | +| ↳ `key` | string | Field key | +| ↳ `name` | string | Human-readable field name | +| ↳ `custom` | boolean | Whether this is a custom field | +| ↳ `navigable` | boolean | Whether the field is navigable in issue views | +| ↳ `searchable` | boolean | Whether the field can be used in JQL searches | +| ↳ `schemaType` | string | Field value type \(e.g., string, number, array, user\) | +| ↳ `customType` | string | Custom field type identifier \(only for custom fields\) | +| `total` | number | Number of fields returned | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/monday.mdx b/apps/docs/content/docs/en/integrations/monday.mdx index d60977dd329..35a2e25eced 100644 --- a/apps/docs/content/docs/en/integrations/monday.mdx +++ b/apps/docs/content/docs/en/integrations/monday.mdx @@ -242,6 +242,72 @@ Update column values of an item on a Monday.com board | ↳ `updatedAt` | string | Last updated timestamp | | ↳ `url` | string | Item URL | +### `monday_change_column_value` + +Update a single column's value on a Monday.com item + +#### 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 | +| `columnId` | string | Yes | The ID of the column to update \(e.g., "status", "date4"\) | +| `value` | string | Yes | The new column value as a JSON string \(e.g., \{"label":"Done"\} for a status column\) | +| `createLabelsIfMissing` | boolean | No | Create status/dropdown labels that do not yet exist on the column | + +#### 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_duplicate_item` + +Duplicate an existing 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 duplicate | +| `withUpdates` | boolean | No | Whether to also duplicate the item updates | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The duplicated 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 @@ -384,6 +450,80 @@ Create a new group on a Monday.com board | ↳ `deleted` | boolean | Whether deleted | | ↳ `position` | string | Group position | +### `monday_get_groups` + +Get the groups on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to retrieve groups from | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `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 | +| `count` | number | Number of returned groups | + +### `monday_create_board` + +Create a new board in Monday.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardName` | string | Yes | The name of the new board | +| `boardKind` | string | Yes | The board kind: public, private, or share | +| `description` | string | No | The board description | +| `workspaceId` | string | No | The ID of the workspace to create the board in | +| `folderId` | string | No | The ID of the folder to create the board in | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `board` | json | The created board | +| ↳ `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 | + +### `monday_create_column` + +Create a new column on a Monday.com board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | The ID of the board to create the column on | +| `columnTitle` | string | Yes | The title of the new column | +| `columnType` | string | Yes | The column type \(e.g., status, text, numbers, date, people, dropdown\) | +| `columnDescription` | string | No | The column description | +| `columnDefaults` | string | No | JSON string of default settings for the column \(e.g., status labels\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `column` | json | The created column | +| ↳ `id` | string | Column ID | +| ↳ `title` | string | Column title | +| ↳ `type` | string | Column type | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/slack.mdx b/apps/docs/content/docs/en/integrations/slack.mdx index 851d2b92bdc..c90a179c1b9 100644 --- a/apps/docs/content/docs/en/integrations/slack.mdx +++ b/apps/docs/content/docs/en/integrations/slack.mdx @@ -1686,6 +1686,186 @@ Publish a static view to a user's Home tab in Slack. Used to create or update th | ↳ `app_id` | string | Application identifier | | ↳ `bot_id` | string | Bot identifier | +### `slack_schedule_message` + +Schedule a message to be sent to a Slack channel or DM at a future time. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Channel, private group, or DM to receive the message \(e.g., C1234567890\) | +| `postAt` | number | Yes | Unix timestamp \(seconds\) representing the future time the message should post | +| `text` | string | No | Message text to send \(supports Slack mrkdwn formatting\) | +| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. | +| `threadTs` | string | No | Thread timestamp to reply to \(creates a scheduled thread reply\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduledMessageId` | string | Identifier of the scheduled message \(used to delete it before it posts\) | +| `postAt` | number | Unix timestamp when the message will post | +| `channel` | string | Channel ID where the message is scheduled | +| `message` | object | The scheduled message object returned by Slack | + +### `slack_list_scheduled_messages` + +List pending scheduled messages in a Slack workspace, optionally filtered by channel. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | No | Optional channel ID to filter scheduled messages \(e.g., C1234567890\) | +| `limit` | number | No | Maximum number of scheduled messages to return | +| `cursor` | string | No | Pagination cursor \(next_cursor\) from a previous response | +| `oldest` | string | No | Unix timestamp of the oldest scheduled message to include | +| `latest` | string | No | Unix timestamp of the latest scheduled message to include | +| `teamId` | string | No | Encoded team ID \(required only with org-level tokens\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `scheduledMessages` | array | Array of pending scheduled message objects | +| ↳ `id` | string | Scheduled message ID | +| ↳ `channel_id` | string | Channel the message is scheduled for | +| ↳ `post_at` | number | Unix timestamp when the message will post | +| ↳ `date_created` | number | Unix timestamp when the schedule was created | +| ↳ `text` | string | Scheduled message text | +| `nextCursor` | string | Cursor for the next page \(null when there are no more pages\) | + +### `slack_delete_scheduled_message` + +Delete a pending scheduled message before it posts to Slack. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | Channel ID where the scheduled message is queued \(e.g., C1234567890\) | +| `scheduledMessageId` | string | Yes | Scheduled message ID from chat.scheduleMessage \(e.g., Q1234ABCD\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ok` | boolean | Whether the scheduled message was deleted successfully | + +### `slack_archive_conversation` + +Archive a Slack channel so it is closed to new activity. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | ID of the channel to archive \(e.g., C1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ok` | boolean | Whether the conversation was archived successfully | + +### `slack_rename_conversation` + +Rename an existing Slack channel. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | ID of the channel to rename \(e.g., C1234567890\) | +| `name` | string | Yes | New channel name \(lowercase letters, numbers, hyphens, underscores only; max 80 characters\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `channelInfo` | object | The channel object after renaming | +| ↳ `id` | string | Channel ID \(e.g., C1234567890\) | +| ↳ `name` | string | Channel name without # prefix | +| ↳ `is_channel` | boolean | Whether this is a channel | +| ↳ `is_private` | boolean | Whether channel is private | +| ↳ `is_archived` | boolean | Whether channel is archived | +| ↳ `is_general` | boolean | Whether this is the general channel | +| ↳ `is_member` | boolean | Whether the bot/user is a member | +| ↳ `is_shared` | boolean | Whether channel is shared across workspaces | +| ↳ `is_ext_shared` | boolean | Whether channel is externally shared | +| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared | +| ↳ `num_members` | number | Number of members in the channel | +| ↳ `topic` | string | Channel topic | +| ↳ `purpose` | string | Channel purpose/description | +| ↳ `created` | number | Unix timestamp when channel was created | +| ↳ `creator` | string | User ID of channel creator | +| ↳ `updated` | number | Unix timestamp of last update | + +### `slack_set_conversation_topic` + +Set the topic for a Slack channel (max 250 characters). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | ID of the channel to update \(e.g., C1234567890\) | +| `topic` | string | Yes | New topic text \(max 250 characters; no formatting or linkification\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `channelInfo` | object | The channel object after updating the topic | +| ↳ `id` | string | Channel ID \(e.g., C1234567890\) | +| ↳ `name` | string | Channel name without # prefix | +| ↳ `is_channel` | boolean | Whether this is a channel | +| ↳ `is_private` | boolean | Whether channel is private | +| ↳ `is_archived` | boolean | Whether channel is archived | +| ↳ `is_general` | boolean | Whether this is the general channel | +| ↳ `is_member` | boolean | Whether the bot/user is a member | +| ↳ `is_shared` | boolean | Whether channel is shared across workspaces | +| ↳ `is_ext_shared` | boolean | Whether channel is externally shared | +| ↳ `is_org_shared` | boolean | Whether channel is org-wide shared | +| ↳ `num_members` | number | Number of members in the channel | +| ↳ `topic` | string | Channel topic | +| ↳ `purpose` | string | Channel purpose/description | +| ↳ `created` | number | Unix timestamp when channel was created | +| ↳ `creator` | string | User ID of channel creator | +| ↳ `updated` | number | Unix timestamp of last update | + +### `slack_set_conversation_purpose` + +Set the purpose (description) for a Slack channel (max 250 characters). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `channel` | string | Yes | ID of the channel to update \(e.g., C1234567890\) | +| `purpose` | string | Yes | New purpose/description text \(max 250 characters\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `purpose` | string | The purpose/description that was set on the channel | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/trello.mdx b/apps/docs/content/docs/en/integrations/trello.mdx index 6354aa115ad..86cdef85b99 100644 --- a/apps/docs/content/docs/en/integrations/trello.mdx +++ b/apps/docs/content/docs/en/integrations/trello.mdx @@ -251,4 +251,161 @@ Add a comment to a Trello card | ↳ `id` | string | List ID | | ↳ `name` | string | List name | +### `trello_create_board` + +Create a new Trello board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Name of the board | +| `desc` | string | No | Description of the board | +| `idOrganization` | string | No | ID or name of the workspace/organization the board belongs to | +| `defaultLists` | boolean | No | Whether to create the default lists \(To Do, Doing, Done\) on the new board | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `board` | json | Created board \(id, name, desc, url, closed, idOrganization\) | +| ↳ `id` | string | Board ID | +| ↳ `name` | string | Board name | +| ↳ `desc` | string | Board description | +| ↳ `url` | string | Full board URL | +| ↳ `closed` | boolean | Whether the board is closed | +| ↳ `idOrganization` | string | ID of the workspace/organization the board belongs to | + +### `trello_get_board` + +Retrieve a single Trello board by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | Trello board ID \(24-character hex string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `board` | json | Board \(id, name, desc, url, closed, idOrganization\) | +| ↳ `id` | string | Board ID | +| ↳ `name` | string | Board name | +| ↳ `desc` | string | Board description | +| ↳ `url` | string | Full board URL | +| ↳ `closed` | boolean | Whether the board is closed | +| ↳ `idOrganization` | string | ID of the workspace/organization the board belongs to | + +### `trello_create_list` + +Create a new list on a Trello board + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `boardId` | string | Yes | Trello board ID the list belongs to \(24-character hex string\) | +| `name` | string | Yes | Name of the list | +| `pos` | string | No | Position of the list \(top, bottom, or positive float\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `list` | json | Created list \(id, name, closed, pos, idBoard\) | +| ↳ `id` | string | List ID | +| ↳ `name` | string | List name | +| ↳ `closed` | boolean | Whether the list is archived | +| ↳ `pos` | number | List position on the board | +| ↳ `idBoard` | string | Board ID containing the list | + +### `trello_get_card` + +Retrieve a single Trello card by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cardId` | string | Yes | Trello card ID \(24-character hex string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `card` | json | Card \(id, name, desc, url, idBoard, idList, closed, labelIds, labels, due, dueComplete\) | +| ↳ `id` | string | Card ID | +| ↳ `name` | string | Card name | +| ↳ `desc` | string | Card description | +| ↳ `url` | string | Full card URL | +| ↳ `idBoard` | string | Board ID containing the card | +| ↳ `idList` | string | List ID containing the card | +| ↳ `closed` | boolean | Whether the card is archived | +| ↳ `labelIds` | array | Label IDs applied to the card | +| ↳ `labels` | array | Labels applied to the card | +| ↳ `id` | string | Label ID | +| ↳ `name` | string | Label name | +| ↳ `color` | string | Label color | +| ↳ `due` | string | Card due date in ISO 8601 format | +| ↳ `dueComplete` | boolean | Whether the due date is complete | + +### `trello_add_checklist` + +Add a checklist to a Trello card + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cardId` | string | Yes | Trello card ID to add the checklist to \(24-character hex string\) | +| `name` | string | Yes | Name of the checklist | +| `pos` | string | No | Position of the checklist \(top, bottom, or positive float\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `checklist` | json | Created checklist \(id, name, idCard, idBoard, pos\) | +| ↳ `id` | string | Checklist ID | +| ↳ `name` | string | Checklist name | +| ↳ `idCard` | string | Card ID containing the checklist | +| ↳ `idBoard` | string | Board ID containing the checklist | +| ↳ `pos` | number | Checklist position on the card | + +### `trello_add_label` + +Attach an existing label to a Trello card + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cardId` | string | Yes | Trello card ID to attach the label to \(24-character hex string\) | +| `labelId` | string | Yes | ID of the label to attach \(24-character hex string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `labelIds` | array | Label IDs now applied to the card | + +### `trello_add_member` + +Assign a member to a Trello card + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cardId` | string | Yes | Trello card ID to assign the member to \(24-character hex string\) | +| `memberId` | string | Yes | ID of the member to assign \(24-character hex string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `memberIds` | array | Member IDs now assigned to the card | + diff --git a/apps/sim/app/api/tools/asana/add-followers/route.ts b/apps/sim/app/api/tools/asana/add-followers/route.ts new file mode 100644 index 00000000000..d9ada412efd --- /dev/null +++ b/apps/sim/app/api/tools/asana/add-followers/route.ts @@ -0,0 +1,103 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaAddFollowersContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaAddFollowersAPI') + +interface AsanaFollower { + gid: string + name: string +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaAddFollowersContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, taskGid, followers } = parsed.data.body + + const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100) + if (!taskGidValidation.isValid) { + return NextResponse.json({ error: taskGidValidation.error }, { status: 400 }) + } + + for (const follower of followers) { + const followerValidation = validateAlphanumericId(follower, 'follower', 100) + if (!followerValidation.isValid) { + return NextResponse.json({ error: followerValidation.error }, { status: 400 }) + } + } + + const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/addFollowers?opt_fields=name,followers.name` + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: { followers } }), + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const task = result.data + const taskFollowers: AsanaFollower[] = Array.isArray(task.followers) ? task.followers : [] + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: task.gid, + name: task.name || '', + followers: taskFollowers.map((follower) => ({ + gid: follower.gid, + name: follower.name, + })), + }) + } catch (error) { + logger.error('Error adding followers to Asana task:', error) + return NextResponse.json( + { error: 'Failed to add followers to Asana task', details: (error as Error).message }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/create-project/route.ts b/apps/sim/app/api/tools/asana/create-project/route.ts new file mode 100644 index 00000000000..c0632307d2d --- /dev/null +++ b/apps/sim/app/api/tools/asana/create-project/route.ts @@ -0,0 +1,100 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaCreateProjectContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaCreateProjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaCreateProjectContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, workspace, name, notes } = parsed.data.body + + const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100) + if (!workspaceValidation.isValid) { + return NextResponse.json({ error: workspaceValidation.error }, { status: 400 }) + } + + const projectData: Record = { name, workspace } + if (notes) { + projectData.notes = notes + } + + const response = await fetch('https://app.asana.com/api/1.0/projects', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: projectData }), + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const project = result.data + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: project.gid, + name: project.name, + notes: project.notes || '', + archived: project.archived ?? false, + color: project.color ?? null, + created_at: project.created_at, + modified_at: project.modified_at, + permalink_url: project.permalink_url, + }) + } catch (error) { + logger.error('Error creating Asana project:', { + error: toError(error).message, + stack: error instanceof Error ? error.stack : undefined, + }) + + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/create-section/route.ts b/apps/sim/app/api/tools/asana/create-section/route.ts new file mode 100644 index 00000000000..0d351d6da67 --- /dev/null +++ b/apps/sim/app/api/tools/asana/create-section/route.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaCreateSectionContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaCreateSectionAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaCreateSectionContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, projectGid, name } = parsed.data.body + + const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100) + if (!projectGidValidation.isValid) { + return NextResponse.json({ error: projectGidValidation.error }, { status: 400 }) + } + + const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections` + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: { name } }), + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const section = result.data + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: section.gid, + name: section.name, + created_at: section.created_at, + }) + } catch (error) { + logger.error('Error creating Asana section:', { + error: toError(error).message, + stack: error instanceof Error ? error.stack : undefined, + }) + + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/create-subtask/route.ts b/apps/sim/app/api/tools/asana/create-subtask/route.ts new file mode 100644 index 00000000000..6f5df17ed97 --- /dev/null +++ b/apps/sim/app/api/tools/asana/create-subtask/route.ts @@ -0,0 +1,106 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaCreateSubtaskContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaCreateSubtaskAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaCreateSubtaskContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, taskGid, name, notes, assignee, due_on } = parsed.data.body + + const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100) + if (!taskGidValidation.isValid) { + return NextResponse.json({ error: taskGidValidation.error }, { status: 400 }) + } + + const subtaskData: Record = { name } + if (notes) { + subtaskData.notes = notes + } + if (assignee) { + subtaskData.assignee = assignee + } + if (due_on) { + subtaskData.due_on = due_on + } + + const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/subtasks` + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: subtaskData }), + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const task = result.data + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: task.gid, + name: task.name, + notes: task.notes || '', + completed: task.completed || false, + created_at: task.created_at, + permalink_url: task.permalink_url, + }) + } catch (error) { + logger.error('Error creating Asana subtask:', { + error: toError(error).message, + stack: error instanceof Error ? error.stack : undefined, + }) + + return NextResponse.json( + { error: getErrorMessage(error, 'Internal server error'), success: false }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/delete-task/route.ts b/apps/sim/app/api/tools/asana/delete-task/route.ts new file mode 100644 index 00000000000..6ca717e4f62 --- /dev/null +++ b/apps/sim/app/api/tools/asana/delete-task/route.ts @@ -0,0 +1,81 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaDeleteTaskContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaDeleteTaskAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaDeleteTaskContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, taskGid } = parsed.data.body + + const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100) + if (!taskGidValidation.isValid) { + return NextResponse.json({ error: taskGidValidation.error }, { status: 400 }) + } + + const url = `https://app.asana.com/api/1.0/tasks/${taskGid}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: taskGid, + deleted: true, + }) + } catch (error) { + logger.error('Error deleting Asana task:', error) + return NextResponse.json( + { error: 'Failed to delete Asana task', details: (error as Error).message }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/get-project/route.ts b/apps/sim/app/api/tools/asana/get-project/route.ts new file mode 100644 index 00000000000..3e7022a5a9b --- /dev/null +++ b/apps/sim/app/api/tools/asana/get-project/route.ts @@ -0,0 +1,92 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaGetProjectContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaGetProjectAPI') + +const PROJECT_OPT_FIELDS = 'name,notes,archived,color,created_at,modified_at,permalink_url' + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaGetProjectContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, projectGid } = parsed.data.body + + const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100) + if (!projectGidValidation.isValid) { + return NextResponse.json({ error: projectGidValidation.error }, { status: 400 }) + } + + const url = `https://app.asana.com/api/1.0/projects/${projectGid}?opt_fields=${PROJECT_OPT_FIELDS}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const project = result.data + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + gid: project.gid, + name: project.name, + notes: project.notes || '', + archived: project.archived ?? false, + color: project.color ?? null, + created_at: project.created_at, + modified_at: project.modified_at, + permalink_url: project.permalink_url, + }) + } catch (error) { + logger.error('Error processing request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Asana project', details: (error as Error).message }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/list-sections/route.ts b/apps/sim/app/api/tools/asana/list-sections/route.ts new file mode 100644 index 00000000000..05524251068 --- /dev/null +++ b/apps/sim/app/api/tools/asana/list-sections/route.ts @@ -0,0 +1,93 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaListSectionsContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaListSectionsAPI') + +interface AsanaSection { + gid: string + name: string + resource_type?: string +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaListSectionsContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, projectGid } = parsed.data.body + + const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100) + if (!projectGidValidation.isValid) { + return NextResponse.json({ error: projectGidValidation.error }, { status: 400 }) + } + + const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections` + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const sections: AsanaSection[] = Array.isArray(result.data) ? result.data : [] + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + sections: sections.map((section) => ({ + gid: section.gid, + name: section.name, + resource_type: section.resource_type, + })), + }) + } catch (error) { + logger.error('Error processing request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Asana sections', details: (error as Error).message }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/asana/list-workspaces/route.ts b/apps/sim/app/api/tools/asana/list-workspaces/route.ts new file mode 100644 index 00000000000..1c550e2c4d7 --- /dev/null +++ b/apps/sim/app/api/tools/asana/list-workspaces/route.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { asanaListWorkspacesContract } from '@/lib/api/contracts/tools/asana' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AsanaListWorkspacesAPI') + +interface AsanaWorkspace { + gid: string + name: string + resource_type?: string +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(asanaListWorkspacesContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken } = parsed.data.body + + const url = 'https://app.asana.com/api/1.0/workspaces?limit=100' + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Asana API error: ${response.status} ${response.statusText}` + + try { + const errorData = JSON.parse(errorText) + const asanaError = errorData.errors?.[0] + if (asanaError) { + errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})` + } + logger.error('Asana API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + } catch (_e) { + logger.error('Asana API error (unparsed):', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + } + + return NextResponse.json( + { success: false, error: errorMessage, details: errorText }, + { status: response.status } + ) + } + + const result = await response.json() + const workspaces: AsanaWorkspace[] = Array.isArray(result.data) ? result.data : [] + + return NextResponse.json({ + success: true, + ts: new Date().toISOString(), + workspaces: workspaces.map((workspace) => ({ + gid: workspace.gid, + name: workspace.name, + resource_type: workspace.resource_type, + })), + }) + } catch (error) { + logger.error('Error processing request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Asana workspaces', details: (error as Error).message }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 1695a702041..bd2ff64c1a8 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -27,6 +27,14 @@ export const AsanaBlock: BlockConfig = { { label: 'Get Projects', id: 'get_projects' }, { label: 'Search Tasks', id: 'search_tasks' }, { label: 'Add Comment', id: 'add_comment' }, + { label: 'Create Subtask', id: 'create_subtask' }, + { label: 'Delete Task', id: 'delete_task' }, + { label: 'Add Followers', id: 'add_followers' }, + { label: 'Create Project', id: 'create_project' }, + { label: 'Get Project', id: 'get_project' }, + { label: 'List Workspaces', id: 'list_workspaces' }, + { label: 'Create Section', id: 'create_section' }, + { label: 'List Sections', id: 'list_sections' }, ], value: () => 'get_task', }, @@ -234,6 +242,121 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n value: ['add_comment'], }, }, + { + id: 'createProjectWorkspaceSelector', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'createProject_workspace', + serviceId: 'asana', + selectorKey: 'asana.workspaces', + placeholder: 'Select Asana workspace', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['create_project'], + }, + required: true, + }, + { + id: 'createProject_workspace', + title: 'Workspace GID', + type: 'short-input', + canonicalParamId: 'createProject_workspace', + placeholder: 'Enter Asana workspace GID', + dependsOn: ['credential'], + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_project'], + }, + required: true, + }, + { + id: 'projectGid', + title: 'Project GID', + type: 'short-input', + required: true, + placeholder: 'Enter Asana project GID', + condition: { + field: 'operation', + value: ['get_project', 'create_section', 'list_sections'], + }, + }, + { + id: 'subtaskParentGid', + title: 'Parent Task GID', + type: 'short-input', + required: true, + placeholder: 'Enter parent task GID', + condition: { + field: 'operation', + value: ['create_subtask'], + }, + }, + { + id: 'taskGid', + title: 'Task GID', + type: 'short-input', + required: true, + placeholder: 'Enter Asana task GID', + condition: { + field: 'operation', + value: ['delete_task', 'add_followers'], + }, + }, + { + id: 'name', + title: 'Name', + type: 'short-input', + required: true, + placeholder: 'Enter a name', + condition: { + field: 'operation', + value: ['create_subtask', 'create_project', 'create_section'], + }, + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + placeholder: 'Enter notes or description', + condition: { + field: 'operation', + value: ['create_subtask', 'create_project'], + }, + }, + { + id: 'assignee', + title: 'Assignee GID', + type: 'short-input', + placeholder: 'Enter assignee user GID', + condition: { + field: 'operation', + value: ['create_subtask'], + }, + }, + { + id: 'due_on', + title: 'Due Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['create_subtask'], + }, + }, + { + id: 'followers', + title: 'Followers', + type: 'short-input', + required: true, + placeholder: 'Comma-separated user GIDs (e.g. 12345, 67890)', + condition: { + field: 'operation', + value: ['add_followers'], + }, + }, ], tools: { access: [ @@ -243,6 +366,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n 'asana_get_projects', 'asana_search_tasks', 'asana_add_comment', + 'asana_create_subtask', + 'asana_delete_task', + 'asana_add_followers', + 'asana_create_project', + 'asana_get_project', + 'asana_list_workspaces', + 'asana_create_section', + 'asana_list_sections', ], config: { tool: (params) => { @@ -259,6 +390,22 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return 'asana_search_tasks' case 'add_comment': return 'asana_add_comment' + case 'create_subtask': + return 'asana_create_subtask' + case 'delete_task': + return 'asana_delete_task' + case 'add_followers': + return 'asana_add_followers' + case 'create_project': + return 'asana_create_project' + case 'get_project': + return 'asana_get_project' + case 'list_workspaces': + return 'asana_list_workspaces' + case 'create_section': + return 'asana_create_section' + case 'list_sections': + return 'asana_list_sections' default: return 'asana_get_task' } @@ -325,6 +472,58 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n taskGid: params.taskGid, text: params.commentText, } + case 'create_subtask': + return { + ...baseParams, + taskGid: params.subtaskParentGid, + name: params.name, + notes: params.notes, + assignee: params.assignee, + due_on: params.due_on, + } + case 'delete_task': + return { + ...baseParams, + taskGid: params.taskGid, + } + case 'add_followers': + return { + ...baseParams, + taskGid: params.taskGid, + followers: params.followers + ? params.followers + .split(',') + .map((f: string) => f.trim()) + .filter((f: string) => f.length > 0) + : [], + } + case 'create_project': + return { + ...baseParams, + workspace: params.createProject_workspace, + name: params.name, + notes: params.notes, + } + case 'get_project': + return { + ...baseParams, + projectGid: params.projectGid, + } + case 'list_workspaces': + return { + ...baseParams, + } + case 'create_section': + return { + ...baseParams, + projectGid: params.projectGid, + name: params.name, + } + case 'list_sections': + return { + ...baseParams, + projectGid: params.projectGid, + } default: return baseParams } @@ -347,6 +546,13 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n completed: { type: 'array', description: 'Completion status' }, searchText: { type: 'string', description: 'Search text' }, commentText: { type: 'string', description: 'Comment text' }, + createProject_workspace: { + type: 'string', + description: 'Workspace GID for creating a project', + }, + projectGid: { type: 'string', description: 'Project GID' }, + subtaskParentGid: { type: 'string', description: 'Parent task GID for creating a subtask' }, + followers: { type: 'string', description: 'Comma-separated user GIDs to add as followers' }, }, outputs: { success: { type: 'boolean', description: 'Operation success status' }, @@ -364,6 +570,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n permalink_url: { type: 'string', description: 'URL to the resource in Asana' }, tasks: { type: 'json', description: 'Array of tasks' }, projects: { type: 'json', description: 'Array of projects' }, + workspaces: { type: 'json', description: 'Array of workspaces' }, + sections: { type: 'json', description: 'Array of sections' }, + followers: { type: 'json', description: 'Array of followers on the task' }, + archived: { type: 'boolean', description: 'Whether the project is archived' }, + color: { type: 'string', description: 'Project color' }, + deleted: { type: 'boolean', description: 'Whether the task was deleted' }, }, } diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index 36834c53985..7243937d258 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -33,6 +33,12 @@ export const GoogleDocsBlock: BlockConfig = { { label: 'Insert Image', id: 'insert_image' }, { label: 'Insert Page Break', id: 'insert_page_break' }, { label: 'Apply Text Style', id: 'update_text_style' }, + { label: 'Apply Paragraph Style', id: 'update_paragraph_style' }, + { label: 'Create Bullets', id: 'create_paragraph_bullets' }, + { label: 'Delete Bullets', id: 'delete_paragraph_bullets' }, + { label: 'Delete Content Range', id: 'delete_content_range' }, + { label: 'Create Named Range', id: 'create_named_range' }, + { label: 'Delete Named Range', id: 'delete_named_range' }, ], value: () => 'read', }, @@ -82,6 +88,12 @@ export const GoogleDocsBlock: BlockConfig = { 'insert_image', 'insert_page_break', 'update_text_style', + 'update_paragraph_style', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'delete_content_range', + 'create_named_range', + 'delete_named_range', ], }, }, @@ -105,6 +117,12 @@ export const GoogleDocsBlock: BlockConfig = { 'insert_image', 'insert_page_break', 'update_text_style', + 'update_paragraph_style', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'delete_content_range', + 'create_named_range', + 'delete_named_range', ], }, }, @@ -275,13 +293,23 @@ Return ONLY the text to insert - no explanations, no extra text.`, condition: { field: 'operation', value: 'insert_image' }, mode: 'advanced', }, - // Apply Text Style fields + // Range fields shared by style/bullet/range/named-range operations { id: 'startIndex', title: 'Start Index', type: 'short-input', placeholder: 'Start character index (inclusive)', - condition: { field: 'operation', value: 'update_text_style' }, + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'delete_content_range', + 'create_named_range', + ], + }, required: true, }, { @@ -289,7 +317,17 @@ Return ONLY the text to insert - no explanations, no extra text.`, title: 'End Index', type: 'short-input', placeholder: 'End character index (exclusive)', - condition: { field: 'operation', value: 'update_text_style' }, + condition: { + field: 'operation', + value: [ + 'update_text_style', + 'update_paragraph_style', + 'create_paragraph_bullets', + 'delete_paragraph_bullets', + 'delete_content_range', + 'create_named_range', + ], + }, required: true, }, { @@ -318,6 +356,76 @@ Return ONLY the text to insert - no explanations, no extra text.`, condition: { field: 'operation', value: 'update_text_style' }, mode: 'advanced', }, + // Apply Paragraph Style fields + { + id: 'namedStyleType', + title: 'Paragraph Style', + type: 'dropdown', + options: [ + { label: 'Normal Text', id: 'NORMAL_TEXT' }, + { label: 'Title', id: 'TITLE' }, + { label: 'Subtitle', id: 'SUBTITLE' }, + { label: 'Heading 1', id: 'HEADING_1' }, + { label: 'Heading 2', id: 'HEADING_2' }, + { label: 'Heading 3', id: 'HEADING_3' }, + { label: 'Heading 4', id: 'HEADING_4' }, + { label: 'Heading 5', id: 'HEADING_5' }, + { label: 'Heading 6', id: 'HEADING_6' }, + ], + condition: { field: 'operation', value: 'update_paragraph_style' }, + }, + { + id: 'alignment', + title: 'Alignment', + type: 'dropdown', + options: [ + { label: 'Default (unchanged)', id: '' }, + { label: 'Left', id: 'LEFT' }, + { label: 'Center', id: 'CENTER' }, + { label: 'Right', id: 'RIGHT' }, + { label: 'Justify', id: 'JUSTIFY' }, + ], + condition: { field: 'operation', value: 'update_paragraph_style' }, + }, + // Create Bullets fields + { + id: 'bulletPreset', + title: 'Bullet Style', + type: 'dropdown', + options: [ + { label: 'Disc / Circle / Square', id: 'BULLET_DISC_CIRCLE_SQUARE' }, + { label: 'Checkbox', id: 'BULLET_CHECKBOX' }, + { label: 'Arrow / Diamond / Disc', id: 'BULLET_ARROW_DIAMOND_DISC' }, + { label: 'Star / Circle / Square', id: 'BULLET_STAR_CIRCLE_SQUARE' }, + { label: 'Numbered: Decimal / Alpha / Roman', id: 'NUMBERED_DECIMAL_ALPHA_ROMAN' }, + { label: 'Numbered: Decimal Nested', id: 'NUMBERED_DECIMAL_NESTED' }, + ], + condition: { field: 'operation', value: 'create_paragraph_bullets' }, + }, + // Create Named Range fields + { + id: 'name', + title: 'Range Name', + type: 'short-input', + placeholder: 'Name for the range (1-256 characters)', + condition: { field: 'operation', value: 'create_named_range' }, + required: true, + }, + // Delete Named Range fields + { + id: 'namedRangeId', + title: 'Named Range ID', + type: 'short-input', + placeholder: 'ID of the named range to delete', + condition: { field: 'operation', value: 'delete_named_range' }, + }, + { + id: 'namedRangeName', + title: 'Named Range Name', + type: 'short-input', + placeholder: 'Name of the named range(s) to delete', + condition: { field: 'operation', value: 'delete_named_range' }, + }, // Shared insertion index (advanced) for the insert operations { id: 'index', @@ -342,6 +450,12 @@ Return ONLY the text to insert - no explanations, no extra text.`, 'google_docs_insert_image', 'google_docs_insert_page_break', 'google_docs_update_text_style', + 'google_docs_update_paragraph_style', + 'google_docs_create_paragraph_bullets', + 'google_docs_delete_paragraph_bullets', + 'google_docs_delete_content_range', + 'google_docs_create_named_range', + 'google_docs_delete_named_range', ], config: { tool: (params) => { @@ -364,6 +478,18 @@ Return ONLY the text to insert - no explanations, no extra text.`, return 'google_docs_insert_page_break' case 'update_text_style': return 'google_docs_update_text_style' + case 'update_paragraph_style': + return 'google_docs_update_paragraph_style' + case 'create_paragraph_bullets': + return 'google_docs_create_paragraph_bullets' + case 'delete_paragraph_bullets': + return 'google_docs_delete_paragraph_bullets' + case 'delete_content_range': + return 'google_docs_delete_content_range' + case 'create_named_range': + return 'google_docs_create_named_range' + case 'delete_named_range': + return 'google_docs_delete_named_range' default: throw new Error(`Invalid Google Docs operation: ${params.operation}`) } @@ -436,6 +562,12 @@ Return ONLY the text to insert - no explanations, no extra text.`, italic: { type: 'boolean', description: 'Apply italic styling' }, underline: { type: 'boolean', description: 'Apply underline styling' }, fontSize: { type: 'number', description: 'Font size in points' }, + namedStyleType: { type: 'string', description: 'Named paragraph style to apply' }, + alignment: { type: 'string', description: 'Paragraph alignment to apply' }, + bulletPreset: { type: 'string', description: 'Bullet glyph preset to apply' }, + name: { type: 'string', description: 'Name for a created named range' }, + namedRangeId: { type: 'string', description: 'ID of a named range to delete' }, + namedRangeName: { type: 'string', description: 'Name of named range(s) to delete' }, }, outputs: { content: { type: 'string', description: 'Document content' }, @@ -446,6 +578,7 @@ Return ONLY the text to insert - no explanations, no extra text.`, description: 'Number of occurrences replaced during find & replace', }, objectId: { type: 'string', description: 'ID of an inserted inline image object' }, + namedRangeId: { type: 'string', description: 'ID of a created named range' }, }, } diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index b9c61b3cd3d..943a01e9f4d 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -50,6 +50,11 @@ export const JiraBlock: BlockConfig = { { label: 'Remove Watcher', id: 'remove_watcher' }, { label: 'Get Users', id: 'get_users' }, { label: 'Search Users', id: 'search_users' }, + { label: 'List Projects', id: 'list_projects' }, + { label: 'Get Project', id: 'get_project' }, + { label: 'Get Transitions', id: 'get_transitions' }, + { label: 'List Issue Types', id: 'list_issue_types' }, + { label: 'Get Fields', id: 'get_fields' }, ], value: () => 'read', }, @@ -91,7 +96,7 @@ export const JiraBlock: BlockConfig = { placeholder: 'Select Jira project', dependsOn: ['credential', 'domain'], mode: 'basic', - required: { field: 'operation', value: ['write', 'read-bulk'] }, + required: { field: 'operation', value: ['write', 'read-bulk', 'get_project'] }, }, // Manual project ID input (advanced mode) { @@ -102,7 +107,7 @@ export const JiraBlock: BlockConfig = { placeholder: 'Enter Jira project ID', dependsOn: ['credential', 'domain'], mode: 'advanced', - required: { field: 'operation', value: ['write', 'read-bulk'] }, + required: { field: 'operation', value: ['write', 'read-bulk', 'get_project'] }, }, // Issue selector (basic mode) { @@ -134,6 +139,7 @@ export const JiraBlock: BlockConfig = { 'delete_worklog', 'add_watcher', 'remove_watcher', + 'get_transitions', ], }, required: { @@ -156,6 +162,7 @@ export const JiraBlock: BlockConfig = { 'delete_worklog', 'add_watcher', 'remove_watcher', + 'get_transitions', ], }, mode: 'basic', @@ -188,6 +195,7 @@ export const JiraBlock: BlockConfig = { 'delete_worklog', 'add_watcher', 'remove_watcher', + 'get_transitions', ], }, required: { @@ -210,6 +218,7 @@ export const JiraBlock: BlockConfig = { 'delete_worklog', 'add_watcher', 'remove_watcher', + 'get_transitions', ], }, mode: 'advanced', @@ -715,6 +724,30 @@ Return ONLY the comment text - no explanations.`, condition: { field: 'operation', value: 'search_users' }, mode: 'advanced', }, + // List Projects fields + { + id: 'projectSearchQuery', + title: 'Project Filter', + type: 'short-input', + placeholder: 'Filter projects by name or key (optional)', + condition: { field: 'operation', value: 'list_projects' }, + }, + { + id: 'listProjectsStartAt', + title: 'Start At', + type: 'short-input', + placeholder: 'Pagination start index (default: 0)', + condition: { field: 'operation', value: 'list_projects' }, + mode: 'advanced', + }, + { + id: 'listProjectsMaxResults', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum projects to return (default: 50, max: 100)', + condition: { field: 'operation', value: 'list_projects' }, + mode: 'advanced', + }, // Trigger SubBlocks ...getTrigger('jira_issue_created').subBlocks, ...getTrigger('jira_issue_updated').subBlocks, @@ -759,6 +792,11 @@ Return ONLY the comment text - no explanations.`, 'jira_remove_watcher', 'jira_get_users', 'jira_search_users', + 'jira_list_projects', + 'jira_get_project', + 'jira_get_transitions', + 'jira_list_issue_types', + 'jira_get_fields', ], config: { tool: (params) => { @@ -813,6 +851,16 @@ Return ONLY the comment text - no explanations.`, return 'jira_get_users' case 'search_users': return 'jira_search_users' + case 'list_projects': + return 'jira_list_projects' + case 'get_project': + return 'jira_get_project' + case 'get_transitions': + return 'jira_get_transitions' + case 'list_issue_types': + return 'jira_list_issue_types' + case 'get_fields': + return 'jira_get_fields' default: return 'jira_retrieve' } @@ -1108,6 +1156,40 @@ Return ONLY the comment text - no explanations.`, : undefined, } } + case 'list_projects': { + return { + ...baseParams, + query: params.projectSearchQuery || undefined, + startAt: params.listProjectsStartAt + ? Number.parseInt(params.listProjectsStartAt) + : undefined, + maxResults: params.listProjectsMaxResults + ? Number.parseInt(params.listProjectsMaxResults) + : undefined, + } + } + case 'get_project': { + return { + ...baseParams, + projectId: effectiveProjectId, + } + } + case 'get_transitions': { + return { + ...baseParams, + issueKey: effectiveIssueKey, + } + } + case 'list_issue_types': { + return { + ...baseParams, + } + } + case 'get_fields': { + return { + ...baseParams, + } + } default: return baseParams } @@ -1198,6 +1280,19 @@ Return ONLY the comment text - no explanations.`, }, searchUsersMaxResults: { type: 'string', description: 'Maximum users to return from search' }, searchUsersStartAt: { type: 'string', description: 'Pagination start index for user search' }, + // List Projects operation inputs + projectSearchQuery: { + type: 'string', + description: 'Filter projects by partial name or key match', + }, + listProjectsStartAt: { + type: 'string', + description: 'Pagination start index for listing projects', + }, + listProjectsMaxResults: { + type: 'string', + description: 'Maximum projects to return when listing', + }, }, outputs: { // Common outputs across all Jira operations @@ -1285,6 +1380,31 @@ Return ONLY the comment text - no explanations.`, description: 'Array of users with accountId, displayName, emailAddress, active status', }, + // jira_list_projects outputs + projects: { + type: 'json', + description: 'Array of projects with id, key, name, projectTypeKey, and lead', + }, + + // jira_get_project / jira_list_issue_types outputs + projectTypeKey: { type: 'string', description: 'Project type key (e.g., software, business)' }, + issueTypes: { + type: 'json', + description: 'Array of issue types with id, name, description, subtask, hierarchyLevel', + }, + + // jira_get_transitions outputs + transitions: { + type: 'json', + description: 'Array of available workflow transitions with id, name, and target status', + }, + + // jira_get_fields outputs + fields: { + type: 'json', + description: 'Array of Jira fields with id, key, name, custom flag, and schema type', + }, + // jira_bulk_read outputs // Note: bulk_read returns an array in the output field, each item contains: // ts, issueKey, summary, description, status, assignee, created, updated diff --git a/apps/sim/blocks/blocks/monday.ts b/apps/sim/blocks/blocks/monday.ts index 1753a05c5ba..a7d95ccb875 100644 --- a/apps/sim/blocks/blocks/monday.ts +++ b/apps/sim/blocks/blocks/monday.ts @@ -4,12 +4,17 @@ import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import type { MondayArchiveItemResponse, + MondayChangeColumnValueResponse, + MondayCreateBoardResponse, + MondayCreateColumnResponse, MondayCreateGroupResponse, MondayCreateItemResponse, MondayCreateSubitemResponse, MondayCreateUpdateResponse, MondayDeleteItemResponse, + MondayDuplicateItemResponse, MondayGetBoardResponse, + MondayGetGroupsResponse, MondayGetItemResponse, MondayGetItemsResponse, MondayListBoardsResponse, @@ -33,6 +38,11 @@ type MondayResponse = | MondaySearchItemsResponse | MondayCreateSubitemResponse | MondayMoveItemToGroupResponse + | MondayChangeColumnValueResponse + | MondayCreateBoardResponse + | MondayCreateColumnResponse + | MondayGetGroupsResponse + | MondayDuplicateItemResponse const BOARD_OPS = [ 'get_board', @@ -41,6 +51,10 @@ const BOARD_OPS = [ 'create_item', 'update_item', 'create_group', + 'get_groups', + 'create_column', + 'change_column_value', + 'duplicate_item', ] const ITEM_ID_OPS = [ @@ -50,6 +64,8 @@ const ITEM_ID_OPS = [ 'archive_item', 'create_update', 'move_item_to_group', + 'change_column_value', + 'duplicate_item', ] export const MondayBlock: BlockConfig = { @@ -77,12 +93,17 @@ export const MondayBlock: BlockConfig = { { label: 'Search Items', id: 'search_items' }, { label: 'Create Item', id: 'create_item' }, { label: 'Update Item', id: 'update_item' }, + { label: 'Change Column Value', id: 'change_column_value' }, + { label: 'Duplicate Item', id: 'duplicate_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' }, + { label: 'Get Groups', id: 'get_groups' }, + { label: 'Create Board', id: 'create_board' }, + { label: 'Create Column', id: 'create_column' }, ], value: () => 'list_boards', }, @@ -243,6 +264,142 @@ export const MondayBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'create_group' }, }, + { + id: 'columnId', + title: 'Column ID', + type: 'short-input', + placeholder: 'Enter column ID (e.g., status)', + condition: { field: 'operation', value: 'change_column_value' }, + required: { field: 'operation', value: 'change_column_value' }, + }, + { + id: 'columnValue', + title: 'Column Value', + type: 'long-input', + placeholder: '{"label":"Done"}', + condition: { field: 'operation', value: 'change_column_value' }, + required: { field: 'operation', value: 'change_column_value' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON value for a single Monday.com column. The shape depends on the column type (e.g., {"label":"Done"} for status, {"date":"2024-01-01"} for date). Return ONLY the JSON value - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'createLabelsIfMissing', + title: 'Create Labels If Missing', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'change_column_value' }, + }, + { + id: 'withUpdates', + title: 'Include Updates', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'duplicate_item' }, + }, + { + id: 'boardName', + title: 'Board Name', + type: 'short-input', + placeholder: 'Enter board name', + condition: { field: 'operation', value: 'create_board' }, + required: { field: 'operation', value: 'create_board' }, + }, + { + id: 'boardKind', + title: 'Board Kind', + type: 'dropdown', + options: [ + { label: 'Public', id: 'public' }, + { label: 'Private', id: 'private' }, + { label: 'Shareable', id: 'share' }, + ], + value: () => 'public', + condition: { field: 'operation', value: 'create_board' }, + required: { field: 'operation', value: 'create_board' }, + }, + { + id: 'boardDescription', + title: 'Board Description', + type: 'long-input', + placeholder: 'Enter board description', + mode: 'advanced', + condition: { field: 'operation', value: 'create_board' }, + }, + { + id: 'workspaceId', + title: 'Workspace ID', + type: 'short-input', + placeholder: 'Enter workspace ID', + mode: 'advanced', + condition: { field: 'operation', value: 'create_board' }, + }, + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'Enter folder ID', + mode: 'advanced', + condition: { field: 'operation', value: 'create_board' }, + }, + { + id: 'columnTitle', + title: 'Column Title', + type: 'short-input', + placeholder: 'Enter column title', + condition: { field: 'operation', value: 'create_column' }, + required: { field: 'operation', value: 'create_column' }, + }, + { + id: 'columnType', + title: 'Column Type', + type: 'dropdown', + options: [ + { label: 'Status', id: 'status' }, + { label: 'Text', id: 'text' }, + { label: 'Long Text', id: 'long_text' }, + { label: 'Numbers', id: 'numbers' }, + { label: 'Date', id: 'date' }, + { label: 'People', id: 'people' }, + { label: 'Dropdown', id: 'dropdown' }, + { label: 'Checkbox', id: 'checkbox' }, + { label: 'Email', id: 'email' }, + { label: 'Phone', id: 'phone' }, + { label: 'Link', id: 'link' }, + { label: 'Timeline', id: 'timeline' }, + { label: 'Tags', id: 'tags' }, + { label: 'Rating', id: 'rating' }, + { label: 'Country', id: 'country' }, + ], + value: () => 'text', + condition: { field: 'operation', value: 'create_column' }, + required: { field: 'operation', value: 'create_column' }, + }, + { + id: 'columnDescription', + title: 'Column Description', + type: 'long-input', + placeholder: 'Enter column description', + mode: 'advanced', + condition: { field: 'operation', value: 'create_column' }, + }, + { + id: 'columnDefaults', + title: 'Column Defaults', + type: 'long-input', + placeholder: '{"labels":{"0":"To Do","1":"Done"}}', + mode: 'advanced', + condition: { field: 'operation', value: 'create_column' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of default settings for a Monday.com column (e.g., status labels). Return ONLY the JSON object string - no explanations, no extra text.', + generationType: 'json-object', + }, + }, { id: 'limit', title: 'Limit', @@ -289,12 +446,17 @@ export const MondayBlock: BlockConfig = { 'monday_search_items', 'monday_create_item', 'monday_update_item', + 'monday_change_column_value', + 'monday_duplicate_item', 'monday_delete_item', 'monday_archive_item', 'monday_move_item_to_group', 'monday_create_subitem', 'monday_create_update', 'monday_create_group', + 'monday_get_groups', + 'monday_create_board', + 'monday_create_column', ], config: { tool: (params) => { @@ -348,6 +510,42 @@ export const MondayBlock: BlockConfig = { itemId: params.itemId, columnValues: params.columnValues, } + case 'change_column_value': + return { + ...baseParams, + boardId: params.boardId, + itemId: params.itemId, + columnId: params.columnId, + value: params.columnValue, + createLabelsIfMissing: Boolean(params.createLabelsIfMissing), + } + case 'duplicate_item': + return { + ...baseParams, + boardId: params.boardId, + itemId: params.itemId, + withUpdates: Boolean(params.withUpdates), + } + case 'create_board': + return { + ...baseParams, + boardName: params.boardName, + boardKind: params.boardKind || 'public', + description: params.boardDescription || undefined, + workspaceId: params.workspaceId || undefined, + folderId: params.folderId || undefined, + } + case 'create_column': + return { + ...baseParams, + boardId: params.boardId, + columnTitle: params.columnTitle, + columnType: params.columnType || 'text', + columnDescription: params.columnDescription || undefined, + columnDefaults: params.columnDefaults || undefined, + } + case 'get_groups': + return { ...baseParams, boardId: params.boardId } case 'delete_item': return { ...baseParams, itemId: params.itemId } case 'archive_item': @@ -394,6 +592,22 @@ export const MondayBlock: BlockConfig = { 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' }, + columnId: { type: 'string', description: 'Single column ID to change' }, + columnValue: { type: 'string', description: 'JSON value for a single column' }, + createLabelsIfMissing: { + type: 'boolean', + description: 'Create status/dropdown labels that do not yet exist', + }, + withUpdates: { type: 'boolean', description: 'Include item updates when duplicating' }, + boardName: { type: 'string', description: 'Board name for creation' }, + boardKind: { type: 'string', description: 'Board kind (public, private, share)' }, + boardDescription: { type: 'string', description: 'Board description' }, + workspaceId: { type: 'string', description: 'Workspace ID for board creation' }, + folderId: { type: 'string', description: 'Folder ID for board creation' }, + columnTitle: { type: 'string', description: 'Column title for creation' }, + columnType: { type: 'string', description: 'Column type for creation' }, + columnDescription: { type: 'string', description: 'Column description' }, + columnDefaults: { type: 'string', description: 'JSON defaults for the new column' }, updateBody: { type: 'string', description: 'Update text content' }, groupName: { type: 'string', description: 'Group name' }, groupColor: { type: 'string', description: 'Group color hex code' }, @@ -412,18 +626,23 @@ export const MondayBlock: BlockConfig = { type: 'json', description: 'Board details (id, name, description, state, boardKind, itemsCount, url, updatedAt)', - condition: { field: 'operation', value: 'get_board' }, + condition: { field: 'operation', value: ['get_board', 'create_board'] }, }, groups: { type: 'json', description: 'Board groups (id, title, color, archived, deleted, position)', - condition: { field: 'operation', value: 'get_board' }, + condition: { field: 'operation', value: ['get_board', 'get_groups'] }, }, columns: { type: 'json', description: 'Board columns (id, title, type)', condition: { field: 'operation', value: 'get_board' }, }, + column: { + type: 'json', + description: 'Created column (id, title, type)', + condition: { field: 'operation', value: 'create_column' }, + }, items: { type: 'json', description: @@ -436,7 +655,15 @@ export const MondayBlock: BlockConfig = { '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'], + value: [ + 'get_item', + 'create_item', + 'update_item', + 'create_subitem', + 'move_item_to_group', + 'change_column_value', + 'duplicate_item', + ], }, }, id: { @@ -457,7 +684,10 @@ export const MondayBlock: BlockConfig = { count: { type: 'number', description: 'Number of returned results', - condition: { field: 'operation', value: ['list_boards', 'get_items', 'search_items'] }, + condition: { + field: 'operation', + value: ['list_boards', 'get_items', 'search_items', 'get_groups'], + }, }, cursor: { type: 'string', diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index bb663157a91..fa54796bd78 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -62,6 +62,13 @@ export const SlackBlock: BlockConfig = { { label: 'Update View', id: 'update_view' }, { label: 'Push View', id: 'push_view' }, { label: 'Publish View', id: 'publish_view' }, + { label: 'Schedule Message', id: 'schedule_message' }, + { label: 'List Scheduled Messages', id: 'list_scheduled_messages' }, + { label: 'Delete Scheduled Message', id: 'delete_scheduled_message' }, + { label: 'Archive Conversation', id: 'archive_conversation' }, + { label: 'Rename Conversation', id: 'rename_conversation' }, + { label: 'Set Conversation Topic', id: 'set_conversation_topic' }, + { label: 'Set Conversation Purpose', id: 'set_conversation_purpose' }, ], value: () => 'send', }, @@ -175,7 +182,7 @@ export const SlackBlock: BlockConfig = { }, required: { field: 'operation', - value: 'list_canvases', + value: ['list_canvases', 'list_scheduled_messages'], not: true, }, }, @@ -219,7 +226,7 @@ export const SlackBlock: BlockConfig = { }, required: { field: 'operation', - value: 'list_canvases', + value: ['list_canvases', 'list_scheduled_messages'], not: true, }, }, @@ -294,7 +301,7 @@ export const SlackBlock: BlockConfig = { value: () => 'text', condition: { field: 'operation', - value: ['send', 'ephemeral', 'update'], + value: ['send', 'ephemeral', 'update', 'schedule_message'], }, }, { @@ -304,12 +311,12 @@ export const SlackBlock: BlockConfig = { placeholder: 'Enter your message (supports Slack mrkdwn)', condition: { field: 'operation', - value: ['send', 'ephemeral'], + value: ['send', 'ephemeral', 'schedule_message'], and: { field: 'messageFormat', value: 'blocks', not: true }, }, required: { field: 'operation', - value: ['send', 'ephemeral'], + value: ['send', 'ephemeral', 'schedule_message'], and: { field: 'messageFormat', value: 'blocks', not: true }, }, }, @@ -321,12 +328,12 @@ export const SlackBlock: BlockConfig = { placeholder: 'JSON array of Block Kit blocks', condition: { field: 'operation', - value: ['send', 'ephemeral', 'update'], + value: ['send', 'ephemeral', 'update', 'schedule_message'], and: { field: 'messageFormat', value: 'blocks' }, }, required: { field: 'operation', - value: ['send', 'ephemeral', 'update'], + value: ['send', 'ephemeral', 'update', 'schedule_message'], and: { field: 'messageFormat', value: 'blocks' }, }, wandConfig: { @@ -383,7 +390,7 @@ Do not include any explanations, markdown formatting, or other text outside the placeholder: 'Reply to thread (e.g., 1405894322.002768)', condition: { field: 'operation', - value: ['send', 'ephemeral'], + value: ['send', 'ephemeral', 'schedule_message'], }, required: false, }, @@ -1344,6 +1351,105 @@ Do not include any explanations, markdown formatting, or other text outside the placeholder: 'Describe the view/modal you want to create...', }, }, + // Schedule Message specific fields + { + id: 'scheduleAt', + title: 'Send At', + type: 'short-input', + placeholder: 'Unix timestamp in seconds (e.g., 1700000000)', + condition: { + field: 'operation', + value: 'schedule_message', + }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a Unix timestamp in seconds based on the user's description. +The timestamp must represent a time in the future (Slack rejects past times and times more than 120 days out). +Examples: +- "in 1 hour" -> current Unix time + 3600 +- "tomorrow at 9am" -> Unix timestamp for tomorrow 09:00 local time +- "next Monday" -> Unix timestamp for the next Monday at 00:00 + +If the input looks like a reference to another block's output (contains < and >) or is already a numeric Unix timestamp, return it as-is. +Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra text.`, + placeholder: 'Describe when to send (e.g., "in 2 hours", "tomorrow at 9am")...', + generationType: 'timestamp', + }, + }, + // List Scheduled Messages specific fields + { + id: 'scheduledLimit', + title: 'Message Limit', + type: 'short-input', + placeholder: '100', + condition: { + field: 'operation', + value: 'list_scheduled_messages', + }, + mode: 'advanced', + required: false, + }, + { + id: 'scheduledCursor', + title: 'Pagination Cursor', + type: 'short-input', + placeholder: 'next_cursor from a previous response', + condition: { + field: 'operation', + value: 'list_scheduled_messages', + }, + mode: 'advanced', + required: false, + }, + // Delete Scheduled Message specific fields + { + id: 'scheduledMessageId', + title: 'Scheduled Message ID', + type: 'short-input', + placeholder: 'Scheduled message ID (e.g., Q1234ABCD)', + condition: { + field: 'operation', + value: 'delete_scheduled_message', + }, + required: true, + }, + // Rename Conversation specific fields + { + id: 'renameChannelName', + title: 'New Channel Name', + type: 'short-input', + placeholder: 'e.g., project-updates (max 80 chars)', + condition: { + field: 'operation', + value: 'rename_conversation', + }, + required: true, + }, + // Set Conversation Topic specific fields + { + id: 'conversationTopic', + title: 'Topic', + type: 'long-input', + placeholder: 'New channel topic (max 250 characters)', + condition: { + field: 'operation', + value: 'set_conversation_topic', + }, + required: true, + }, + // Set Conversation Purpose specific fields + { + id: 'conversationPurpose', + title: 'Purpose', + type: 'long-input', + placeholder: 'New channel purpose/description (max 250 characters)', + condition: { + field: 'operation', + value: 'set_conversation_purpose', + }, + required: true, + }, ...getTrigger('slack_webhook').subBlocks, ], tools: { @@ -1383,6 +1489,13 @@ Do not include any explanations, markdown formatting, or other text outside the 'slack_update_view', 'slack_push_view', 'slack_publish_view', + 'slack_schedule_message', + 'slack_list_scheduled_messages', + 'slack_delete_scheduled_message', + 'slack_archive_conversation', + 'slack_rename_conversation', + 'slack_set_conversation_topic', + 'slack_set_conversation_purpose', ], config: { tool: (params) => { @@ -1457,6 +1570,20 @@ Do not include any explanations, markdown formatting, or other text outside the return 'slack_push_view' case 'publish_view': return 'slack_publish_view' + case 'schedule_message': + return 'slack_schedule_message' + case 'list_scheduled_messages': + return 'slack_list_scheduled_messages' + case 'delete_scheduled_message': + return 'slack_delete_scheduled_message' + case 'archive_conversation': + return 'slack_archive_conversation' + case 'rename_conversation': + return 'slack_rename_conversation' + case 'set_conversation_topic': + return 'slack_set_conversation_topic' + case 'set_conversation_purpose': + return 'slack_set_conversation_purpose' default: throw new Error(`Invalid Slack operation: ${params.operation}`) } @@ -1539,6 +1666,13 @@ Do not include any explanations, markdown formatting, or other text outside the fileId, fileName, paginationCursor, + scheduleAt, + scheduledLimit, + scheduledCursor, + scheduledMessageId, + renameChannelName, + conversationTopic, + conversationPurpose, ...rest } = params @@ -1869,6 +2003,54 @@ Do not include any explanations, markdown formatting, or other text outside the } baseParams.view = viewPayload break + + case 'schedule_message': { + baseParams.text = messageFormat === 'blocks' && !text ? ' ' : text + if (blocks) { + baseParams.blocks = blocks + } + if (threadTs) { + baseParams.threadTs = threadTs + } + const parsedPostAt = Number.parseInt(String(scheduleAt ?? '').trim(), 10) + if (Number.isNaN(parsedPostAt)) { + throw new Error('Send At must be a Unix timestamp in seconds') + } + baseParams.postAt = parsedPostAt + break + } + + case 'list_scheduled_messages': { + if (scheduledLimit) { + const parsedLimit = Number.parseInt(scheduledLimit, 10) + if (!Number.isNaN(parsedLimit) && parsedLimit > 0) { + baseParams.limit = parsedLimit + } + } + if (scheduledCursor) { + baseParams.cursor = String(scheduledCursor).trim() + } + break + } + + case 'delete_scheduled_message': + baseParams.scheduledMessageId = scheduledMessageId + break + + case 'archive_conversation': + break + + case 'rename_conversation': + baseParams.name = renameChannelName + break + + case 'set_conversation_topic': + baseParams.topic = conversationTopic + break + + case 'set_conversation_purpose': + baseParams.purpose = conversationPurpose + break } return baseParams @@ -2012,6 +2194,28 @@ Do not include any explanations, markdown formatting, or other text outside the description: 'User ID to publish Home tab view to', }, viewPayload: { type: 'json', description: 'View payload object with type, title, and blocks' }, + // Schedule Message inputs + scheduleAt: { + type: 'string', + description: 'Unix timestamp (seconds) for when the scheduled message should post', + }, + // List Scheduled Messages inputs + scheduledLimit: { + type: 'string', + description: 'Maximum number of scheduled messages to return', + }, + scheduledCursor: { type: 'string', description: 'Pagination cursor for scheduled messages' }, + // Delete Scheduled Message inputs + scheduledMessageId: { type: 'string', description: 'Scheduled message ID to delete' }, + // Rename Conversation inputs + renameChannelName: { type: 'string', description: 'New name for the channel' }, + // Set Conversation Topic inputs + conversationTopic: { type: 'string', description: 'New channel topic (max 250 characters)' }, + // Set Conversation Purpose inputs + conversationPurpose: { + type: 'string', + description: 'New channel purpose/description (max 250 characters)', + }, }, outputs: { // slack_message outputs (send operation) @@ -2199,6 +2403,29 @@ Do not include any explanations, markdown formatting, or other text outside the 'Array of per-user error objects when force is true and some invitations failed (user, ok, error)', }, + // slack_schedule_message outputs (schedule_message operation) + scheduledMessageId: { + type: 'string', + description: 'Identifier of the scheduled message (used to delete it before it posts)', + }, + postAt: { + type: 'number', + description: 'Unix timestamp when a scheduled message will post', + }, + + // slack_list_scheduled_messages outputs (list_scheduled_messages operation) + scheduledMessages: { + type: 'json', + description: + 'Array of pending scheduled message objects with properties: id, channel_id, post_at, date_created, text', + }, + + // slack_set_conversation_purpose outputs (set_conversation_purpose operation) + purpose: { + type: 'string', + description: 'The purpose/description that was set on the channel', + }, + // Trigger outputs (when used as webhook trigger) event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' }, subtype: { diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index 9cb459895e4..371ab9dcaa9 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -73,9 +73,16 @@ export const TrelloBlock: BlockConfig = { { label: 'Get Lists', id: 'trello_list_lists' }, { label: 'List Cards', id: 'trello_list_cards' }, { label: 'Create Card', id: 'trello_create_card' }, + { label: 'Get Card', id: 'trello_get_card' }, { label: 'Update Card', id: 'trello_update_card' }, { label: 'Get Actions', id: 'trello_get_actions' }, { label: 'Add Comment', id: 'trello_add_comment' }, + { label: 'Add Checklist', id: 'trello_add_checklist' }, + { label: 'Add Label', id: 'trello_add_label' }, + { label: 'Add Member', id: 'trello_add_member' }, + { label: 'Create Board', id: 'trello_create_board' }, + { label: 'Get Board', id: 'trello_get_board' }, + { label: 'Create List', id: 'trello_create_list' }, ], value: () => 'trello_list_lists', }, @@ -111,11 +118,17 @@ export const TrelloBlock: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: ['trello_list_lists', 'trello_list_cards', 'trello_get_actions'], + value: [ + 'trello_list_lists', + 'trello_list_cards', + 'trello_get_actions', + 'trello_get_board', + 'trello_create_list', + ], }, required: { field: 'operation', - value: 'trello_list_lists', + value: ['trello_list_lists', 'trello_get_board', 'trello_create_list'], }, }, { @@ -128,11 +141,17 @@ export const TrelloBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['trello_list_lists', 'trello_list_cards', 'trello_get_actions'], + value: [ + 'trello_list_lists', + 'trello_list_cards', + 'trello_get_actions', + 'trello_get_board', + 'trello_create_list', + ], }, required: { field: 'operation', - value: 'trello_list_lists', + value: ['trello_list_lists', 'trello_get_board', 'trello_create_list'], }, }, { @@ -156,11 +175,26 @@ export const TrelloBlock: BlockConfig = { placeholder: 'Enter Trello card ID', condition: { field: 'operation', - value: ['trello_update_card', 'trello_get_actions', 'trello_add_comment'], + value: [ + 'trello_update_card', + 'trello_get_actions', + 'trello_add_comment', + 'trello_get_card', + 'trello_add_checklist', + 'trello_add_label', + 'trello_add_member', + ], }, required: { field: 'operation', - value: ['trello_update_card', 'trello_add_comment'], + value: [ + 'trello_update_card', + 'trello_add_comment', + 'trello_get_card', + 'trello_add_checklist', + 'trello_add_label', + 'trello_add_member', + ], }, }, { @@ -327,6 +361,120 @@ Return ONLY the date/timestamp string - no explanations, no extra text.`, }, required: true, }, + { + id: 'boardName', + title: 'Board Name', + type: 'short-input', + placeholder: 'Enter board name', + condition: { + field: 'operation', + value: 'trello_create_board', + }, + required: true, + }, + { + id: 'boardDesc', + title: 'Description', + type: 'long-input', + placeholder: 'Enter board description', + condition: { + field: 'operation', + value: 'trello_create_board', + }, + }, + { + id: 'idOrganization', + title: 'Workspace ID', + type: 'short-input', + placeholder: 'Enter workspace/organization ID or name', + mode: 'advanced', + condition: { + field: 'operation', + value: 'trello_create_board', + }, + }, + { + id: 'defaultLists', + title: 'Default Lists', + type: 'dropdown', + options: [ + { label: 'Leave Unset', id: '' }, + { label: 'Create Default Lists', id: 'true' }, + { label: 'No Default Lists', id: 'false' }, + ], + value: () => '', + mode: 'advanced', + condition: { + field: 'operation', + value: 'trello_create_board', + }, + }, + { + id: 'listName', + title: 'List Name', + type: 'short-input', + placeholder: 'Enter list name', + condition: { + field: 'operation', + value: 'trello_create_list', + }, + required: true, + }, + { + id: 'listPos', + title: 'List Position', + type: 'short-input', + placeholder: 'top, bottom, or a positive float', + mode: 'advanced', + condition: { + field: 'operation', + value: 'trello_create_list', + }, + }, + { + id: 'checklistName', + title: 'Checklist Name', + type: 'short-input', + placeholder: 'Enter checklist name', + condition: { + field: 'operation', + value: 'trello_add_checklist', + }, + required: true, + }, + { + id: 'checklistPos', + title: 'Checklist Position', + type: 'short-input', + placeholder: 'top, bottom, or a positive float', + mode: 'advanced', + condition: { + field: 'operation', + value: 'trello_add_checklist', + }, + }, + { + id: 'labelId', + title: 'Label ID', + type: 'short-input', + placeholder: 'Enter Trello label ID', + condition: { + field: 'operation', + value: 'trello_add_label', + }, + required: true, + }, + { + id: 'memberId', + title: 'Member ID', + type: 'short-input', + placeholder: 'Enter Trello member ID', + condition: { + field: 'operation', + value: 'trello_add_member', + }, + required: true, + }, ], tools: { access: [ @@ -336,6 +484,13 @@ Return ONLY the date/timestamp string - no explanations, no extra text.`, 'trello_update_card', 'trello_get_actions', 'trello_add_comment', + 'trello_create_board', + 'trello_get_board', + 'trello_create_list', + 'trello_get_card', + 'trello_add_checklist', + 'trello_add_label', + 'trello_add_member', ], config: { tool: (params) => getTrimmedString(params.operation) ?? 'trello_list_lists', @@ -462,6 +617,126 @@ Return ONLY the date/timestamp string - no explanations, no extra text.`, } } + case 'trello_create_board': { + const name = getTrimmedString(params.boardName) + + if (!name) { + throw new Error('Board name is required.') + } + + return { + ...baseParams, + name, + desc: getTrimmedString(params.boardDesc), + idOrganization: getTrimmedString(params.idOrganization), + defaultLists: parseOptionalBooleanInput(params.defaultLists), + } + } + + case 'trello_get_board': { + const boardId = getTrimmedString(params.boardId) + + if (!boardId) { + throw new Error('Board ID is required.') + } + + return { + ...baseParams, + boardId, + } + } + + case 'trello_create_list': { + const boardId = getTrimmedString(params.boardId) + const name = getTrimmedString(params.listName) + + if (!boardId) { + throw new Error('Board ID is required.') + } + + if (!name) { + throw new Error('List name is required.') + } + + return { + ...baseParams, + boardId, + name, + pos: getTrimmedString(params.listPos), + } + } + + case 'trello_get_card': { + const cardId = getTrimmedString(params.cardId) + + if (!cardId) { + throw new Error('Card ID is required.') + } + + return { + ...baseParams, + cardId, + } + } + + case 'trello_add_checklist': { + const cardId = getTrimmedString(params.cardId) + const name = getTrimmedString(params.checklistName) + + if (!cardId) { + throw new Error('Card ID is required.') + } + + if (!name) { + throw new Error('Checklist name is required.') + } + + return { + ...baseParams, + cardId, + name, + pos: getTrimmedString(params.checklistPos), + } + } + + case 'trello_add_label': { + const cardId = getTrimmedString(params.cardId) + const labelId = getTrimmedString(params.labelId) + + if (!cardId) { + throw new Error('Card ID is required.') + } + + if (!labelId) { + throw new Error('Label ID is required.') + } + + return { + ...baseParams, + cardId, + labelId, + } + } + + case 'trello_add_member': { + const cardId = getTrimmedString(params.cardId) + const memberId = getTrimmedString(params.memberId) + + if (!cardId) { + throw new Error('Card ID is required.') + } + + if (!memberId) { + throw new Error('Member ID is required.') + } + + return { + ...baseParams, + cardId, + memberId, + } + } + default: return baseParams } @@ -489,6 +764,25 @@ Return ONLY the date/timestamp string - no explanations, no extra text.`, limit: { type: 'number', description: 'Maximum number of board actions to return' }, page: { type: 'number', description: 'Page number for action results' }, text: { type: 'string', description: 'Comment text' }, + boardName: { type: 'string', description: 'Board name' }, + boardDesc: { type: 'string', description: 'Board description' }, + idOrganization: { + type: 'string', + description: 'Workspace/organization ID or name for a new board', + }, + defaultLists: { + type: 'boolean', + description: 'Whether to create default lists on a new board', + }, + listName: { type: 'string', description: 'List name' }, + listPos: { type: 'string', description: 'List position (top, bottom, or positive float)' }, + checklistName: { type: 'string', description: 'Checklist name' }, + checklistPos: { + type: 'string', + description: 'Checklist position (top, bottom, or positive float)', + }, + labelId: { type: 'string', description: 'Label ID to attach to a card' }, + memberId: { type: 'string', description: 'Member ID to assign to a card' }, }, outputs: { lists: { @@ -503,7 +797,27 @@ Return ONLY the date/timestamp string - no explanations, no extra text.`, card: { type: 'json', description: - 'Created or updated card (id, name, desc, url, idBoard, idList, closed, labelIds, labels, due, dueComplete)', + 'Created, updated, or fetched card (id, name, desc, url, idBoard, idList, closed, labelIds, labels, due, dueComplete)', + }, + board: { + type: 'json', + description: 'Created or fetched board (id, name, desc, url, closed, idOrganization)', + }, + list: { + type: 'json', + description: 'Created list (id, name, closed, pos, idBoard)', + }, + checklist: { + type: 'json', + description: 'Created checklist (id, name, idCard, idBoard, pos)', + }, + labelIds: { + type: 'json', + description: 'Label IDs applied to a card after adding a label', + }, + memberIds: { + type: 'json', + description: 'Member IDs assigned to a card after adding a member', }, actions: { type: 'json', diff --git a/apps/sim/lib/api/contracts/tools/asana.ts b/apps/sim/lib/api/contracts/tools/asana.ts index e51d8c1e8ff..5364dda722e 100644 --- a/apps/sim/lib/api/contracts/tools/asana.ts +++ b/apps/sim/lib/api/contracts/tools/asana.ts @@ -79,6 +79,66 @@ const asanaGetTaskResponseSchema = z.union([ asanaTasksResponseSchema, ]) +const asanaProjectRecordResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + gid: z.string(), + name: z.string(), + notes: z.string(), + archived: z.boolean().optional(), + color: z.string().nullable().optional(), + created_at: z.string().optional(), + modified_at: z.string().optional(), + permalink_url: z.string().optional(), +}) + +const asanaWorkspaceSchema = z.object({ + gid: z.string(), + name: z.string(), + resource_type: z.string().optional(), +}) + +const asanaListWorkspacesResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + workspaces: z.array(asanaWorkspaceSchema), +}) + +const asanaDeleteTaskResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + gid: z.string(), + deleted: z.literal(true), +}) + +const asanaAddFollowersResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + gid: z.string(), + name: z.string(), + followers: z.array(asanaUserSummarySchema), +}) + +const asanaSectionSchema = z.object({ + gid: z.string(), + name: z.string(), + resource_type: z.string().optional(), +}) + +const asanaSectionResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + gid: z.string(), + name: z.string(), + created_at: z.string().optional(), +}) + +const asanaListSectionsResponseSchema = z.object({ + success: z.literal(true), + ts: z.string(), + sections: z.array(asanaSectionSchema), +}) + export const asanaAddCommentBodySchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), taskGid: z.string().min(1, 'Task GID is required'), @@ -126,6 +186,53 @@ export const asanaUpdateTaskBodySchema = z.object({ due_on: z.string().nullish(), }) +export const asanaCreateProjectBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + workspace: z.string().min(1, 'Workspace GID is required'), + name: z.string().min(1, 'Project name is required'), + notes: z.string().nullish(), +}) + +export const asanaGetProjectBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + projectGid: z.string().min(1, 'Project GID is required'), +}) + +export const asanaListWorkspacesBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), +}) + +export const asanaCreateSubtaskBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + taskGid: z.string().min(1, 'Parent task GID is required'), + name: z.string().min(1, 'Subtask name is required'), + notes: z.string().nullish(), + assignee: z.string().nullish(), + due_on: z.string().nullish(), +}) + +export const asanaDeleteTaskBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + taskGid: z.string().min(1, 'Task GID is required'), +}) + +export const asanaAddFollowersBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + taskGid: z.string().min(1, 'Task GID is required'), + followers: z.array(z.string().min(1)).min(1, 'At least one follower GID is required'), +}) + +export const asanaCreateSectionBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + projectGid: z.string().min(1, 'Project GID is required'), + name: z.string().min(1, 'Section name is required'), +}) + +export const asanaListSectionsBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + projectGid: z.string().min(1, 'Project GID is required'), +}) + export const asanaAddCommentContract = defineRouteContract({ method: 'POST', path: '/api/tools/asana/add-comment', @@ -168,6 +275,87 @@ export const asanaUpdateTaskContract = defineRouteContract({ response: { mode: 'json', schema: asanaTaskMutationResponseSchema }, }) +export const asanaCreateProjectContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/create-project', + body: asanaCreateProjectBodySchema, + response: { mode: 'json', schema: asanaProjectRecordResponseSchema }, +}) + +export const asanaGetProjectContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/get-project', + body: asanaGetProjectBodySchema, + response: { mode: 'json', schema: asanaProjectRecordResponseSchema }, +}) + +export const asanaListWorkspacesContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/list-workspaces', + body: asanaListWorkspacesBodySchema, + response: { mode: 'json', schema: asanaListWorkspacesResponseSchema }, +}) + +export const asanaCreateSubtaskContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/create-subtask', + body: asanaCreateSubtaskBodySchema, + response: { mode: 'json', schema: asanaTaskMutationResponseSchema }, +}) + +export const asanaDeleteTaskContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/delete-task', + body: asanaDeleteTaskBodySchema, + response: { mode: 'json', schema: asanaDeleteTaskResponseSchema }, +}) + +export const asanaAddFollowersContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/add-followers', + body: asanaAddFollowersBodySchema, + response: { mode: 'json', schema: asanaAddFollowersResponseSchema }, +}) + +export const asanaCreateSectionContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/create-section', + body: asanaCreateSectionBodySchema, + response: { mode: 'json', schema: asanaSectionResponseSchema }, +}) + +export const asanaListSectionsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/asana/list-sections', + body: asanaListSectionsBodySchema, + response: { mode: 'json', schema: asanaListSectionsResponseSchema }, +}) + +export type AsanaCreateProjectBody = ContractBody +export type AsanaCreateProjectBodyInput = ContractBodyInput +export type AsanaCreateProjectResponse = ContractJsonResponse +export type AsanaGetProjectBody = ContractBody +export type AsanaGetProjectBodyInput = ContractBodyInput +export type AsanaGetProjectResponse = ContractJsonResponse +export type AsanaListWorkspacesBody = ContractBody +export type AsanaListWorkspacesBodyInput = ContractBodyInput +export type AsanaListWorkspacesResponse = ContractJsonResponse +export type AsanaCreateSubtaskBody = ContractBody +export type AsanaCreateSubtaskBodyInput = ContractBodyInput +export type AsanaCreateSubtaskResponse = ContractJsonResponse +export type AsanaDeleteTaskBody = ContractBody +export type AsanaDeleteTaskBodyInput = ContractBodyInput +export type AsanaDeleteTaskResponse = ContractJsonResponse +export type AsanaAddFollowersBody = ContractBody +export type AsanaAddFollowersBodyInput = ContractBodyInput +export type AsanaAddFollowersResponse = ContractJsonResponse +export type AsanaCreateSectionBody = ContractBody +export type AsanaCreateSectionBodyInput = ContractBodyInput +export type AsanaCreateSectionResponse = ContractJsonResponse +export type AsanaListSectionsBody = ContractBody +export type AsanaListSectionsBodyInput = ContractBodyInput +export type AsanaListSectionsResponse = ContractJsonResponse + export type AsanaAddCommentBody = ContractBody export type AsanaAddCommentBodyInput = ContractBodyInput export type AsanaAddCommentResponse = ContractJsonResponse diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 4649bc1cad4..917a746c666 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-29", + "updatedAt": "2026-06-30", "integrations": [ { "type": "onepassword", @@ -949,9 +949,41 @@ { "name": "Add Comment", "description": "Add a comment (story) to an Asana task" + }, + { + "name": "Create Subtask", + "description": "Create a subtask under an existing Asana task" + }, + { + "name": "Delete Task", + "description": "Delete an Asana task by its GID (moves it to the trash)" + }, + { + "name": "Add Followers", + "description": "Add one or more followers to an Asana task" + }, + { + "name": "Create Project", + "description": "Create a new project in an Asana workspace" + }, + { + "name": "Get Project", + "description": "Retrieve a single Asana project by its GID" + }, + { + "name": "List Workspaces", + "description": "List all Asana workspaces and organizations the authenticated user belongs to" + }, + { + "name": "Create Section", + "description": "Create a new section in an Asana project" + }, + { + "name": "List Sections", + "description": "List all sections in an Asana project" } ], - "operationCount": 6, + "operationCount": 14, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -6383,9 +6415,33 @@ { "name": "Apply Text Style", "description": "Apply bold, italic, underline, and/or font size to a range of text in a Google Docs document, identified by its start and end character index." + }, + { + "name": "Apply Paragraph Style", + "description": "Apply a named paragraph style (such as a heading or title) and/or alignment to the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index." + }, + { + "name": "Create Bullets", + "description": "Add bulleted or numbered list formatting to the paragraphs overlapping a range of text in a Google Docs document, using a chosen bullet glyph preset." + }, + { + "name": "Delete Bullets", + "description": "Remove bullet or numbered list formatting from the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index." + }, + { + "name": "Delete Content Range", + "description": "Delete all content between a start and end character index in a Google Docs document. The endIndex is exclusive and must be greater than the startIndex." + }, + { + "name": "Create Named Range", + "description": "Create a named range over a span of content in a Google Docs document so it can be referenced or deleted later. The name may be 1-256 characters and need not be unique." + }, + { + "name": "Delete Named Range", + "description": "Delete one or more named ranges from a Google Docs document by their ID or by name. Provide exactly one of namedRangeId or name; deleting by name removes all ranges sharing that name. The content itself is not removed." } ], - "operationCount": 9, + "operationCount": 15, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -8729,9 +8785,29 @@ { "name": "Search Users", "description": "Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress." + }, + { + "name": "List Projects", + "description": "List Jira projects visible to the user, with optional name/key filtering and pagination. Returns each project with id, key, name, and type." + }, + { + "name": "Get Project", + "description": "Get the details of a single Jira project by its ID or key, including its type, lead, components, issue types, and versions." + }, + { + "name": "Get Transitions", + "description": "Get the workflow transitions available for an issue in its current status. Use the returned transition IDs with the Transition Issue operation." + }, + { + "name": "List Issue Types", + "description": "List all issue types visible to the user across projects (e.g., Task, Bug, Story, Epic, Subtask). Useful for discovering valid issue types before creating an issue." + }, + { + "name": "Get Fields", + "description": "Get all system and custom fields defined in the Jira instance. Useful for discovering custom field IDs (e.g., customfield_10001) to use when writing or updating issues." } ], - "operationCount": 25, + "operationCount": 30, "triggers": [ { "id": "jira_issue_created", @@ -10979,6 +11055,14 @@ "name": "Update Item", "description": "Update column values of an item on a Monday.com board" }, + { + "name": "Change Column Value", + "description": "Update a single column's value on a Monday.com item" + }, + { + "name": "Duplicate Item", + "description": "Duplicate an existing item on a Monday.com board" + }, { "name": "Delete Item", "description": "Delete an item from a Monday.com board" @@ -11002,9 +11086,21 @@ { "name": "Create Group", "description": "Create a new group on a Monday.com board" + }, + { + "name": "Get Groups", + "description": "Get the groups on a Monday.com board" + }, + { + "name": "Create Board", + "description": "Create a new board in Monday.com" + }, + { + "name": "Create Column", + "description": "Create a new column on a Monday.com board" } ], - "operationCount": 13, + "operationCount": 18, "triggers": [ { "id": "monday_item_created", @@ -15506,9 +15602,37 @@ { "name": "Publish View", "description": "Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience." + }, + { + "name": "Schedule Message", + "description": "Schedule a message to be sent to a Slack channel or DM at a future time." + }, + { + "name": "List Scheduled Messages", + "description": "List pending scheduled messages in a Slack workspace, optionally filtered by channel." + }, + { + "name": "Delete Scheduled Message", + "description": "Delete a pending scheduled message before it posts to Slack." + }, + { + "name": "Archive Conversation", + "description": "Archive a Slack channel so it is closed to new activity." + }, + { + "name": "Rename Conversation", + "description": "Rename an existing Slack channel." + }, + { + "name": "Set Conversation Topic", + "description": "Set the topic for a Slack channel (max 250 characters)." + }, + { + "name": "Set Conversation Purpose", + "description": "Set the purpose (description) for a Slack channel (max 250 characters)." } ], - "operationCount": 35, + "operationCount": 42, "triggers": [ { "id": "slack_webhook", @@ -17666,6 +17790,10 @@ "name": "Create Card", "description": "Create a new card in a Trello list" }, + { + "name": "Get Card", + "description": "Retrieve a single Trello card by ID" + }, { "name": "Update Card", "description": "Update an existing card on Trello" @@ -17677,9 +17805,33 @@ { "name": "Add Comment", "description": "Add a comment to a Trello card" + }, + { + "name": "Add Checklist", + "description": "Add a checklist to a Trello card" + }, + { + "name": "Add Label", + "description": "Attach an existing label to a Trello card" + }, + { + "name": "Add Member", + "description": "Assign a member to a Trello card" + }, + { + "name": "Create Board", + "description": "Create a new Trello board" + }, + { + "name": "Get Board", + "description": "Retrieve a single Trello board by ID" + }, + { + "name": "Create List", + "description": "Create a new list on a Trello board" } ], - "operationCount": 6, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "oauth", diff --git a/apps/sim/tools/asana/add_followers.ts b/apps/sim/tools/asana/add_followers.ts new file mode 100644 index 00000000000..4c3c8c358cc --- /dev/null +++ b/apps/sim/tools/asana/add_followers.ts @@ -0,0 +1,87 @@ +import type { AsanaAddFollowersParams, AsanaAddFollowersResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaAddFollowersTool: ToolConfig = + { + id: 'asana_add_followers', + name: 'Asana Add Followers', + description: 'Add one or more followers to an Asana task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + taskGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GID of the Asana task (numeric string)', + }, + followers: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Array of user GIDs to add as followers to the task', + }, + }, + + request: { + url: '/api/tools/asana/add-followers', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + taskGid: params.taskGid, + followers: params.followers, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), gid: '', name: '', followers: [] }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'Task globally unique identifier' }, + name: { type: 'string', description: 'Task name' }, + followers: { + type: 'array', + description: 'Current followers on the task after the update', + items: { + type: 'object', + properties: { + gid: { type: 'string', description: 'Follower GID' }, + name: { type: 'string', description: 'Follower name' }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/asana/create_project.ts b/apps/sim/tools/asana/create_project.ts new file mode 100644 index 00000000000..b79bcefb4f6 --- /dev/null +++ b/apps/sim/tools/asana/create_project.ts @@ -0,0 +1,91 @@ +import type { AsanaCreateProjectParams, AsanaProjectRecordResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaCreateProjectTool: ToolConfig< + AsanaCreateProjectParams, + AsanaProjectRecordResponse +> = { + id: 'asana_create_project', + name: 'Asana Create Project', + description: 'Create a new project in an Asana workspace', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + workspace: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Asana workspace GID (numeric string) where the project will be created', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the project', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Notes or description for the project', + }, + }, + + request: { + url: '/api/tools/asana/create-project', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + workspace: params.workspace, + name: params.name, + notes: params.notes, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), gid: '', name: '', notes: '' }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'Project globally unique identifier' }, + name: { type: 'string', description: 'Project name' }, + notes: { type: 'string', description: 'Project notes or description' }, + archived: { type: 'boolean', description: 'Whether the project is archived' }, + color: { type: 'string', description: 'Project color' }, + created_at: { type: 'string', description: 'Project creation timestamp' }, + modified_at: { type: 'string', description: 'Project last modified timestamp' }, + permalink_url: { type: 'string', description: 'URL to the project in Asana' }, + }, +} diff --git a/apps/sim/tools/asana/create_section.ts b/apps/sim/tools/asana/create_section.ts new file mode 100644 index 00000000000..90a40da32c4 --- /dev/null +++ b/apps/sim/tools/asana/create_section.ts @@ -0,0 +1,76 @@ +import type { AsanaCreateSectionParams, AsanaSectionResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaCreateSectionTool: ToolConfig = { + id: 'asana_create_section', + name: 'Asana Create Section', + description: 'Create a new section in an Asana project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + projectGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GID of the Asana project (numeric string) to add the section to', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the section', + }, + }, + + request: { + url: '/api/tools/asana/create-section', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + projectGid: params.projectGid, + name: params.name, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), gid: '', name: '' }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'Section globally unique identifier' }, + name: { type: 'string', description: 'Section name' }, + created_at: { type: 'string', description: 'Section creation timestamp' }, + }, +} diff --git a/apps/sim/tools/asana/create_subtask.ts b/apps/sim/tools/asana/create_subtask.ts new file mode 100644 index 00000000000..1e653b9871f --- /dev/null +++ b/apps/sim/tools/asana/create_subtask.ts @@ -0,0 +1,109 @@ +import type { AsanaCreateSubtaskParams, AsanaCreateTaskResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaCreateSubtaskTool: ToolConfig = + { + id: 'asana_create_subtask', + name: 'Asana Create Subtask', + description: 'Create a subtask under an existing Asana task', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + taskGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GID of the parent Asana task (numeric string)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the subtask', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Notes or description for the subtask', + }, + assignee: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User GID to assign the subtask to', + }, + due_on: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date in YYYY-MM-DD format', + }, + }, + + request: { + url: '/api/tools/asana/create-subtask', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + taskGid: params.taskGid, + name: params.name, + notes: params.notes, + assignee: params.assignee, + due_on: params.due_on, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { + ts: new Date().toISOString(), + gid: '', + name: '', + notes: '', + completed: false, + created_at: new Date().toISOString(), + permalink_url: '', + }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'Subtask globally unique identifier' }, + name: { type: 'string', description: 'Subtask name' }, + notes: { type: 'string', description: 'Subtask notes or description' }, + completed: { type: 'boolean', description: 'Whether the subtask is completed' }, + created_at: { type: 'string', description: 'Subtask creation timestamp' }, + permalink_url: { type: 'string', description: 'URL to the subtask in Asana' }, + }, + } diff --git a/apps/sim/tools/asana/delete_task.ts b/apps/sim/tools/asana/delete_task.ts new file mode 100644 index 00000000000..337238c8d45 --- /dev/null +++ b/apps/sim/tools/asana/delete_task.ts @@ -0,0 +1,68 @@ +import type { AsanaDeleteTaskParams, AsanaDeleteTaskResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaDeleteTaskTool: ToolConfig = { + id: 'asana_delete_task', + name: 'Asana Delete Task', + description: 'Delete an Asana task by its GID (moves it to the trash)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + taskGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GID of the Asana task to delete (numeric string)', + }, + }, + + request: { + url: '/api/tools/asana/delete-task', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + taskGid: params.taskGid, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), gid: '', deleted: true }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'GID of the deleted task' }, + deleted: { type: 'boolean', description: 'Whether the task was deleted' }, + }, +} diff --git a/apps/sim/tools/asana/get_project.ts b/apps/sim/tools/asana/get_project.ts new file mode 100644 index 00000000000..661aeda6df9 --- /dev/null +++ b/apps/sim/tools/asana/get_project.ts @@ -0,0 +1,74 @@ +import type { AsanaGetProjectParams, AsanaProjectRecordResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaGetProjectTool: ToolConfig = { + id: 'asana_get_project', + name: 'Asana Get Project', + description: 'Retrieve a single Asana project by its GID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + projectGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Asana project GID (numeric string) to retrieve', + }, + }, + + request: { + url: '/api/tools/asana/get-project', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + projectGid: params.projectGid, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), gid: '', name: '', notes: '' }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + gid: { type: 'string', description: 'Project globally unique identifier' }, + name: { type: 'string', description: 'Project name' }, + notes: { type: 'string', description: 'Project notes or description' }, + archived: { type: 'boolean', description: 'Whether the project is archived' }, + color: { type: 'string', description: 'Project color' }, + created_at: { type: 'string', description: 'Project creation timestamp' }, + modified_at: { type: 'string', description: 'Project last modified timestamp' }, + permalink_url: { type: 'string', description: 'URL to the project in Asana' }, + }, +} diff --git a/apps/sim/tools/asana/index.ts b/apps/sim/tools/asana/index.ts index 5099c422aa0..ae3dca1c1c2 100644 --- a/apps/sim/tools/asana/index.ts +++ b/apps/sim/tools/asana/index.ts @@ -1,7 +1,15 @@ import { asanaAddCommentTool } from '@/tools/asana/add_comment' +import { asanaAddFollowersTool } from '@/tools/asana/add_followers' +import { asanaCreateProjectTool } from '@/tools/asana/create_project' +import { asanaCreateSectionTool } from '@/tools/asana/create_section' +import { asanaCreateSubtaskTool } from '@/tools/asana/create_subtask' import { asanaCreateTaskTool } from '@/tools/asana/create_task' +import { asanaDeleteTaskTool } from '@/tools/asana/delete_task' +import { asanaGetProjectTool } from '@/tools/asana/get_project' import { asanaGetProjectsTool } from '@/tools/asana/get_projects' import { asanaGetTaskTool } from '@/tools/asana/get_task' +import { asanaListSectionsTool } from '@/tools/asana/list_sections' +import { asanaListWorkspacesTool } from '@/tools/asana/list_workspaces' import { asanaSearchTasksTool } from '@/tools/asana/search_tasks' import { asanaUpdateTaskTool } from '@/tools/asana/update_task' @@ -11,3 +19,11 @@ export { asanaUpdateTaskTool } export { asanaGetProjectsTool } export { asanaSearchTasksTool } export { asanaAddCommentTool } +export { asanaCreateProjectTool } +export { asanaGetProjectTool } +export { asanaListWorkspacesTool } +export { asanaCreateSubtaskTool } +export { asanaDeleteTaskTool } +export { asanaAddFollowersTool } +export { asanaCreateSectionTool } +export { asanaListSectionsTool } diff --git a/apps/sim/tools/asana/list_sections.ts b/apps/sim/tools/asana/list_sections.ts new file mode 100644 index 00000000000..a0fa47e2fe5 --- /dev/null +++ b/apps/sim/tools/asana/list_sections.ts @@ -0,0 +1,79 @@ +import type { AsanaListSectionsParams, AsanaListSectionsResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaListSectionsTool: ToolConfig = + { + id: 'asana_list_sections', + name: 'Asana List Sections', + description: 'List all sections in an Asana project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + projectGid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GID of the Asana project (numeric string) to list sections from', + }, + }, + + request: { + url: '/api/tools/asana/list-sections', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + projectGid: params.projectGid, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), sections: [] }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + sections: { + type: 'array', + description: 'Array of sections in the project', + items: { + type: 'object', + properties: { + gid: { type: 'string', description: 'Section GID' }, + name: { type: 'string', description: 'Section name' }, + resource_type: { type: 'string', description: 'Resource type (section)' }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/asana/list_workspaces.ts b/apps/sim/tools/asana/list_workspaces.ts new file mode 100644 index 00000000000..ff182f15063 --- /dev/null +++ b/apps/sim/tools/asana/list_workspaces.ts @@ -0,0 +1,74 @@ +import type { AsanaListWorkspacesParams, AsanaListWorkspacesResponse } from '@/tools/asana/types' +import type { ToolConfig } from '@/tools/types' + +export const asanaListWorkspacesTool: ToolConfig< + AsanaListWorkspacesParams, + AsanaListWorkspacesResponse +> = { + id: 'asana_list_workspaces', + name: 'Asana List Workspaces', + description: 'List all Asana workspaces and organizations the authenticated user belongs to', + version: '1.0.0', + + oauth: { + required: true, + provider: 'asana', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Asana', + }, + }, + + request: { + url: '/api/tools/asana/list-workspaces', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + }), + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + + if (!responseText) { + return { + success: false, + output: { ts: new Date().toISOString(), workspaces: [] }, + error: 'Empty response from Asana', + } + } + + const data = JSON.parse(responseText) + const { success, error, ...output } = data + return { + success: success ?? true, + output, + error, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + ts: { type: 'string', description: 'Timestamp of the response' }, + workspaces: { + type: 'array', + description: 'Array of workspaces', + items: { + type: 'object', + properties: { + gid: { type: 'string', description: 'Workspace GID' }, + name: { type: 'string', description: 'Workspace name' }, + resource_type: { type: 'string', description: 'Resource type (workspace)' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/asana/types.ts b/apps/sim/tools/asana/types.ts index c728b86bd1c..dfa688c7570 100644 --- a/apps/sim/tools/asana/types.ts +++ b/apps/sim/tools/asana/types.ts @@ -201,6 +201,118 @@ export interface AsanaAddCommentResponse extends ToolResponse { } } +export interface AsanaCreateProjectParams { + accessToken: string + workspace: string + name: string + notes?: string +} + +export interface AsanaProjectRecordResponse extends ToolResponse { + output: { + ts: string + gid: string + name: string + notes: string + archived?: boolean + color?: string | null + created_at?: string + modified_at?: string + permalink_url?: string + } +} + +export interface AsanaGetProjectParams { + accessToken: string + projectGid: string +} + +export interface AsanaListWorkspacesParams { + accessToken: string +} + +export interface AsanaListWorkspacesResponse extends ToolResponse { + output: { + ts: string + workspaces: Array<{ + gid: string + name: string + resource_type?: string + }> + } +} + +export interface AsanaCreateSubtaskParams { + accessToken: string + taskGid: string + name: string + notes?: string + assignee?: string + due_on?: string +} + +export interface AsanaDeleteTaskParams { + accessToken: string + taskGid: string +} + +export interface AsanaDeleteTaskResponse extends ToolResponse { + output: { + ts: string + gid: string + deleted: true + } +} + +export interface AsanaAddFollowersParams { + accessToken: string + taskGid: string + followers: string[] +} + +export interface AsanaAddFollowersResponse extends ToolResponse { + output: { + ts: string + gid: string + name: string + followers: Array<{ + gid: string + name: string + }> + } +} + +export interface AsanaCreateSectionParams { + accessToken: string + projectGid: string + name: string +} + +export interface AsanaSectionResponse extends ToolResponse { + output: { + ts: string + gid: string + name: string + created_at?: string + } +} + +export interface AsanaListSectionsParams { + accessToken: string + projectGid: string +} + +export interface AsanaListSectionsResponse extends ToolResponse { + output: { + ts: string + sections: Array<{ + gid: string + name: string + resource_type?: string + }> + } +} + export type AsanaResponse = | AsanaGetTaskResponse | AsanaCreateTaskResponse @@ -208,3 +320,9 @@ export type AsanaResponse = | AsanaGetProjectsResponse | AsanaSearchTasksResponse | AsanaAddCommentResponse + | AsanaProjectRecordResponse + | AsanaListWorkspacesResponse + | AsanaDeleteTaskResponse + | AsanaAddFollowersResponse + | AsanaSectionResponse + | AsanaListSectionsResponse diff --git a/apps/sim/tools/google_docs/create-named-range.ts b/apps/sim/tools/google_docs/create-named-range.ts new file mode 100644 index 00000000000..566520ada3a --- /dev/null +++ b/apps/sim/tools/google_docs/create-named-range.ts @@ -0,0 +1,123 @@ +import type { + GoogleDocsCreateNamedRangeResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildContentRange, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const createNamedRangeTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsCreateNamedRangeResponse +> = { + id: 'google_docs_create_named_range', + name: 'Create Named Range in Google Docs Document', + description: + 'Create a named range over a span of content in a Google Docs document so it can be referenced or deleted later. The name may be 1-256 characters and need not be unique.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to update', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the range to create (1-256 characters)', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The 1-based start character index of the range (inclusive)', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end character index of the range (exclusive)', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const name = params.name ? String(params.name).trim() : '' + if (!name) { + throw new Error('name is required') + } + if (name.length > 256) { + throw new Error('name must be 256 characters or fewer') + } + const range = buildContentRange(params.startIndex, params.endIndex) + return { + requests: [ + { + createNamedRange: { name, range }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + const namedRangeId = data.replies?.[0]?.createNamedRange?.namedRangeId ?? null + + return { + success: true, + output: { + namedRangeId, + metadata, + }, + } + }, + + outputs: { + namedRangeId: { + type: 'string', + description: 'The ID of the created named range', + optional: true, + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/create-paragraph-bullets.ts b/apps/sim/tools/google_docs/create-paragraph-bullets.ts new file mode 100644 index 00000000000..6d42cc8d76c --- /dev/null +++ b/apps/sim/tools/google_docs/create-paragraph-bullets.ts @@ -0,0 +1,143 @@ +import type { + GoogleDocsCreateParagraphBulletsResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildContentRange, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +const BULLET_PRESETS = new Set([ + 'BULLET_DISC_CIRCLE_SQUARE', + 'BULLET_DIAMONDX_ARROW3D_SQUARE', + 'BULLET_CHECKBOX', + 'BULLET_ARROW_DIAMOND_DISC', + 'BULLET_STAR_CIRCLE_SQUARE', + 'BULLET_ARROW3D_CIRCLE_SQUARE', + 'NUMBERED_DECIMAL_ALPHA_ROMAN', + 'NUMBERED_DECIMAL_ALPHA_ROMAN_PARENS', + 'NUMBERED_DECIMAL_NESTED', + 'NUMBERED_UPPERALPHA_ALPHA_ROMAN', + 'NUMBERED_UPPERROMAN_UPPERALPHA_DECIMAL', + 'NUMBERED_ZERODECIMAL_ALPHA_ROMAN', +]) + +const DEFAULT_BULLET_PRESET = 'BULLET_DISC_CIRCLE_SQUARE' + +export const createParagraphBulletsTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsCreateParagraphBulletsResponse +> = { + id: 'google_docs_create_paragraph_bullets', + name: 'Create Paragraph Bullets in Google Docs Document', + description: + 'Add bulleted or numbered list formatting to the paragraphs overlapping a range of text in a Google Docs document, using a chosen bullet glyph preset.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to update', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The 1-based start character index of the range to bullet (inclusive)', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end character index of the range to bullet (exclusive)', + }, + bulletPreset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The bullet glyph preset to apply. Defaults to BULLET_DISC_CIRCLE_SQUARE. Examples: BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED.', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const range = buildContentRange(params.startIndex, params.endIndex) + + let bulletPreset = DEFAULT_BULLET_PRESET + if (params.bulletPreset != null && String(params.bulletPreset).trim() !== '') { + bulletPreset = String(params.bulletPreset).trim().toUpperCase() + if (!BULLET_PRESETS.has(bulletPreset)) { + throw new Error( + 'bulletPreset must be a valid BulletGlyphPreset (e.g. BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN)' + ) + } + } + + return { + requests: [ + { + createParagraphBullets: { range, bulletPreset }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + + outputs: { + updatedContent: { + type: 'boolean', + description: 'Indicates if the bullets were applied successfully', + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/delete-content-range.ts b/apps/sim/tools/google_docs/delete-content-range.ts new file mode 100644 index 00000000000..03dd0e58659 --- /dev/null +++ b/apps/sim/tools/google_docs/delete-content-range.ts @@ -0,0 +1,108 @@ +import type { + GoogleDocsDeleteContentRangeResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildContentRange, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteContentRangeTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsDeleteContentRangeResponse +> = { + id: 'google_docs_delete_content_range', + name: 'Delete Content Range in Google Docs Document', + description: + 'Delete all content between a start and end character index in a Google Docs document. The endIndex is exclusive and must be greater than the startIndex.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to delete content from', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The 1-based start character index of the range to delete (inclusive)', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end character index of the range to delete (exclusive)', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const range = buildContentRange(params.startIndex, params.endIndex) + return { + requests: [ + { + deleteContentRange: { range }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + + outputs: { + updatedContent: { + type: 'boolean', + description: 'Indicates if the content range was deleted successfully', + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/delete-named-range.ts b/apps/sim/tools/google_docs/delete-named-range.ts new file mode 100644 index 00000000000..3a2ce788035 --- /dev/null +++ b/apps/sim/tools/google_docs/delete-named-range.ts @@ -0,0 +1,110 @@ +import type { + GoogleDocsDeleteNamedRangeResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { buildBatchUpdateMetadata, resolveDocumentId } from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteNamedRangeTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsDeleteNamedRangeResponse +> = { + id: 'google_docs_delete_named_range', + name: 'Delete Named Range in Google Docs Document', + description: + 'Delete one or more named ranges from a Google Docs document by their ID or by name. Provide exactly one of namedRangeId or name; deleting by name removes all ranges sharing that name. The content itself is not removed.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to update', + }, + namedRangeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the named range to delete. Provide exactly one of namedRangeId or namedRangeName.', + }, + namedRangeName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the named range(s) to delete. All ranges sharing this name are removed. Provide exactly one of namedRangeId or namedRangeName.', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const namedRangeId = params.namedRangeId ? String(params.namedRangeId).trim() : '' + const name = params.namedRangeName ? String(params.namedRangeName).trim() : '' + if (!namedRangeId && !name) { + throw new Error('Either namedRangeId or namedRangeName is required') + } + if (namedRangeId && name) { + throw new Error('Provide exactly one of namedRangeId or namedRangeName, not both') + } + const deleteNamedRange = namedRangeId ? { namedRangeId } : { name } + return { + requests: [{ deleteNamedRange }], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + + outputs: { + updatedContent: { + type: 'boolean', + description: 'Indicates if the named range(s) were deleted successfully', + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/delete-paragraph-bullets.ts b/apps/sim/tools/google_docs/delete-paragraph-bullets.ts new file mode 100644 index 00000000000..2085aabb5a3 --- /dev/null +++ b/apps/sim/tools/google_docs/delete-paragraph-bullets.ts @@ -0,0 +1,109 @@ +import type { + GoogleDocsDeleteParagraphBulletsResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildContentRange, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteParagraphBulletsTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsDeleteParagraphBulletsResponse +> = { + id: 'google_docs_delete_paragraph_bullets', + name: 'Delete Paragraph Bullets in Google Docs Document', + description: + 'Remove bullet or numbered list formatting from the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to update', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The 1-based start character index of the range to clear bullets from (inclusive)', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end character index of the range to clear bullets from (exclusive)', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const range = buildContentRange(params.startIndex, params.endIndex) + return { + requests: [ + { + deleteParagraphBullets: { range }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + + outputs: { + updatedContent: { + type: 'boolean', + description: 'Indicates if the bullets were removed successfully', + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/index.ts b/apps/sim/tools/google_docs/index.ts index f69d6de914a..33a4cf1bc11 100644 --- a/apps/sim/tools/google_docs/index.ts +++ b/apps/sim/tools/google_docs/index.ts @@ -1,10 +1,16 @@ import { createTool } from '@/tools/google_docs/create' +import { createNamedRangeTool } from '@/tools/google_docs/create-named-range' +import { createParagraphBulletsTool } from '@/tools/google_docs/create-paragraph-bullets' +import { deleteContentRangeTool } from '@/tools/google_docs/delete-content-range' +import { deleteNamedRangeTool } from '@/tools/google_docs/delete-named-range' +import { deleteParagraphBulletsTool } from '@/tools/google_docs/delete-paragraph-bullets' import { insertImageTool } from '@/tools/google_docs/insert-image' import { insertPageBreakTool } from '@/tools/google_docs/insert-page-break' import { insertTableTool } from '@/tools/google_docs/insert-table' import { insertTextTool } from '@/tools/google_docs/insert-text' import { readTool } from '@/tools/google_docs/read' import { replaceTextTool } from '@/tools/google_docs/replace-text' +import { updateParagraphStyleTool } from '@/tools/google_docs/update-paragraph-style' import { updateTextStyleTool } from '@/tools/google_docs/update-text-style' import { writeTool } from '@/tools/google_docs/write' @@ -17,5 +23,11 @@ export const googleDocsInsertTableTool = insertTableTool export const googleDocsInsertImageTool = insertImageTool export const googleDocsInsertPageBreakTool = insertPageBreakTool export const googleDocsUpdateTextStyleTool = updateTextStyleTool +export const googleDocsDeleteContentRangeTool = deleteContentRangeTool +export const googleDocsUpdateParagraphStyleTool = updateParagraphStyleTool +export const googleDocsCreateParagraphBulletsTool = createParagraphBulletsTool +export const googleDocsDeleteParagraphBulletsTool = deleteParagraphBulletsTool +export const googleDocsCreateNamedRangeTool = createNamedRangeTool +export const googleDocsDeleteNamedRangeTool = deleteNamedRangeTool export * from './types' diff --git a/apps/sim/tools/google_docs/types.ts b/apps/sim/tools/google_docs/types.ts index aae94a98d41..112ad681dd6 100644 --- a/apps/sim/tools/google_docs/types.ts +++ b/apps/sim/tools/google_docs/types.ts @@ -71,6 +71,48 @@ export interface GoogleDocsUpdateTextStyleResponse extends ToolResponse { } } +export interface GoogleDocsDeleteContentRangeResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsUpdateParagraphStyleResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsCreateParagraphBulletsResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsDeleteParagraphBulletsResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsCreateNamedRangeResponse extends ToolResponse { + output: { + namedRangeId: string | null + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsDeleteNamedRangeResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + export interface GoogleDocsToolParams { accessToken: string documentId?: string @@ -96,6 +138,12 @@ export interface GoogleDocsToolParams { italic?: boolean underline?: boolean fontSize?: number + namedStyleType?: string + alignment?: string + bulletPreset?: string + name?: string + namedRangeId?: string + namedRangeName?: string } export type GoogleDocsResponse = @@ -108,3 +156,9 @@ export type GoogleDocsResponse = | GoogleDocsInsertImageResponse | GoogleDocsInsertPageBreakResponse | GoogleDocsUpdateTextStyleResponse + | GoogleDocsDeleteContentRangeResponse + | GoogleDocsUpdateParagraphStyleResponse + | GoogleDocsCreateParagraphBulletsResponse + | GoogleDocsDeleteParagraphBulletsResponse + | GoogleDocsCreateNamedRangeResponse + | GoogleDocsDeleteNamedRangeResponse diff --git a/apps/sim/tools/google_docs/update-paragraph-style.ts b/apps/sim/tools/google_docs/update-paragraph-style.ts new file mode 100644 index 00000000000..6b9a88559d8 --- /dev/null +++ b/apps/sim/tools/google_docs/update-paragraph-style.ts @@ -0,0 +1,167 @@ +import type { + GoogleDocsToolParams, + GoogleDocsUpdateParagraphStyleResponse, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildContentRange, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +const NAMED_STYLE_TYPES = new Set([ + 'NORMAL_TEXT', + 'TITLE', + 'SUBTITLE', + 'HEADING_1', + 'HEADING_2', + 'HEADING_3', + 'HEADING_4', + 'HEADING_5', + 'HEADING_6', +]) + +const ALIGNMENTS = new Set(['LEFT', 'CENTER', 'RIGHT', 'JUSTIFY']) + +export const updateParagraphStyleTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsUpdateParagraphStyleResponse +> = { + id: 'google_docs_update_paragraph_style', + name: 'Update Paragraph Style in Google Docs Document', + description: + 'Apply a named paragraph style (such as a heading or title) and/or alignment to the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index.', + version: '1.0', + oauth: { + required: true, + provider: 'google-docs', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Docs API', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document to update', + }, + startIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The 1-based start character index of the range to style (inclusive)', + }, + endIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The end character index of the range to style (exclusive)', + }, + namedStyleType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The named paragraph style to apply. One of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6.', + }, + alignment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The paragraph alignment to apply. One of: LEFT, CENTER, RIGHT, JUSTIFY.', + }, + }, + request: { + url: (params) => { + const documentId = resolveDocumentId(params) + return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const range = buildContentRange(params.startIndex, params.endIndex) + + const paragraphStyle: Record = {} + const fields: string[] = [] + + if (params.namedStyleType != null && String(params.namedStyleType).trim() !== '') { + const namedStyleType = String(params.namedStyleType).trim().toUpperCase() + if (!NAMED_STYLE_TYPES.has(namedStyleType)) { + throw new Error( + 'namedStyleType must be one of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6' + ) + } + paragraphStyle.namedStyleType = namedStyleType + fields.push('namedStyleType') + } + + if (params.alignment != null && String(params.alignment).trim() !== '') { + const alignment = String(params.alignment).trim().toUpperCase() + if (!ALIGNMENTS.has(alignment)) { + throw new Error('alignment must be one of: LEFT, CENTER, RIGHT, JUSTIFY') + } + paragraphStyle.alignment = alignment + fields.push('alignment') + } + + if (fields.length === 0) { + throw new Error('At least one of namedStyleType or alignment must be provided') + } + + return { + requests: [ + { + updateParagraphStyle: { + range, + paragraphStyle, + fields: fields.join(','), + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, + + outputs: { + updatedContent: { + type: 'boolean', + description: 'Indicates if the paragraph style was applied successfully', + }, + metadata: { + type: 'json', + description: 'Updated document metadata including ID, title, and URL', + properties: { + documentId: { type: 'string', description: 'Google Docs document ID' }, + title: { type: 'string', description: 'Document title' }, + mimeType: { type: 'string', description: 'Document MIME type' }, + url: { type: 'string', description: 'Document URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/utils.ts b/apps/sim/tools/google_docs/utils.ts index f4d040c2f35..5be3c11799a 100644 --- a/apps/sim/tools/google_docs/utils.ts +++ b/apps/sim/tools/google_docs/utils.ts @@ -45,6 +45,27 @@ export function buildInsertLocation( return { endOfSegmentLocation: {} } } +/** + * Build and validate a Docs API `Range` from a start and end character index. + * The Docs API requires `endIndex` to be strictly greater than `startIndex`. + * Throws when either index is missing or the range is empty/inverted so callers + * fail loudly before issuing a request. + */ +export function buildContentRange( + startIndex: unknown, + endIndex: unknown +): { startIndex: number; endIndex: number } { + const start = Number(startIndex) + const end = Number(endIndex) + if (!Number.isFinite(start) || !Number.isFinite(end)) { + throw new Error('startIndex and endIndex are required') + } + if (end <= start) { + throw new Error('endIndex must be greater than startIndex') + } + return { startIndex: start, endIndex: end } +} + /** * Build canonical Google Docs metadata from a batchUpdate response. The * `documentId` is taken from the response body when present, otherwise parsed diff --git a/apps/sim/tools/jira/get_fields.ts b/apps/sim/tools/jira/get_fields.ts new file mode 100644 index 00000000000..6a15b0a3c2c --- /dev/null +++ b/apps/sim/tools/jira/get_fields.ts @@ -0,0 +1,153 @@ +import type { JiraGetFieldsParams, JiraGetFieldsResponse } from '@/tools/jira/types' +import { TIMESTAMP_OUTPUT } from '@/tools/jira/types' +import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +function buildFieldsUrl(cloudId: string): string { + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/field` +} + +export const jiraGetFieldsTool: ToolConfig = { + id: 'jira_get_fields', + name: 'Jira Get Fields', + description: + 'Get all system and custom fields defined in the Jira instance. Useful for discovering custom field IDs (e.g., customfield_10001) to use when writing or updating issues.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetFieldsParams) => { + if (params.cloudId) { + return buildFieldsUrl(params.cloudId) + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetFieldsParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: JiraGetFieldsParams) => { + const fetchFields = async (cloudId: string) => { + const fieldsResponse = await fetch(buildFieldsUrl(cloudId), { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!fieldsResponse.ok) { + const errorText = await fieldsResponse.text() + throw new Error( + parseAtlassianErrorMessage(fieldsResponse.status, fieldsResponse.statusText, errorText) + ) + } + + return fieldsResponse.json() + } + + let data: any + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + data = await fetchFields(cloudId) + } else { + if (!response.ok) { + const errorText = await response.text() + throw new Error(parseAtlassianErrorMessage(response.status, response.statusText, errorText)) + } + data = await response.json() + } + + const fields = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + ts: new Date().toISOString(), + fields: fields.map((f: any) => ({ + id: f?.id ?? '', + key: f?.key ?? null, + name: f?.name ?? '', + custom: f?.custom ?? null, + navigable: f?.navigable ?? null, + searchable: f?.searchable ?? null, + schemaType: f?.schema?.type ?? null, + customType: f?.schema?.custom ?? null, + })), + total: fields.length, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + fields: { + type: 'array', + description: 'Array of Jira fields (system and custom)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Field ID (e.g., summary, customfield_10001)' }, + key: { type: 'string', description: 'Field key', optional: true }, + name: { type: 'string', description: 'Human-readable field name' }, + custom: { + type: 'boolean', + description: 'Whether this is a custom field', + optional: true, + }, + navigable: { + type: 'boolean', + description: 'Whether the field is navigable in issue views', + optional: true, + }, + searchable: { + type: 'boolean', + description: 'Whether the field can be used in JQL searches', + optional: true, + }, + schemaType: { + type: 'string', + description: 'Field value type (e.g., string, number, array, user)', + optional: true, + }, + customType: { + type: 'string', + description: 'Custom field type identifier (only for custom fields)', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Number of fields returned' }, + }, +} diff --git a/apps/sim/tools/jira/get_project.ts b/apps/sim/tools/jira/get_project.ts new file mode 100644 index 00000000000..6e4db960d15 --- /dev/null +++ b/apps/sim/tools/jira/get_project.ts @@ -0,0 +1,173 @@ +import type { JiraGetProjectParams, JiraGetProjectResponse } from '@/tools/jira/types' +import { TIMESTAMP_OUTPUT } from '@/tools/jira/types' +import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +function buildProjectUrl(cloudId: string, projectIdOrKey: string): string { + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/${encodeURIComponent(projectIdOrKey)}` +} + +export const jiraGetProjectTool: ToolConfig = { + id: 'jira_get_project', + name: 'Jira Get Project', + description: + 'Get the details of a single Jira project by its ID or key, including its type, lead, components, issue types, and versions.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The project ID or key (e.g., "PROJ" or "10000")', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetProjectParams) => { + if (params.cloudId) { + return buildProjectUrl(params.cloudId, params.projectId) + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetProjectParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: JiraGetProjectParams) => { + const fetchProject = async (cloudId: string) => { + const projectResponse = await fetch(buildProjectUrl(cloudId, params!.projectId), { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!projectResponse.ok) { + const errorText = await projectResponse.text() + throw new Error( + parseAtlassianErrorMessage(projectResponse.status, projectResponse.statusText, errorText) + ) + } + + return projectResponse.json() + } + + let data: any + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + data = await fetchProject(cloudId) + } else { + if (!response.ok) { + const errorText = await response.text() + throw new Error(parseAtlassianErrorMessage(response.status, response.statusText, errorText)) + } + data = await response.json() + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + id: data?.id ?? '', + key: data?.key ?? '', + name: data?.name ?? '', + description: data?.description ?? null, + projectTypeKey: data?.projectTypeKey ?? null, + simplified: data?.simplified ?? null, + style: data?.style ?? null, + isPrivate: data?.isPrivate ?? null, + url: data?.self ?? null, + leadDisplayName: data?.lead?.displayName ?? null, + leadAccountId: data?.lead?.accountId ?? null, + issueTypes: Array.isArray(data?.issueTypes) + ? data.issueTypes.map((t: any) => ({ + id: t?.id ?? '', + name: t?.name ?? '', + subtask: t?.subtask ?? null, + })) + : [], + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Project ID' }, + key: { type: 'string', description: 'Project key (e.g., PROJ)' }, + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description', optional: true }, + projectTypeKey: { + type: 'string', + description: 'Project type key (e.g., software, service_desk, business)', + optional: true, + }, + simplified: { + type: 'boolean', + description: 'Whether the project is a simplified (team-managed) project', + optional: true, + }, + style: { + type: 'string', + description: 'Project style (e.g., classic, next-gen)', + optional: true, + }, + isPrivate: { type: 'boolean', description: 'Whether the project is private', optional: true }, + url: { type: 'string', description: 'REST API URL for this project', optional: true }, + leadDisplayName: { + type: 'string', + description: 'Display name of the project lead', + optional: true, + }, + leadAccountId: { + type: 'string', + description: 'Account ID of the project lead', + optional: true, + }, + issueTypes: { + type: 'array', + description: 'Issue types available in this project', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Issue type ID' }, + name: { type: 'string', description: 'Issue type name (e.g., Task, Bug, Story)' }, + subtask: { + type: 'boolean', + description: 'Whether this issue type is a subtask', + optional: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/jira/get_transitions.ts b/apps/sim/tools/jira/get_transitions.ts new file mode 100644 index 00000000000..94f92df44dc --- /dev/null +++ b/apps/sim/tools/jira/get_transitions.ts @@ -0,0 +1,166 @@ +import type { JiraGetTransitionsParams, JiraGetTransitionsResponse } from '@/tools/jira/types' +import { TIMESTAMP_OUTPUT } from '@/tools/jira/types' +import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +function buildTransitionsUrl(cloudId: string, issueKey: string): string { + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions` +} + +export const jiraGetTransitionsTool: ToolConfig< + JiraGetTransitionsParams, + JiraGetTransitionsResponse +> = { + id: 'jira_get_transitions', + name: 'Jira Get Transitions', + description: + 'Get the workflow transitions available for an issue in its current status. Use the returned transition IDs with the Transition Issue operation.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The issue key or ID (e.g., PROJ-123)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetTransitionsParams) => { + if (params.cloudId) { + return buildTransitionsUrl(params.cloudId, params.issueKey) + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetTransitionsParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: JiraGetTransitionsParams) => { + const fetchTransitions = async (cloudId: string) => { + const transitionsResponse = await fetch(buildTransitionsUrl(cloudId, params!.issueKey), { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!transitionsResponse.ok) { + const errorText = await transitionsResponse.text() + throw new Error( + parseAtlassianErrorMessage( + transitionsResponse.status, + transitionsResponse.statusText, + errorText + ) + ) + } + + return transitionsResponse.json() + } + + let data: any + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + data = await fetchTransitions(cloudId) + } else { + if (!response.ok) { + const errorText = await response.text() + throw new Error(parseAtlassianErrorMessage(response.status, response.statusText, errorText)) + } + data = await response.json() + } + + const transitions = Array.isArray(data?.transitions) ? data.transitions : [] + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey ?? '', + transitions: transitions.map((t: any) => ({ + id: t?.id ?? '', + name: t?.name ?? '', + toStatusId: t?.to?.id ?? null, + toStatusName: t?.to?.name ?? null, + toStatusCategory: t?.to?.statusCategory?.key ?? null, + isAvailable: t?.isAvailable ?? null, + hasScreen: t?.hasScreen ?? null, + })), + total: transitions.length, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + issueKey: { type: 'string', description: 'Issue key the transitions belong to' }, + transitions: { + type: 'array', + description: 'Available workflow transitions for the issue', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Transition ID (use with Transition Issue)' }, + name: { type: 'string', description: 'Transition name (e.g., "Start Progress")' }, + toStatusId: { + type: 'string', + description: 'ID of the status the issue moves to', + optional: true, + }, + toStatusName: { + type: 'string', + description: 'Name of the status the issue moves to', + optional: true, + }, + toStatusCategory: { + type: 'string', + description: 'Status category key of the target status (new, indeterminate, done)', + optional: true, + }, + isAvailable: { + type: 'boolean', + description: 'Whether the transition can currently be performed', + optional: true, + }, + hasScreen: { + type: 'boolean', + description: 'Whether the transition requires a screen with fields', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Number of available transitions' }, + }, +} diff --git a/apps/sim/tools/jira/index.ts b/apps/sim/tools/jira/index.ts index 877fdb8b51d..07f0bfe2b9d 100644 --- a/apps/sim/tools/jira/index.ts +++ b/apps/sim/tools/jira/index.ts @@ -12,8 +12,13 @@ import { jiraDeleteIssueLinkTool } from '@/tools/jira/delete_issue_link' import { jiraDeleteWorklogTool } from '@/tools/jira/delete_worklog' import { jiraGetAttachmentsTool } from '@/tools/jira/get_attachments' import { jiraGetCommentsTool } from '@/tools/jira/get_comments' +import { jiraGetFieldsTool } from '@/tools/jira/get_fields' +import { jiraGetProjectTool } from '@/tools/jira/get_project' +import { jiraGetTransitionsTool } from '@/tools/jira/get_transitions' import { jiraGetUsersTool } from '@/tools/jira/get_users' import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs' +import { jiraListIssueTypesTool } from '@/tools/jira/list_issue_types' +import { jiraListProjectsTool } from '@/tools/jira/list_projects' import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher' import { jiraRetrieveTool } from '@/tools/jira/retrieve' import { jiraSearchIssuesTool } from '@/tools/jira/search_issues' @@ -50,4 +55,9 @@ export { jiraRemoveWatcherTool, jiraGetUsersTool, jiraSearchUsersTool, + jiraListProjectsTool, + jiraGetProjectTool, + jiraGetTransitionsTool, + jiraListIssueTypesTool, + jiraGetFieldsTool, } diff --git a/apps/sim/tools/jira/list_issue_types.ts b/apps/sim/tools/jira/list_issue_types.ts new file mode 100644 index 00000000000..9cf4ed07071 --- /dev/null +++ b/apps/sim/tools/jira/list_issue_types.ts @@ -0,0 +1,150 @@ +import type { JiraListIssueTypesParams, JiraListIssueTypesResponse } from '@/tools/jira/types' +import { TIMESTAMP_OUTPUT } from '@/tools/jira/types' +import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +function buildIssueTypesUrl(cloudId: string): string { + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issuetype` +} + +export const jiraListIssueTypesTool: ToolConfig< + JiraListIssueTypesParams, + JiraListIssueTypesResponse +> = { + id: 'jira_list_issue_types', + name: 'Jira List Issue Types', + description: + 'List all issue types visible to the user across projects (e.g., Task, Bug, Story, Epic, Subtask). Useful for discovering valid issue types before creating an issue.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraListIssueTypesParams) => { + if (params.cloudId) { + return buildIssueTypesUrl(params.cloudId) + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraListIssueTypesParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: JiraListIssueTypesParams) => { + const fetchIssueTypes = async (cloudId: string) => { + const issueTypesResponse = await fetch(buildIssueTypesUrl(cloudId), { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!issueTypesResponse.ok) { + const errorText = await issueTypesResponse.text() + throw new Error( + parseAtlassianErrorMessage( + issueTypesResponse.status, + issueTypesResponse.statusText, + errorText + ) + ) + } + + return issueTypesResponse.json() + } + + let data: any + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + data = await fetchIssueTypes(cloudId) + } else { + if (!response.ok) { + const errorText = await response.text() + throw new Error(parseAtlassianErrorMessage(response.status, response.statusText, errorText)) + } + data = await response.json() + } + + const issueTypes = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueTypes: issueTypes.map((t: any) => ({ + id: t?.id ?? '', + name: t?.name ?? '', + description: t?.description ?? null, + subtask: t?.subtask ?? null, + hierarchyLevel: typeof t?.hierarchyLevel === 'number' ? t.hierarchyLevel : null, + iconUrl: t?.iconUrl ?? null, + scope: t?.scope?.project?.id ?? null, + })), + total: issueTypes.length, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + issueTypes: { + type: 'array', + description: 'Array of issue types', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Issue type ID' }, + name: { type: 'string', description: 'Issue type name (e.g., Task, Bug, Story)' }, + description: { type: 'string', description: 'Issue type description', optional: true }, + subtask: { + type: 'boolean', + description: 'Whether this issue type is a subtask', + optional: true, + }, + hierarchyLevel: { + type: 'number', + description: 'Hierarchy level (0 = standard, 1 = epic, -1 = subtask)', + optional: true, + }, + iconUrl: { type: 'string', description: 'URL of the issue type icon', optional: true }, + scope: { + type: 'string', + description: 'Project ID if this issue type is scoped to a team-managed project', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Number of issue types returned' }, + }, +} diff --git a/apps/sim/tools/jira/list_projects.ts b/apps/sim/tools/jira/list_projects.ts new file mode 100644 index 00000000000..75f42366110 --- /dev/null +++ b/apps/sim/tools/jira/list_projects.ts @@ -0,0 +1,206 @@ +import type { JiraListProjectsParams, JiraListProjectsResponse } from '@/tools/jira/types' +import { TIMESTAMP_OUTPUT } from '@/tools/jira/types' +import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Transforms a raw Jira project object into typed output. + */ +function transformProject(project: any) { + return { + id: project.id ?? '', + key: project.key ?? '', + name: project.name ?? '', + projectTypeKey: project.projectTypeKey ?? null, + simplified: project.simplified ?? null, + style: project.style ?? null, + isPrivate: project.isPrivate ?? null, + url: project.self ?? null, + leadDisplayName: project.lead?.displayName ?? null, + leadAccountId: project.lead?.accountId ?? null, + } +} + +function buildSearchUrl(cloudId: string, params: JiraListProjectsParams): string { + const queryParams = new URLSearchParams() + if (params.query) queryParams.append('query', params.query) + if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt)) + if (params.maxResults !== undefined) queryParams.append('maxResults', String(params.maxResults)) + const queryString = queryParams.toString() + return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/search${queryString ? `?${queryString}` : ''}` +} + +export const jiraListProjectsTool: ToolConfig = { + id: 'jira_list_projects', + name: 'Jira List Projects', + description: + 'List Jira projects visible to the user, with optional name/key filtering and pagination. Returns each project with id, key, name, and type.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter projects by partial name or key match', + }, + startAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'The index of the first project to return (for pagination, default: 0)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of projects to return (default: 50, max: 100)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraListProjectsParams) => { + if (params.cloudId) { + return buildSearchUrl(params.cloudId, params) + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraListProjectsParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params?: JiraListProjectsParams) => { + const fetchProjects = async (cloudId: string) => { + const projectsResponse = await fetch(buildSearchUrl(cloudId, params!), { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!projectsResponse.ok) { + const errorText = await projectsResponse.text() + throw new Error( + parseAtlassianErrorMessage( + projectsResponse.status, + projectsResponse.statusText, + errorText + ) + ) + } + + return projectsResponse.json() + } + + let data: any + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + data = await fetchProjects(cloudId) + } else { + if (!response.ok) { + const errorText = await response.text() + throw new Error(parseAtlassianErrorMessage(response.status, response.statusText, errorText)) + } + data = await response.json() + } + + const values = Array.isArray(data?.values) ? data.values : [] + + return { + success: true, + output: { + ts: new Date().toISOString(), + projects: values.map(transformProject), + total: typeof data?.total === 'number' ? data.total : values.length, + startAt: typeof data?.startAt === 'number' ? data.startAt : (params?.startAt ?? 0), + maxResults: + typeof data?.maxResults === 'number' ? data.maxResults : (params?.maxResults ?? 50), + isLast: data?.isLast ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + projects: { + type: 'array', + description: 'Array of Jira projects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Project ID' }, + key: { type: 'string', description: 'Project key (e.g., PROJ)' }, + name: { type: 'string', description: 'Project name' }, + projectTypeKey: { + type: 'string', + description: 'Project type key (e.g., software, service_desk, business)', + optional: true, + }, + simplified: { + type: 'boolean', + description: 'Whether the project is a simplified (team-managed) project', + optional: true, + }, + style: { + type: 'string', + description: 'Project style (e.g., classic, next-gen)', + optional: true, + }, + isPrivate: { + type: 'boolean', + description: 'Whether the project is private', + optional: true, + }, + url: { type: 'string', description: 'REST API URL for this project', optional: true }, + leadDisplayName: { + type: 'string', + description: 'Display name of the project lead', + optional: true, + }, + leadAccountId: { + type: 'string', + description: 'Account ID of the project lead', + optional: true, + }, + }, + }, + }, + total: { type: 'number', description: 'Total number of matching projects' }, + startAt: { type: 'number', description: 'Pagination start index' }, + maxResults: { type: 'number', description: 'Maximum results per page' }, + isLast: { + type: 'boolean', + description: 'Whether this is the last page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/jira/types.ts b/apps/sim/tools/jira/types.ts index 0c2feee9b62..aec32e52270 100644 --- a/apps/sim/tools/jira/types.ts +++ b/apps/sim/tools/jira/types.ts @@ -1597,6 +1597,135 @@ export interface JiraGetUsersResponse extends ToolResponse { } } +export interface JiraListProjectsParams { + accessToken: string + domain: string + query?: string + startAt?: number + maxResults?: number + cloudId?: string +} + +export interface JiraListProjectsResponse extends ToolResponse { + output: { + ts: string + projects: Array<{ + id: string + key: string + name: string + projectTypeKey?: string | null + simplified?: boolean | null + style?: string | null + isPrivate?: boolean | null + url?: string | null + leadDisplayName?: string | null + leadAccountId?: string | null + }> + total: number + startAt: number + maxResults: number + isLast?: boolean | null + } +} + +export interface JiraGetProjectParams { + accessToken: string + domain: string + projectId: string + cloudId?: string +} + +export interface JiraGetProjectResponse extends ToolResponse { + output: { + ts: string + id: string + key: string + name: string + description?: string | null + projectTypeKey?: string | null + simplified?: boolean | null + style?: string | null + isPrivate?: boolean | null + url?: string | null + leadDisplayName?: string | null + leadAccountId?: string | null + issueTypes: Array<{ + id: string + name: string + subtask?: boolean | null + }> + } +} + +export interface JiraGetTransitionsParams { + accessToken: string + domain: string + issueKey: string + cloudId?: string +} + +export interface JiraGetTransitionsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + transitions: Array<{ + id: string + name: string + toStatusId?: string | null + toStatusName?: string | null + toStatusCategory?: string | null + isAvailable?: boolean | null + hasScreen?: boolean | null + }> + total: number + } +} + +export interface JiraListIssueTypesParams { + accessToken: string + domain: string + cloudId?: string +} + +export interface JiraListIssueTypesResponse extends ToolResponse { + output: { + ts: string + issueTypes: Array<{ + id: string + name: string + description?: string | null + subtask?: boolean | null + hierarchyLevel?: number | null + iconUrl?: string | null + scope?: string | null + }> + total: number + } +} + +export interface JiraGetFieldsParams { + accessToken: string + domain: string + cloudId?: string +} + +export interface JiraGetFieldsResponse extends ToolResponse { + output: { + ts: string + fields: Array<{ + id: string + key?: string | null + name: string + custom?: boolean | null + navigable?: boolean | null + searchable?: boolean | null + schemaType?: string | null + customType?: string | null + }> + total: number + } +} + export type JiraResponse = | JiraRetrieveResponse | JiraUpdateResponse @@ -1623,3 +1752,8 @@ export type JiraResponse = | JiraRemoveWatcherResponse | JiraGetUsersResponse | JiraSearchUsersResponse + | JiraListProjectsResponse + | JiraGetProjectResponse + | JiraGetTransitionsResponse + | JiraListIssueTypesResponse + | JiraGetFieldsResponse diff --git a/apps/sim/tools/monday/change_column_value.ts b/apps/sim/tools/monday/change_column_value.ts new file mode 100644 index 00000000000..3c5ab4ff0ff --- /dev/null +++ b/apps/sim/tools/monday/change_column_value.ts @@ -0,0 +1,160 @@ +import type { + MondayChangeColumnValueParams, + MondayChangeColumnValueResponse, +} from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayChangeColumnValueTool: ToolConfig< + MondayChangeColumnValueParams, + MondayChangeColumnValueResponse +> = { + id: 'monday_change_column_value', + name: 'Monday Change Column Value', + description: "Update a single column's value on 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', + }, + 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', + }, + columnId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the column to update (e.g., "status", "date4")', + }, + value: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The new column value as a JSON string (e.g., {"label":"Done"} for a status column)', + }, + createLabelsIfMissing: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Create status/dropdown labels that do not yet exist on the column', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_id: ${sanitizeNumericId(params.boardId, 'boardId')}`, + `item_id: ${sanitizeNumericId(params.itemId, 'itemId')}`, + `column_id: ${JSON.stringify(params.columnId)}`, + `value: ${JSON.stringify(params.value)}`, + ] + if (params.createLabelsIfMissing) { + args.push('create_labels_if_missing: true') + } + return { + query: `mutation { change_column_value(${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?.change_column_value + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to change column value' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The updated item', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/create_board.ts b/apps/sim/tools/monday/create_board.ts new file mode 100644 index 00000000000..f0f44f3d309 --- /dev/null +++ b/apps/sim/tools/monday/create_board.ts @@ -0,0 +1,134 @@ +import type { MondayCreateBoardParams, MondayCreateBoardResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeEnum, + sanitizeNumericId, +} from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +const BOARD_KINDS = ['public', 'private', 'share'] as const + +export const mondayCreateBoardTool: ToolConfig = + { + id: 'monday_create_board', + name: 'Monday Create Board', + description: 'Create a new board in 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', + }, + boardName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the new board', + }, + boardKind: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The board kind: public, private, or share', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The board description', + }, + workspaceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the workspace to create the board in', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the folder to create the board in', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_name: ${JSON.stringify(params.boardName)}`, + `board_kind: ${sanitizeEnum(params.boardKind, 'boardKind', BOARD_KINDS)}`, + ] + if (params.description) { + args.push(`description: ${JSON.stringify(params.description)}`) + } + if (params.workspaceId) { + args.push(`workspace_id: ${sanitizeNumericId(params.workspaceId, 'workspaceId')}`) + } + if (params.folderId) { + args.push(`folder_id: ${sanitizeNumericId(params.folderId, 'folderId')}`) + } + return { + query: `mutation { create_board(${args.join(', ')}) { 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: { board: null }, error } + } + + const raw = data.data?.create_board + if (!raw) { + return { success: false, output: { board: null }, error: 'Failed to create board' } + } + + return { + success: true, + output: { + board: { + id: raw.id as string, + name: (raw.name as string) ?? '', + description: (raw.description as string) ?? null, + state: (raw.state as string) ?? 'active', + boardKind: (raw.board_kind as string) ?? 'public', + itemsCount: (raw.items_count as number) ?? 0, + url: (raw.url as string) ?? '', + updatedAt: (raw.updated_at as string) ?? null, + }, + }, + } + }, + + outputs: { + board: { + type: 'json', + description: 'The created board', + 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 }, + }, + }, + }, + } diff --git a/apps/sim/tools/monday/create_column.ts b/apps/sim/tools/monday/create_column.ts new file mode 100644 index 00000000000..fdd360be6f6 --- /dev/null +++ b/apps/sim/tools/monday/create_column.ts @@ -0,0 +1,159 @@ +import type { MondayCreateColumnParams, MondayCreateColumnResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeEnum, + sanitizeNumericId, +} from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +const COLUMN_TYPES = [ + 'auto_number', + 'board_relation', + 'button', + 'checkbox', + 'color_picker', + 'country', + 'date', + 'dependency', + 'doc', + 'dropdown', + 'email', + 'file', + 'formula', + 'hour', + 'item_id', + 'link', + 'location', + 'long_text', + 'mirror', + 'name', + 'numbers', + 'people', + 'phone', + 'progress', + 'rating', + 'status', + 'tags', + 'team', + 'text', + 'timeline', + 'time_tracking', + 'vote', + 'week', + 'world_clock', +] as const + +export const mondayCreateColumnTool: ToolConfig< + MondayCreateColumnParams, + MondayCreateColumnResponse +> = { + id: 'monday_create_column', + name: 'Monday Create Column', + description: 'Create a new column 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 column on', + }, + columnTitle: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new column', + }, + columnType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The column type (e.g., status, text, numbers, date, people, dropdown)', + }, + columnDescription: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The column description', + }, + columnDefaults: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of default settings for the column (e.g., status labels)', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_id: ${sanitizeNumericId(params.boardId, 'boardId')}`, + `title: ${JSON.stringify(params.columnTitle)}`, + `column_type: ${sanitizeEnum(params.columnType, 'columnType', COLUMN_TYPES)}`, + ] + if (params.columnDescription) { + args.push(`description: ${JSON.stringify(params.columnDescription)}`) + } + if (params.columnDefaults) { + args.push(`defaults: ${JSON.stringify(params.columnDefaults)}`) + } + return { + query: `mutation { create_column(${args.join(', ')}) { id title type } }`, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { column: null }, error } + } + + const raw = data.data?.create_column + if (!raw) { + return { success: false, output: { column: null }, error: 'Failed to create column' } + } + + return { + success: true, + output: { + column: { + id: raw.id as string, + title: (raw.title as string) ?? '', + type: (raw.type as string) ?? '', + }, + }, + } + }, + + outputs: { + column: { + type: 'json', + description: 'The created column', + optional: true, + properties: { + id: { type: 'string', description: 'Column ID' }, + title: { type: 'string', description: 'Column title' }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/duplicate_item.ts b/apps/sim/tools/monday/duplicate_item.ts new file mode 100644 index 00000000000..7d2496ebd68 --- /dev/null +++ b/apps/sim/tools/monday/duplicate_item.ts @@ -0,0 +1,142 @@ +import type { MondayDuplicateItemParams, MondayDuplicateItemResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayDuplicateItemTool: ToolConfig< + MondayDuplicateItemParams, + MondayDuplicateItemResponse +> = { + id: 'monday_duplicate_item', + name: 'Monday Duplicate Item', + description: 'Duplicate an existing 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 duplicate', + }, + withUpdates: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to also duplicate the item updates', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => { + const args: string[] = [ + `board_id: ${sanitizeNumericId(params.boardId, 'boardId')}`, + `item_id: ${sanitizeNumericId(params.itemId, 'itemId')}`, + ] + if (params.withUpdates) { + args.push('with_updates: true') + } + return { + query: `mutation { duplicate_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?.duplicate_item + if (!raw) { + return { success: false, output: { item: null }, error: 'Failed to duplicate item' } + } + + const board = raw.board as Record | null + const group = raw.group as Record | null + const columnValues = ((raw.column_values as Record[]) ?? []).map( + (cv: Record) => ({ + id: cv.id as string, + text: (cv.text as string) ?? null, + value: (cv.value as string) ?? null, + type: (cv.type as string) ?? '', + }) + ) + + return { + success: true, + output: { + item: { + id: raw.id as string, + name: (raw.name as string) ?? '', + state: (raw.state as string) ?? null, + boardId: board ? (board.id as string) : null, + groupId: group ? (group.id as string) : null, + groupTitle: group ? ((group.title as string) ?? null) : null, + columnValues, + createdAt: (raw.created_at as string) ?? null, + updatedAt: (raw.updated_at as string) ?? null, + url: (raw.url as string) ?? null, + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The duplicated item', + optional: true, + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + state: { type: 'string', description: 'Item state', optional: true }, + boardId: { type: 'string', description: 'Board ID', optional: true }, + groupId: { type: 'string', description: 'Group ID', optional: true }, + groupTitle: { type: 'string', description: 'Group title', optional: true }, + columnValues: { + type: 'array', + description: 'Column values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Column ID' }, + text: { type: 'string', description: 'Text value', optional: true }, + value: { type: 'string', description: 'Raw JSON value', optional: true }, + type: { type: 'string', description: 'Column type' }, + }, + }, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last updated timestamp', optional: true }, + url: { type: 'string', description: 'Item URL', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/monday/get_groups.ts b/apps/sim/tools/monday/get_groups.ts new file mode 100644 index 00000000000..7764a076d67 --- /dev/null +++ b/apps/sim/tools/monday/get_groups.ts @@ -0,0 +1,94 @@ +import type { MondayGetGroupsParams, MondayGetGroupsResponse } from '@/tools/monday/types' +import { + extractMondayError, + MONDAY_API_URL, + mondayHeaders, + sanitizeNumericId, +} from '@/tools/monday/utils' +import type { ToolConfig } from '@/tools/types' + +export const mondayGetGroupsTool: ToolConfig = { + id: 'monday_get_groups', + name: 'Monday Get Groups', + description: 'Get the groups 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 retrieve groups from', + }, + }, + + request: { + url: MONDAY_API_URL, + method: 'POST', + headers: (params) => mondayHeaders(params.accessToken), + body: (params) => ({ + query: `query { boards(ids: [${sanitizeNumericId(params.boardId, 'boardId')}]) { groups { id title color archived deleted position } } }`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const error = extractMondayError(data) + if (error) { + return { success: false, output: { groups: [], count: 0 }, error } + } + + const boards = data.data?.boards ?? [] + if (boards.length === 0) { + return { success: false, output: { groups: [], count: 0 }, error: 'Board not found' } + } + + const groups = (boards[0].groups ?? []).map((g: Record) => ({ + id: g.id as string, + title: (g.title as string) ?? '', + color: (g.color as string) ?? '', + archived: (g.archived as boolean) ?? null, + deleted: (g.deleted as boolean) ?? null, + position: (g.position as string) ?? '', + })) + + return { + success: true, + output: { groups, count: groups.length }, + } + }, + + outputs: { + 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' }, + }, + }, + }, + count: { type: 'number', description: 'Number of returned groups' }, + }, +} diff --git a/apps/sim/tools/monday/index.ts b/apps/sim/tools/monday/index.ts index 417c7daa810..907a9183a9c 100644 --- a/apps/sim/tools/monday/index.ts +++ b/apps/sim/tools/monday/index.ts @@ -1,10 +1,15 @@ export { mondayArchiveItemTool } from '@/tools/monday/archive_item' +export { mondayChangeColumnValueTool } from '@/tools/monday/change_column_value' +export { mondayCreateBoardTool } from '@/tools/monday/create_board' +export { mondayCreateColumnTool } from '@/tools/monday/create_column' 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 { mondayDuplicateItemTool } from '@/tools/monday/duplicate_item' export { mondayGetBoardTool } from '@/tools/monday/get_board' +export { mondayGetGroupsTool } from '@/tools/monday/get_groups' export { mondayGetItemTool } from '@/tools/monday/get_item' export { mondayGetItemsTool } from '@/tools/monday/get_items' export { mondayListBoardsTool } from '@/tools/monday/list_boards' diff --git a/apps/sim/tools/monday/types.ts b/apps/sim/tools/monday/types.ts index dd4cafc4b98..12ecbc8fe00 100644 --- a/apps/sim/tools/monday/types.ts +++ b/apps/sim/tools/monday/types.ts @@ -220,3 +220,73 @@ export interface MondayCreateGroupResponse extends ToolResponse { group: MondayGroup | null } } + +export interface MondayChangeColumnValueParams { + accessToken: string + boardId: string + itemId: string + columnId: string + value: string + createLabelsIfMissing?: boolean +} + +export interface MondayChangeColumnValueResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} + +export interface MondayCreateBoardParams { + accessToken: string + boardName: string + boardKind: string + description?: string + workspaceId?: string + folderId?: string +} + +export interface MondayCreateBoardResponse extends ToolResponse { + output: { + board: MondayBoard | null + } +} + +export interface MondayCreateColumnParams { + accessToken: string + boardId: string + columnTitle: string + columnType: string + columnDescription?: string + columnDefaults?: string +} + +export interface MondayCreateColumnResponse extends ToolResponse { + output: { + column: MondayColumn | null + } +} + +export interface MondayGetGroupsParams { + accessToken: string + boardId: string +} + +export interface MondayGetGroupsResponse extends ToolResponse { + output: { + groups: MondayGroup[] + count: number + } +} + +export interface MondayDuplicateItemParams { + accessToken: string + boardId: string + itemId: string + withUpdates?: boolean +} + +export interface MondayDuplicateItemResponse extends ToolResponse { + output: { + item: MondayItem | null + } +} diff --git a/apps/sim/tools/monday/utils.ts b/apps/sim/tools/monday/utils.ts index 3722b01f2ec..f6b130ace96 100644 --- a/apps/sim/tools/monday/utils.ts +++ b/apps/sim/tools/monday/utils.ts @@ -31,6 +31,24 @@ export function sanitizeLimit(value: number | undefined, defaultVal: number, max return Math.min(n, max) } +/** + * Validates a GraphQL enum literal (e.g., board_kind, column_type) against an + * allowlist and returns the bare, unquoted value for safe inlining. GraphQL + * enums must NOT be JSON-stringified; this guards against query injection by + * rejecting anything outside the provided set. + */ +export function sanitizeEnum( + value: string | undefined, + paramName: string, + allowed: readonly string[] +): string { + const normalized = typeof value === 'string' ? value.trim() : '' + if (!allowed.includes(normalized)) { + throw new Error(`Invalid ${paramName}: "${value}". Expected one of: ${allowed.join(', ')}`) + } + return normalized +} + export function extractMondayError(data: Record): string | null { if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) { const messages = (data.errors as Array>) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index a7f50264ed3..2bb910c4eef 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -183,9 +183,17 @@ import { import { arxivGetAuthorPapersTool, arxivGetPaperTool, arxivSearchTool } from '@/tools/arxiv' import { asanaAddCommentTool, + asanaAddFollowersTool, + asanaCreateProjectTool, + asanaCreateSectionTool, + asanaCreateSubtaskTool, asanaCreateTaskTool, + asanaDeleteTaskTool, asanaGetProjectsTool, + asanaGetProjectTool, asanaGetTaskTool, + asanaListSectionsTool, + asanaListWorkspacesTool, asanaSearchTasksTool, asanaUpdateTaskTool, } from '@/tools/asana' @@ -1265,13 +1273,19 @@ import { googleContactsUpdateTool, } from '@/tools/google_contacts' import { + googleDocsCreateNamedRangeTool, + googleDocsCreateParagraphBulletsTool, googleDocsCreateTool, + googleDocsDeleteContentRangeTool, + googleDocsDeleteNamedRangeTool, + googleDocsDeleteParagraphBulletsTool, googleDocsInsertImageTool, googleDocsInsertPageBreakTool, googleDocsInsertTableTool, googleDocsInsertTextTool, googleDocsReadTool, googleDocsReplaceTextTool, + googleDocsUpdateParagraphStyleTool, googleDocsUpdateTextStyleTool, googleDocsWriteTool, } from '@/tools/google_docs' @@ -1760,8 +1774,13 @@ import { jiraDeleteWorklogTool, jiraGetAttachmentsTool, jiraGetCommentsTool, + jiraGetFieldsTool, + jiraGetProjectTool, + jiraGetTransitionsTool, jiraGetUsersTool, jiraGetWorklogsTool, + jiraListIssueTypesTool, + jiraListProjectsTool, jiraRemoveWatcherTool, jiraRetrieveTool, jiraSearchIssuesTool, @@ -2242,12 +2261,17 @@ import { import { mistralParserTool, mistralParserV2Tool, mistralParserV3Tool } from '@/tools/mistral' import { mondayArchiveItemTool, + mondayChangeColumnValueTool, + mondayCreateBoardTool, + mondayCreateColumnTool, mondayCreateGroupTool, mondayCreateItemTool, mondayCreateSubitemTool, mondayCreateUpdateTool, mondayDeleteItemTool, + mondayDuplicateItemTool, mondayGetBoardTool, + mondayGetGroupsTool, mondayGetItemsTool, mondayGetItemTool, mondayListBoardsTool, @@ -3180,11 +3204,13 @@ import { } from '@/tools/sixtyfour' import { slackAddReactionTool, + slackArchiveConversationTool, slackCanvasTool, slackCreateChannelCanvasTool, slackCreateConversationTool, slackDeleteCanvasTool, slackDeleteMessageTool, + slackDeleteScheduledMessageTool, slackDownloadTool, slackEditCanvasTool, slackEphemeralMessageTool, @@ -3201,6 +3227,7 @@ import { slackListCanvasesTool, slackListChannelsTool, slackListMembersTool, + slackListScheduledMessagesTool, slackListUsersTool, slackLookupCanvasSectionsTool, slackMessageReaderTool, @@ -3209,6 +3236,10 @@ import { slackPublishViewTool, slackPushViewTool, slackRemoveReactionTool, + slackRenameConversationTool, + slackScheduleMessageTool, + slackSetConversationPurposeTool, + slackSetConversationTopicTool, slackSetStatusTool, slackSetSuggestedPromptsTool, slackSetTitleTool, @@ -3814,9 +3845,16 @@ import { tinybirdTruncateDatasourceTool, } from '@/tools/tinybird' import { + trelloAddChecklistTool, trelloAddCommentTool, + trelloAddLabelTool, + trelloAddMemberTool, + trelloCreateBoardTool, trelloCreateCardTool, + trelloCreateListTool, trelloGetActionsTool, + trelloGetBoardTool, + trelloGetCardTool, trelloListCardsTool, trelloListListsTool, trelloUpdateCardTool, @@ -4295,6 +4333,14 @@ export const tools: Record = { asana_get_task: asanaGetTaskTool, asana_search_tasks: asanaSearchTasksTool, asana_update_task: asanaUpdateTaskTool, + asana_add_followers: asanaAddFollowersTool, + asana_create_project: asanaCreateProjectTool, + asana_create_section: asanaCreateSectionTool, + asana_create_subtask: asanaCreateSubtaskTool, + asana_delete_task: asanaDeleteTaskTool, + asana_get_project: asanaGetProjectTool, + asana_list_sections: asanaListSectionsTool, + asana_list_workspaces: asanaListWorkspacesTool, ashby_add_candidate_tag: ashbyAddCandidateTagTool, ashby_change_application_stage: ashbyChangeApplicationStageTool, ashby_create_application: ashbyCreateApplicationTool, @@ -4947,6 +4993,11 @@ export const tools: Record = { jira_remove_watcher: jiraRemoveWatcherTool, jira_get_users: jiraGetUsersTool, jira_search_users: jiraSearchUsersTool, + jira_list_projects: jiraListProjectsTool, + jira_get_project: jiraGetProjectTool, + jira_get_transitions: jiraGetTransitionsTool, + jira_list_issue_types: jiraListIssueTypesTool, + jira_get_fields: jiraGetFieldsTool, jsm_get_service_desks: jsmGetServiceDesksTool, jsm_get_request_types: jsmGetRequestTypesTool, jsm_get_request_type_fields: jsmGetRequestTypeFieldsTool, @@ -5089,6 +5140,13 @@ export const tools: Record = { slack_delete_canvas: slackDeleteCanvasTool, slack_create_conversation: slackCreateConversationTool, slack_invite_to_conversation: slackInviteToConversationTool, + slack_schedule_message: slackScheduleMessageTool, + slack_list_scheduled_messages: slackListScheduledMessagesTool, + slack_delete_scheduled_message: slackDeleteScheduledMessageTool, + slack_archive_conversation: slackArchiveConversationTool, + slack_rename_conversation: slackRenameConversationTool, + slack_set_conversation_topic: slackSetConversationTopicTool, + slack_set_conversation_purpose: slackSetConversationPurposeTool, github_repo_info: githubRepoInfoTool, github_repo_info_v2: githubRepoInfoV2Tool, github_latest_commit: githubLatestCommitTool, @@ -5537,6 +5595,11 @@ export const tools: Record = { monday_move_item_to_group: mondayMoveItemToGroupTool, monday_search_items: mondaySearchItemsTool, monday_update_item: mondayUpdateItemTool, + monday_change_column_value: mondayChangeColumnValueTool, + monday_create_board: mondayCreateBoardTool, + monday_create_column: mondayCreateColumnTool, + monday_duplicate_item: mondayDuplicateItemTool, + monday_get_groups: mondayGetGroupsTool, mongodb_query: mongodbQueryTool, mongodb_insert: mongodbInsertTool, mongodb_update: mongodbUpdateTool, @@ -6090,6 +6153,12 @@ export const tools: Record = { google_docs_insert_image: googleDocsInsertImageTool, google_docs_insert_page_break: googleDocsInsertPageBreakTool, google_docs_update_text_style: googleDocsUpdateTextStyleTool, + google_docs_update_paragraph_style: googleDocsUpdateParagraphStyleTool, + google_docs_create_paragraph_bullets: googleDocsCreateParagraphBulletsTool, + google_docs_delete_paragraph_bullets: googleDocsDeleteParagraphBulletsTool, + google_docs_delete_content_range: googleDocsDeleteContentRangeTool, + google_docs_create_named_range: googleDocsCreateNamedRangeTool, + google_docs_delete_named_range: googleDocsDeleteNamedRangeTool, google_books_volume_search: googleBooksVolumeSearchTool, google_books_volume_details: googleBooksVolumeDetailsTool, google_maps_air_quality: googleMapsAirQualityTool, @@ -6419,6 +6488,13 @@ export const tools: Record = { trello_update_card: trelloUpdateCardTool, trello_get_actions: trelloGetActionsTool, trello_add_comment: trelloAddCommentTool, + trello_create_board: trelloCreateBoardTool, + trello_get_board: trelloGetBoardTool, + trello_create_list: trelloCreateListTool, + trello_get_card: trelloGetCardTool, + trello_add_checklist: trelloAddChecklistTool, + trello_add_label: trelloAddLabelTool, + trello_add_member: trelloAddMemberTool, trigger_dev_trigger_task: triggerDevTriggerTaskTool, trigger_dev_batch_trigger_task: triggerDevBatchTriggerTaskTool, trigger_dev_get_batch: triggerDevGetBatchTool, diff --git a/apps/sim/tools/slack/archive_conversation.ts b/apps/sim/tools/slack/archive_conversation.ts new file mode 100644 index 00000000000..83fa22d5508 --- /dev/null +++ b/apps/sim/tools/slack/archive_conversation.ts @@ -0,0 +1,101 @@ +import type { + SlackArchiveConversationParams, + SlackArchiveConversationResponse, +} from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackArchiveConversationTool: ToolConfig< + SlackArchiveConversationParams, + SlackArchiveConversationResponse +> = { + id: 'slack_archive_conversation', + name: 'Slack Archive Conversation', + description: 'Archive a Slack channel so it is closed to new activity.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the channel to archive (e.g., C1234567890)', + }, + }, + + request: { + url: 'https://slack.com/api/conversations.archive', + method: 'POST', + headers: (params: SlackArchiveConversationParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackArchiveConversationParams) => ({ + channel: params.channel?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'already_archived') { + throw new Error('This channel is already archived.') + } + if (data.error === 'cant_archive_general') { + throw new Error('The #general channel cannot be archived.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'not_in_channel') { + throw new Error('The authenticated user is not a member of this channel.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to archive Slack conversation') + } + + return { + success: true, + output: { + ok: true, + }, + } + }, + + outputs: { + ok: { + type: 'boolean', + description: 'Whether the conversation was archived successfully', + }, + }, +} diff --git a/apps/sim/tools/slack/delete_scheduled_message.ts b/apps/sim/tools/slack/delete_scheduled_message.ts new file mode 100644 index 00000000000..efbaadc7cb9 --- /dev/null +++ b/apps/sim/tools/slack/delete_scheduled_message.ts @@ -0,0 +1,104 @@ +import type { + SlackDeleteScheduledMessageParams, + SlackDeleteScheduledMessageResponse, +} from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackDeleteScheduledMessageTool: ToolConfig< + SlackDeleteScheduledMessageParams, + SlackDeleteScheduledMessageResponse +> = { + id: 'slack_delete_scheduled_message', + name: 'Slack Delete Scheduled Message', + description: 'Delete a pending scheduled message before it posts to Slack.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Channel ID where the scheduled message is queued (e.g., C1234567890)', + }, + scheduledMessageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Scheduled message ID from chat.scheduleMessage (e.g., Q1234ABCD)', + }, + }, + + request: { + url: 'https://slack.com/api/chat.deleteScheduledMessage', + method: 'POST', + headers: (params: SlackDeleteScheduledMessageParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackDeleteScheduledMessageParams) => ({ + channel: params.channel?.trim(), + scheduled_message_id: params.scheduledMessageId?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'invalid_scheduled_message_id') { + throw new Error( + 'Invalid scheduled message ID. The message may have already posted or is set to post within 60 seconds.' + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scope (chat:write).' + ) + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to delete scheduled Slack message') + } + + return { + success: true, + output: { + ok: true, + }, + } + }, + + outputs: { + ok: { + type: 'boolean', + description: 'Whether the scheduled message was deleted successfully', + }, + }, +} diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index 1612876c998..90c55b42045 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -1,9 +1,11 @@ import { slackAddReactionTool } from '@/tools/slack/add_reaction' +import { slackArchiveConversationTool } from '@/tools/slack/archive_conversation' import { slackCanvasTool } from '@/tools/slack/canvas' import { slackCreateChannelCanvasTool } from '@/tools/slack/create_channel_canvas' import { slackCreateConversationTool } from '@/tools/slack/create_conversation' import { slackDeleteCanvasTool } from '@/tools/slack/delete_canvas' import { slackDeleteMessageTool } from '@/tools/slack/delete_message' +import { slackDeleteScheduledMessageTool } from '@/tools/slack/delete_scheduled_message' import { slackDownloadTool } from '@/tools/slack/download' import { slackEditCanvasTool } from '@/tools/slack/edit_canvas' import { slackEphemeralMessageTool } from '@/tools/slack/ephemeral_message' @@ -20,6 +22,7 @@ import { slackInviteToConversationTool } from '@/tools/slack/invite_to_conversat import { slackListCanvasesTool } from '@/tools/slack/list_canvases' import { slackListChannelsTool } from '@/tools/slack/list_channels' import { slackListMembersTool } from '@/tools/slack/list_members' +import { slackListScheduledMessagesTool } from '@/tools/slack/list_scheduled_messages' import { slackListUsersTool } from '@/tools/slack/list_users' import { slackLookupCanvasSectionsTool } from '@/tools/slack/lookup_canvas_sections' import { slackMessageTool } from '@/tools/slack/message' @@ -28,6 +31,10 @@ import { slackOpenViewTool } from '@/tools/slack/open_view' import { slackPublishViewTool } from '@/tools/slack/publish_view' import { slackPushViewTool } from '@/tools/slack/push_view' import { slackRemoveReactionTool } from '@/tools/slack/remove_reaction' +import { slackRenameConversationTool } from '@/tools/slack/rename_conversation' +import { slackScheduleMessageTool } from '@/tools/slack/schedule_message' +import { slackSetConversationPurposeTool } from '@/tools/slack/set_conversation_purpose' +import { slackSetConversationTopicTool } from '@/tools/slack/set_conversation_topic' import { slackSetStatusTool } from '@/tools/slack/set_status' import { slackSetSuggestedPromptsTool } from '@/tools/slack/set_suggested_prompts' import { slackSetTitleTool } from '@/tools/slack/set_title' @@ -70,6 +77,13 @@ export { slackSetTitleTool, slackSetSuggestedPromptsTool, slackInviteToConversationTool, + slackScheduleMessageTool, + slackListScheduledMessagesTool, + slackDeleteScheduledMessageTool, + slackArchiveConversationTool, + slackRenameConversationTool, + slackSetConversationTopicTool, + slackSetConversationPurposeTool, } export * from './types' diff --git a/apps/sim/tools/slack/list_scheduled_messages.ts b/apps/sim/tools/slack/list_scheduled_messages.ts new file mode 100644 index 00000000000..5c2a067d30f --- /dev/null +++ b/apps/sim/tools/slack/list_scheduled_messages.ts @@ -0,0 +1,144 @@ +import type { + SlackListScheduledMessagesParams, + SlackListScheduledMessagesResponse, +} from '@/tools/slack/types' +import { SCHEDULED_MESSAGE_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackListScheduledMessagesTool: ToolConfig< + SlackListScheduledMessagesParams, + SlackListScheduledMessagesResponse +> = { + id: 'slack_list_scheduled_messages', + name: 'Slack List Scheduled Messages', + description: + 'List pending scheduled messages in a Slack workspace, optionally filtered by channel.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional channel ID to filter scheduled messages (e.g., C1234567890)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of scheduled messages to return', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor (next_cursor) from a previous response', + }, + oldest: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp of the oldest scheduled message to include', + }, + latest: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp of the latest scheduled message to include', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Encoded team ID (required only with org-level tokens)', + }, + }, + + request: { + url: 'https://slack.com/api/chat.scheduledMessages.list', + method: 'POST', + headers: (params: SlackListScheduledMessagesParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackListScheduledMessagesParams) => { + const body: Record = {} + if (params.channel?.trim()) { + body.channel = params.channel.trim() + } + if (params.limit != null) { + body.limit = params.limit + } + if (params.cursor?.trim()) { + body.cursor = params.cursor.trim() + } + if (params.oldest?.trim()) { + body.oldest = params.oldest.trim() + } + if (params.latest?.trim()) { + body.latest = params.latest.trim() + } + if (params.teamId?.trim()) { + body.team_id = params.teamId.trim() + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to list scheduled Slack messages') + } + + return { + success: true, + output: { + scheduledMessages: data.scheduled_messages || [], + nextCursor: data.response_metadata?.next_cursor || null, + }, + } + }, + + outputs: { + scheduledMessages: { + type: 'array', + description: 'Array of pending scheduled message objects', + items: { + type: 'object', + properties: SCHEDULED_MESSAGE_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (null when there are no more pages)', + }, + }, +} diff --git a/apps/sim/tools/slack/rename_conversation.ts b/apps/sim/tools/slack/rename_conversation.ts new file mode 100644 index 00000000000..8244ca0ae97 --- /dev/null +++ b/apps/sim/tools/slack/rename_conversation.ts @@ -0,0 +1,133 @@ +import type { + SlackRenameConversationParams, + SlackRenameConversationResponse, +} from '@/tools/slack/types' +import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackRenameConversationTool: ToolConfig< + SlackRenameConversationParams, + SlackRenameConversationResponse +> = { + id: 'slack_rename_conversation', + name: 'Slack Rename Conversation', + description: 'Rename an existing Slack channel.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the channel to rename (e.g., C1234567890)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'New channel name (lowercase letters, numbers, hyphens, underscores only; max 80 characters)', + }, + }, + + request: { + url: 'https://slack.com/api/conversations.rename', + method: 'POST', + headers: (params: SlackRenameConversationParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackRenameConversationParams) => ({ + channel: params.channel?.trim(), + name: params.name?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'name_taken') { + throw new Error('A channel with this name already exists in the workspace.') + } + if ( + data.error === 'invalid_name' || + data.error === 'invalid_name_specials' || + data.error === 'invalid_name_maxlength' || + data.error === 'invalid_name_required' + ) { + throw new Error( + 'Invalid channel name. Use only lowercase letters, numbers, hyphens, and underscores (max 80 characters).' + ) + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'not_in_channel') { + throw new Error('The authenticated user is not a member of this channel.') + } + if (data.error === 'not_authorized') { + throw new Error('You do not have permission to rename this channel.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to rename Slack conversation') + } + + const ch = data.channel || {} + + return { + success: true, + output: { + channelInfo: { + id: ch.id, + name: ch.name, + is_private: ch.is_private || false, + is_archived: ch.is_archived || false, + is_member: ch.is_member || false, + topic: ch.topic?.value || '', + purpose: ch.purpose?.value || '', + created: ch.created, + creator: ch.creator, + }, + }, + } + }, + + outputs: { + channelInfo: { + type: 'object', + description: 'The channel object after renaming', + properties: CHANNEL_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/slack/schedule_message.ts b/apps/sim/tools/slack/schedule_message.ts new file mode 100644 index 00000000000..31075e879b9 --- /dev/null +++ b/apps/sim/tools/slack/schedule_message.ts @@ -0,0 +1,138 @@ +import type { SlackScheduleMessageParams, SlackScheduleMessageResponse } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackScheduleMessageTool: ToolConfig< + SlackScheduleMessageParams, + SlackScheduleMessageResponse +> = { + id: 'slack_schedule_message', + name: 'Slack Schedule Message', + description: 'Schedule a message to be sent to a Slack channel or DM at a future time.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Channel, private group, or DM to receive the message (e.g., C1234567890)', + }, + postAt: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Unix timestamp (seconds) representing the future time the message should post', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Message text to send (supports Slack mrkdwn formatting)', + }, + blocks: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text.', + }, + threadTs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Thread timestamp to reply to (creates a scheduled thread reply)', + }, + }, + + request: { + url: 'https://slack.com/api/chat.scheduleMessage', + method: 'POST', + headers: (params: SlackScheduleMessageParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackScheduleMessageParams) => { + const body: Record = { + channel: params.channel?.trim(), + post_at: params.postAt, + } + if (params.text) { + body.text = params.text + } + if (params.blocks) { + body.blocks = typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks + } + if (params.threadTs?.trim()) { + body.thread_ts = params.threadTs.trim() + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'time_in_past' || data.error === 'time_too_far') { + throw new Error( + 'The scheduled time is invalid. It must be in the future and within 120 days.' + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scope (chat:write).' + ) + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to schedule Slack message') + } + + return { + success: true, + output: { + scheduledMessageId: data.scheduled_message_id, + postAt: data.post_at, + channel: data.channel, + message: data.message || {}, + }, + } + }, + + outputs: { + scheduledMessageId: { + type: 'string', + description: 'Identifier of the scheduled message (used to delete it before it posts)', + }, + postAt: { type: 'number', description: 'Unix timestamp when the message will post' }, + channel: { type: 'string', description: 'Channel ID where the message is scheduled' }, + message: { type: 'object', description: 'The scheduled message object returned by Slack' }, + }, +} diff --git a/apps/sim/tools/slack/set_conversation_purpose.ts b/apps/sim/tools/slack/set_conversation_purpose.ts new file mode 100644 index 00000000000..bad99f8c209 --- /dev/null +++ b/apps/sim/tools/slack/set_conversation_purpose.ts @@ -0,0 +1,105 @@ +import type { + SlackSetConversationPurposeParams, + SlackSetConversationPurposeResponse, +} from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackSetConversationPurposeTool: ToolConfig< + SlackSetConversationPurposeParams, + SlackSetConversationPurposeResponse +> = { + id: 'slack_set_conversation_purpose', + name: 'Slack Set Conversation Purpose', + description: 'Set the purpose (description) for a Slack channel (max 250 characters).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the channel to update (e.g., C1234567890)', + }, + purpose: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New purpose/description text (max 250 characters)', + }, + }, + + request: { + url: 'https://slack.com/api/conversations.setPurpose', + method: 'POST', + headers: (params: SlackSetConversationPurposeParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackSetConversationPurposeParams) => ({ + channel: params.channel?.trim(), + purpose: params.purpose, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'too_long') { + throw new Error('The purpose is too long. The maximum length is 250 characters.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'not_in_channel') { + throw new Error('The authenticated user is not a member of this channel.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to set Slack conversation purpose') + } + + return { + success: true, + output: { + purpose: data.purpose ?? '', + }, + } + }, + + outputs: { + purpose: { + type: 'string', + description: 'The purpose/description that was set on the channel', + }, + }, +} diff --git a/apps/sim/tools/slack/set_conversation_topic.ts b/apps/sim/tools/slack/set_conversation_topic.ts new file mode 100644 index 00000000000..693de3c329e --- /dev/null +++ b/apps/sim/tools/slack/set_conversation_topic.ts @@ -0,0 +1,119 @@ +import type { + SlackSetConversationTopicParams, + SlackSetConversationTopicResponse, +} from '@/tools/slack/types' +import { CHANNEL_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackSetConversationTopicTool: ToolConfig< + SlackSetConversationTopicParams, + SlackSetConversationTopicResponse +> = { + id: 'slack_set_conversation_topic', + name: 'Slack Set Conversation Topic', + description: 'Set the topic for a Slack channel (max 250 characters).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + channel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the channel to update (e.g., C1234567890)', + }, + topic: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New topic text (max 250 characters; no formatting or linkification)', + }, + }, + + request: { + url: 'https://slack.com/api/conversations.setTopic', + method: 'POST', + headers: (params: SlackSetConversationTopicParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackSetConversationTopicParams) => ({ + channel: params.channel?.trim(), + topic: params.topic, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'too_long') { + throw new Error('The topic is too long. The maximum length is 250 characters.') + } + if (data.error === 'channel_not_found') { + throw new Error('Channel not found. Please verify the channel ID.') + } + if (data.error === 'not_in_channel') { + throw new Error('The authenticated user is not a member of this channel.') + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:manage, groups:write).' + ) + } + if (data.error === 'invalid_auth') { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to set Slack conversation topic') + } + + const ch = data.channel || {} + + return { + success: true, + output: { + channelInfo: { + id: ch.id, + name: ch.name, + is_private: ch.is_private || false, + is_archived: ch.is_archived || false, + is_member: ch.is_member || false, + topic: ch.topic?.value || '', + purpose: ch.purpose?.value || '', + created: ch.created, + creator: ch.creator, + }, + }, + } + }, + + outputs: { + channelInfo: { + type: 'object', + description: 'The channel object after updating the topic', + properties: CHANNEL_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 552b93a3452..3489109202d 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -302,6 +302,18 @@ export const CHANNEL_OUTPUT_PROPERTIES = { updated: { type: 'number', description: 'Unix timestamp of last update', optional: true }, } as const satisfies Record +/** + * Output definition for scheduled message objects + * Based on Slack chat.scheduledMessages.list (https://docs.slack.dev/reference/methods/chat.scheduledMessages.list) + */ +export const SCHEDULED_MESSAGE_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'Scheduled message ID' }, + channel_id: { type: 'string', description: 'Channel the message is scheduled for' }, + post_at: { type: 'number', description: 'Unix timestamp when the message will post' }, + date_created: { type: 'number', description: 'Unix timestamp when the schedule was created' }, + text: { type: 'string', description: 'Scheduled message text', optional: true }, +} as const satisfies Record + /** * Complete channel object output definition */ @@ -913,6 +925,47 @@ export interface SlackPublishViewParams extends SlackBaseParams { view: object | string } +export interface SlackScheduleMessageParams extends SlackBaseParams { + channel: string + postAt: number + text?: string + blocks?: string + threadTs?: string +} + +export interface SlackListScheduledMessagesParams extends SlackBaseParams { + channel?: string + limit?: number + cursor?: string + oldest?: string + latest?: string + teamId?: string +} + +export interface SlackDeleteScheduledMessageParams extends SlackBaseParams { + channel: string + scheduledMessageId: string +} + +export interface SlackArchiveConversationParams extends SlackBaseParams { + channel: string +} + +export interface SlackRenameConversationParams extends SlackBaseParams { + channel: string + name: string +} + +export interface SlackSetConversationTopicParams extends SlackBaseParams { + channel: string + topic: string +} + +export interface SlackSetConversationPurposeParams extends SlackBaseParams { + channel: string + purpose: string +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -1398,6 +1451,60 @@ export interface SlackGetThreadRepliesResponse extends ToolResponse { } } +export interface SlackScheduledMessage { + id: string + channel_id: string + post_at: number + date_created: number + text?: string +} + +export interface SlackScheduleMessageResponse extends ToolResponse { + output: { + scheduledMessageId: string + postAt: number + channel: string + message: Record + } +} + +export interface SlackListScheduledMessagesResponse extends ToolResponse { + output: { + scheduledMessages: SlackScheduledMessage[] + nextCursor: string | null + } +} + +export interface SlackDeleteScheduledMessageResponse extends ToolResponse { + output: { + ok: boolean + } +} + +export interface SlackArchiveConversationResponse extends ToolResponse { + output: { + ok: boolean + } +} + +export interface SlackRenameConversationResponse extends ToolResponse { + output: { + channelInfo: SlackChannel + } +} + +export interface SlackSetConversationTopicResponse extends ToolResponse { + output: { + channelInfo: SlackChannel + } +} + +export interface SlackSetConversationPurposeResponse extends ToolResponse { + output: { + purpose: string + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -1434,3 +1541,10 @@ export type SlackResponse = | SlackUpdateViewResponse | SlackPushViewResponse | SlackPublishViewResponse + | SlackScheduleMessageResponse + | SlackListScheduledMessagesResponse + | SlackDeleteScheduledMessageResponse + | SlackArchiveConversationResponse + | SlackRenameConversationResponse + | SlackSetConversationTopicResponse + | SlackSetConversationPurposeResponse diff --git a/apps/sim/tools/trello/add_checklist.ts b/apps/sim/tools/trello/add_checklist.ts new file mode 100644 index 00000000000..bb4583505ec --- /dev/null +++ b/apps/sim/tools/trello/add_checklist.ts @@ -0,0 +1,136 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { + extractTrelloErrorMessage, + mapTrelloChecklist, + TRELLO_API_BASE_URL, +} from '@/tools/trello/shared' +import type { TrelloAddChecklistParams, TrelloAddChecklistResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloAddChecklistTool: ToolConfig< + TrelloAddChecklistParams, + TrelloAddChecklistResponse +> = { + id: 'trello_add_checklist', + name: 'Trello Add Checklist', + description: 'Add a checklist to a Trello card', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + cardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello card ID to add the checklist to (24-character hex string)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the checklist', + }, + pos: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Position of the checklist (top, bottom, or positive float)', + }, + }, + + request: { + url: (params) => { + if (!params.cardId) { + throw new Error('Card ID is required') + } + if (!params.name) { + throw new Error('Checklist name is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/cards/${params.cardId.trim()}/checklists`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + url.searchParams.set('name', params.name.trim()) + + if (params.pos) url.searchParams.set('pos', params.pos) + + return url.toString() + }, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to add checklist') + + return { + success: false, + output: { + error, + }, + error, + } + } + + try { + const checklist = mapTrelloChecklist(data) + + return { + success: true, + output: { + checklist, + }, + } + } catch (error) { + const message = getErrorMessage(error, 'Failed to parse created checklist') + + return { + success: false, + output: { + error: message, + }, + error: message, + } + } + }, + + outputs: { + checklist: { + type: 'json', + description: 'Created checklist (id, name, idCard, idBoard, pos)', + optional: true, + properties: { + id: { type: 'string', description: 'Checklist ID' }, + name: { type: 'string', description: 'Checklist name' }, + idCard: { type: 'string', description: 'Card ID containing the checklist' }, + idBoard: { + type: 'string', + description: 'Board ID containing the checklist', + optional: true, + }, + pos: { type: 'number', description: 'Checklist position on the card' }, + }, + }, + }, +} diff --git a/apps/sim/tools/trello/add_label.ts b/apps/sim/tools/trello/add_label.ts new file mode 100644 index 00000000000..24773b9b657 --- /dev/null +++ b/apps/sim/tools/trello/add_label.ts @@ -0,0 +1,99 @@ +import { env } from '@/lib/core/config/env' +import { extractTrelloErrorMessage, getIdArray, TRELLO_API_BASE_URL } from '@/tools/trello/shared' +import type { TrelloAddLabelParams, TrelloAddLabelResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloAddLabelTool: ToolConfig = { + id: 'trello_add_label', + name: 'Trello Add Label', + description: 'Attach an existing label to a Trello card', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + cardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello card ID to attach the label to (24-character hex string)', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the label to attach (24-character hex string)', + }, + }, + + request: { + url: (params) => { + if (!params.cardId) { + throw new Error('Card ID is required') + } + if (!params.labelId) { + throw new Error('Label ID is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/cards/${params.cardId.trim()}/idLabels`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + url.searchParams.set('value', params.labelId.trim()) + + return url.toString() + }, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to add label') + + return { + success: false, + output: { + labelIds: [], + error, + }, + error, + } + } + + return { + success: true, + output: { + labelIds: getIdArray(data), + }, + } + }, + + outputs: { + labelIds: { + type: 'array', + description: 'Label IDs now applied to the card', + items: { + type: 'string', + description: 'A Trello label ID', + }, + }, + }, +} diff --git a/apps/sim/tools/trello/add_member.ts b/apps/sim/tools/trello/add_member.ts new file mode 100644 index 00000000000..1baa8df446e --- /dev/null +++ b/apps/sim/tools/trello/add_member.ts @@ -0,0 +1,99 @@ +import { env } from '@/lib/core/config/env' +import { extractTrelloErrorMessage, getIdArray, TRELLO_API_BASE_URL } from '@/tools/trello/shared' +import type { TrelloAddMemberParams, TrelloAddMemberResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloAddMemberTool: ToolConfig = { + id: 'trello_add_member', + name: 'Trello Add Member', + description: 'Assign a member to a Trello card', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + cardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello card ID to assign the member to (24-character hex string)', + }, + memberId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the member to assign (24-character hex string)', + }, + }, + + request: { + url: (params) => { + if (!params.cardId) { + throw new Error('Card ID is required') + } + if (!params.memberId) { + throw new Error('Member ID is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/cards/${params.cardId.trim()}/idMembers`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + url.searchParams.set('value', params.memberId.trim()) + + return url.toString() + }, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to add member') + + return { + success: false, + output: { + memberIds: [], + error, + }, + error, + } + } + + return { + success: true, + output: { + memberIds: getIdArray(data), + }, + } + }, + + outputs: { + memberIds: { + type: 'array', + description: 'Member IDs now assigned to the card', + items: { + type: 'string', + description: 'A Trello member ID', + }, + }, + }, +} diff --git a/apps/sim/tools/trello/create_board.ts b/apps/sim/tools/trello/create_board.ts new file mode 100644 index 00000000000..5c776a2d342 --- /dev/null +++ b/apps/sim/tools/trello/create_board.ts @@ -0,0 +1,143 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { + extractTrelloErrorMessage, + mapTrelloBoard, + TRELLO_API_BASE_URL, +} from '@/tools/trello/shared' +import type { TrelloCreateBoardParams, TrelloCreateBoardResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloCreateBoardTool: ToolConfig = + { + id: 'trello_create_board', + name: 'Trello Create Board', + description: 'Create a new Trello board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the board', + }, + desc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the board', + }, + idOrganization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID or name of the workspace/organization the board belongs to', + }, + defaultLists: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to create the default lists (To Do, Doing, Done) on the new board', + }, + }, + + request: { + url: (params) => { + if (!params.name) { + throw new Error('Board name is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/boards`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + url.searchParams.set('name', params.name.trim()) + + if (params.desc) url.searchParams.set('desc', params.desc) + if (params.idOrganization) + url.searchParams.set('idOrganization', params.idOrganization.trim()) + if (params.defaultLists !== undefined) { + url.searchParams.set('defaultLists', String(params.defaultLists)) + } + + return url.toString() + }, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to create board') + + return { + success: false, + output: { + error, + }, + error, + } + } + + try { + const board = mapTrelloBoard(data) + + return { + success: true, + output: { + board, + }, + } + } catch (error) { + const message = getErrorMessage(error, 'Failed to parse created board') + + return { + success: false, + output: { + error: message, + }, + error: message, + } + } + }, + + outputs: { + board: { + type: 'json', + description: 'Created board (id, name, desc, url, closed, idOrganization)', + optional: true, + properties: { + id: { type: 'string', description: 'Board ID' }, + name: { type: 'string', description: 'Board name' }, + desc: { type: 'string', description: 'Board description' }, + url: { type: 'string', description: 'Full board URL' }, + closed: { type: 'boolean', description: 'Whether the board is closed' }, + idOrganization: { + type: 'string', + description: 'ID of the workspace/organization the board belongs to', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/trello/create_list.ts b/apps/sim/tools/trello/create_list.ts new file mode 100644 index 00000000000..c89c271472a --- /dev/null +++ b/apps/sim/tools/trello/create_list.ts @@ -0,0 +1,130 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { + extractTrelloErrorMessage, + mapTrelloList, + TRELLO_API_BASE_URL, +} from '@/tools/trello/shared' +import type { TrelloCreateListParams, TrelloCreateListResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloCreateListTool: ToolConfig = { + id: 'trello_create_list', + name: 'Trello Create List', + description: 'Create a new list on a Trello board', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello board ID the list belongs to (24-character hex string)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the list', + }, + pos: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Position of the list (top, bottom, or positive float)', + }, + }, + + request: { + url: (params) => { + if (!params.name) { + throw new Error('List name is required') + } + if (!params.boardId) { + throw new Error('Board ID is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/lists`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + url.searchParams.set('name', params.name.trim()) + url.searchParams.set('idBoard', params.boardId.trim()) + + if (params.pos) url.searchParams.set('pos', params.pos) + + return url.toString() + }, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to create list') + + return { + success: false, + output: { + error, + }, + error, + } + } + + try { + const list = mapTrelloList(data) + + return { + success: true, + output: { + list, + }, + } + } catch (error) { + const message = getErrorMessage(error, 'Failed to parse created list') + + return { + success: false, + output: { + error: message, + }, + error: message, + } + } + }, + + outputs: { + list: { + type: 'json', + description: 'Created list (id, name, closed, pos, idBoard)', + optional: true, + properties: { + id: { type: 'string', description: 'List ID' }, + name: { type: 'string', description: 'List name' }, + closed: { type: 'boolean', description: 'Whether the list is archived' }, + pos: { type: 'number', description: 'List position on the board' }, + idBoard: { type: 'string', description: 'Board ID containing the list' }, + }, + }, + }, +} diff --git a/apps/sim/tools/trello/get_board.ts b/apps/sim/tools/trello/get_board.ts new file mode 100644 index 00000000000..2dca4e1dccc --- /dev/null +++ b/apps/sim/tools/trello/get_board.ts @@ -0,0 +1,116 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { + extractTrelloErrorMessage, + mapTrelloBoard, + TRELLO_API_BASE_URL, +} from '@/tools/trello/shared' +import type { TrelloGetBoardParams, TrelloGetBoardResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloGetBoardTool: ToolConfig = { + id: 'trello_get_board', + name: 'Trello Get Board', + description: 'Retrieve a single Trello board by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + boardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello board ID (24-character hex string)', + }, + }, + + request: { + url: (params) => { + if (!params.boardId) { + throw new Error('Board ID is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/boards/${params.boardId.trim()}`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + + return url.toString() + }, + method: 'GET', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to get board') + + return { + success: false, + output: { + error, + }, + error, + } + } + + try { + const board = mapTrelloBoard(data) + + return { + success: true, + output: { + board, + }, + } + } catch (error) { + const message = getErrorMessage(error, 'Failed to parse board') + + return { + success: false, + output: { + error: message, + }, + error: message, + } + } + }, + + outputs: { + board: { + type: 'json', + description: 'Board (id, name, desc, url, closed, idOrganization)', + optional: true, + properties: { + id: { type: 'string', description: 'Board ID' }, + name: { type: 'string', description: 'Board name' }, + desc: { type: 'string', description: 'Board description' }, + url: { type: 'string', description: 'Full board URL' }, + closed: { type: 'boolean', description: 'Whether the board is closed' }, + idOrganization: { + type: 'string', + description: 'ID of the workspace/organization the board belongs to', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/trello/get_card.ts b/apps/sim/tools/trello/get_card.ts new file mode 100644 index 00000000000..33935104ed8 --- /dev/null +++ b/apps/sim/tools/trello/get_card.ts @@ -0,0 +1,148 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { + extractTrelloErrorMessage, + mapTrelloCard, + TRELLO_API_BASE_URL, +} from '@/tools/trello/shared' +import type { TrelloGetCardParams, TrelloGetCardResponse } from '@/tools/trello/types' +import type { ToolConfig } from '@/tools/types' + +export const trelloGetCardTool: ToolConfig = { + id: 'trello_get_card', + name: 'Trello Get Card', + description: 'Retrieve a single Trello card by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'trello', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Trello OAuth access token', + }, + cardId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trello card ID (24-character hex string)', + }, + }, + + request: { + url: (params) => { + if (!params.cardId) { + throw new Error('Card ID is required') + } + const apiKey = env.TRELLO_API_KEY + + if (!apiKey) { + throw new Error('TRELLO_API_KEY environment variable is not set') + } + + const url = new URL(`${TRELLO_API_BASE_URL}/cards/${params.cardId.trim()}`) + url.searchParams.set('key', apiKey) + url.searchParams.set('token', params.accessToken) + + return url.toString() + }, + method: 'GET', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json().catch(() => null) + + if (!response.ok) { + const error = extractTrelloErrorMessage(response, data, 'Failed to get card') + + return { + success: false, + output: { + error, + }, + error, + } + } + + try { + const card = mapTrelloCard(data) + + return { + success: true, + output: { + card, + }, + } + } catch (error) { + const message = getErrorMessage(error, 'Failed to parse card') + + return { + success: false, + output: { + error: message, + }, + error: message, + } + } + }, + + outputs: { + card: { + type: 'json', + description: + 'Card (id, name, desc, url, idBoard, idList, closed, labelIds, labels, due, dueComplete)', + optional: true, + properties: { + id: { type: 'string', description: 'Card ID' }, + name: { type: 'string', description: 'Card name' }, + desc: { type: 'string', description: 'Card description' }, + url: { type: 'string', description: 'Full card URL' }, + idBoard: { type: 'string', description: 'Board ID containing the card' }, + idList: { type: 'string', description: 'List ID containing the card' }, + closed: { type: 'boolean', description: 'Whether the card is archived' }, + labelIds: { + type: 'array', + description: 'Label IDs applied to the card', + items: { + type: 'string', + description: 'A Trello label ID', + }, + }, + labels: { + type: 'array', + description: 'Labels applied to the card', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label name' }, + color: { + type: 'string', + description: 'Label color', + optional: true, + }, + }, + }, + }, + due: { + type: 'string', + description: 'Card due date in ISO 8601 format', + optional: true, + }, + dueComplete: { + type: 'boolean', + description: 'Whether the due date is complete', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/trello/index.ts b/apps/sim/tools/trello/index.ts index e420abf3893..9f25bc4659f 100644 --- a/apps/sim/tools/trello/index.ts +++ b/apps/sim/tools/trello/index.ts @@ -1,6 +1,13 @@ +import { trelloAddChecklistTool } from '@/tools/trello/add_checklist' import { trelloAddCommentTool } from '@/tools/trello/add_comment' +import { trelloAddLabelTool } from '@/tools/trello/add_label' +import { trelloAddMemberTool } from '@/tools/trello/add_member' +import { trelloCreateBoardTool } from '@/tools/trello/create_board' import { trelloCreateCardTool } from '@/tools/trello/create_card' +import { trelloCreateListTool } from '@/tools/trello/create_list' import { trelloGetActionsTool } from '@/tools/trello/get_actions' +import { trelloGetBoardTool } from '@/tools/trello/get_board' +import { trelloGetCardTool } from '@/tools/trello/get_card' import { trelloListCardsTool } from '@/tools/trello/list_cards' import { trelloListListsTool } from '@/tools/trello/list_lists' import { trelloUpdateCardTool } from '@/tools/trello/update_card' @@ -12,6 +19,13 @@ export { trelloUpdateCardTool, trelloGetActionsTool, trelloAddCommentTool, + trelloCreateBoardTool, + trelloGetBoardTool, + trelloCreateListTool, + trelloGetCardTool, + trelloAddChecklistTool, + trelloAddLabelTool, + trelloAddMemberTool, } export * from '@/tools/trello/types' diff --git a/apps/sim/tools/trello/shared.ts b/apps/sim/tools/trello/shared.ts index 2491d5add68..27617a5ca77 100644 --- a/apps/sim/tools/trello/shared.ts +++ b/apps/sim/tools/trello/shared.ts @@ -4,7 +4,9 @@ import type { TrelloActionBoardTarget, TrelloActionCardTarget, TrelloActionListTarget, + TrelloBoard, TrelloCard, + TrelloChecklist, TrelloComment, TrelloLabel, TrelloList, @@ -52,7 +54,7 @@ function getOptionalNumber(value: unknown): number | null { return null } -function getIdArray(value: unknown): string[] { +export function getIdArray(value: unknown): string[] { if (!Array.isArray(value)) { return [] } @@ -175,6 +177,35 @@ export function mapTrelloCard(value: unknown): TrelloCard { } } +export function mapTrelloBoard(value: unknown): TrelloBoard { + if (!isRecordLike(value)) { + throw new Error('Trello returned an invalid board object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getRequiredString(value.name, 'name'), + desc: typeof value.desc === 'string' ? value.desc : '', + url: getRequiredString(value.url, 'url'), + closed: typeof value.closed === 'boolean' ? value.closed : false, + idOrganization: getOptionalString(value.idOrganization), + } +} + +export function mapTrelloChecklist(value: unknown): TrelloChecklist { + if (!isRecordLike(value)) { + throw new Error('Trello returned an invalid checklist object') + } + + return { + id: getRequiredString(value.id, 'id'), + name: getRequiredString(value.name, 'name'), + idCard: getRequiredString(value.idCard, 'idCard'), + idBoard: getOptionalString(value.idBoard), + pos: getNumber(value.pos), + } +} + export function mapTrelloAction(value: unknown): TrelloAction { if (!isRecordLike(value)) { throw new Error('Trello returned an invalid action object') diff --git a/apps/sim/tools/trello/types.ts b/apps/sim/tools/trello/types.ts index e3edc3c592a..fdbbbc8d7e3 100644 --- a/apps/sim/tools/trello/types.ts +++ b/apps/sim/tools/trello/types.ts @@ -1,11 +1,20 @@ import type { ToolResponse } from '@/tools/types' -interface TrelloBoard { +export interface TrelloBoard { id: string name: string desc: string url: string closed: boolean + idOrganization: string | null +} + +export interface TrelloChecklist { + id: string + name: string + idCard: string + idBoard: string | null + pos: number } export interface TrelloLabel { @@ -123,6 +132,50 @@ export interface TrelloAddCommentParams { text: string } +export interface TrelloCreateBoardParams { + accessToken: string + name: string + desc?: string + idOrganization?: string + defaultLists?: boolean +} + +export interface TrelloGetBoardParams { + accessToken: string + boardId: string +} + +export interface TrelloCreateListParams { + accessToken: string + boardId: string + name: string + pos?: string +} + +export interface TrelloGetCardParams { + accessToken: string + cardId: string +} + +export interface TrelloAddChecklistParams { + accessToken: string + cardId: string + name: string + pos?: string +} + +export interface TrelloAddLabelParams { + accessToken: string + cardId: string + labelId: string +} + +export interface TrelloAddMemberParams { + accessToken: string + cardId: string + memberId: string +} + export interface TrelloListListsResponse extends ToolResponse { output: { lists: TrelloList[] @@ -168,6 +221,55 @@ export interface TrelloAddCommentResponse extends ToolResponse { } } +export interface TrelloCreateBoardResponse extends ToolResponse { + output: { + board?: TrelloBoard + error?: string + } +} + +export interface TrelloGetBoardResponse extends ToolResponse { + output: { + board?: TrelloBoard + error?: string + } +} + +export interface TrelloCreateListResponse extends ToolResponse { + output: { + list?: TrelloList + error?: string + } +} + +export interface TrelloGetCardResponse extends ToolResponse { + output: { + card?: TrelloCard + error?: string + } +} + +export interface TrelloAddChecklistResponse extends ToolResponse { + output: { + checklist?: TrelloChecklist + error?: string + } +} + +export interface TrelloAddLabelResponse extends ToolResponse { + output: { + labelIds: string[] + error?: string + } +} + +export interface TrelloAddMemberResponse extends ToolResponse { + output: { + memberIds: string[] + error?: string + } +} + export type TrelloResponse = | TrelloListListsResponse | TrelloListCardsResponse @@ -175,3 +277,10 @@ export type TrelloResponse = | TrelloUpdateCardResponse | TrelloGetActionsResponse | TrelloAddCommentResponse + | TrelloCreateBoardResponse + | TrelloGetBoardResponse + | TrelloCreateListResponse + | TrelloGetCardResponse + | TrelloAddChecklistResponse + | TrelloAddLabelResponse + | TrelloAddMemberResponse diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 5387f3cf638..48fc2560c09 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 873, - zodRoutes: 873, + totalRoutes: 881, + zodRoutes: 881, nonZodRoutes: 0, } as const From ad1bb6bc2c952eb95a63ea6fc8dbef993a9b5efc Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 10:45:50 -0700 Subject: [PATCH 3/7] =?UTF-8?q?fix(integrations):=20wave-4=20validation=20?= =?UTF-8?q?pass=20=E2=80=94=20fix=20alignment=20enum,=20GraphQL=20input-ob?= =?UTF-8?q?ject,=20scope/UI=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive /validate-integration of all 6 modified integrations (existing + new tools) vs live API docs. Fixes: - google_docs: CRITICAL alignment enum LEFT/RIGHT/JUSTIFY -> API enum START/END/JUSTIFIED (mapped); namedStyleType 'unchanged' option; 'zero-based' index wording - monday: CRITICAL search_items columns now emits GraphQL input-object with unquoted keys (was always failing the non-cursor branch) - slack: schedule_message DMs via user-id-as-channel; add channels:manage/groups:write/reactions:read scope descriptions; nextCursor optional - jira: list_projects expand=lead so lead outputs populate (was always null) - trello: get_actions limit now applies to the card path too - asana: add missing 'completed' + 'projects' subBlocks (were unsettable in UI); request permalink_url via opt_fields on create routes --- .../docs/en/integrations/google_docs.mdx | 20 ++++++------- .../api/tools/asana/create-project/route.ts | 21 ++++++++------ .../api/tools/asana/create-subtask/route.ts | 2 +- .../app/api/tools/asana/create-task/route.ts | 3 +- apps/sim/blocks/blocks/asana.ts | 22 ++++++++++++++ apps/sim/blocks/blocks/google_docs.ts | 1 + apps/sim/blocks/blocks/slack.ts | 3 ++ apps/sim/lib/oauth/utils.ts | 3 ++ .../tools/google_docs/create-named-range.ts | 2 +- .../google_docs/create-paragraph-bullets.ts | 2 +- .../tools/google_docs/delete-content-range.ts | 2 +- .../google_docs/delete-paragraph-bullets.ts | 2 +- apps/sim/tools/google_docs/insert-image.ts | 2 +- .../tools/google_docs/insert-page-break.ts | 2 +- apps/sim/tools/google_docs/insert-table.ts | 2 +- apps/sim/tools/google_docs/insert-text.ts | 2 +- .../google_docs/update-paragraph-style.ts | 20 ++++++++++--- .../tools/google_docs/update-text-style.ts | 2 +- apps/sim/tools/jira/list_projects.ts | 2 ++ apps/sim/tools/monday/search_items.ts | 29 +++++++++++++++---- .../tools/slack/list_scheduled_messages.ts | 1 + apps/sim/tools/trello/get_actions.ts | 2 +- 22 files changed, 106 insertions(+), 41 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/google_docs.mdx b/apps/docs/content/docs/en/integrations/google_docs.mdx index cc0963d0155..89694b6d6eb 100644 --- a/apps/docs/content/docs/en/integrations/google_docs.mdx +++ b/apps/docs/content/docs/en/integrations/google_docs.mdx @@ -110,7 +110,7 @@ Insert text at a specific index in a Google Docs document. When no index is prov | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert text into | | `text` | string | Yes | The text to insert | -| `index` | number | No | The 1-based character index at which to insert the text. When omitted, text is appended to the end of the document. | +| `index` | number | No | The zero-based character index at which to insert the text. When omitted, text is appended to the end of the document. | #### Output @@ -158,7 +158,7 @@ Insert an empty table with the given number of rows and columns into a Google Do | `documentId` | string | Yes | The ID of the document to insert the table into | | `rows` | number | Yes | The number of rows in the table | | `columns` | number | Yes | The number of columns in the table | -| `index` | number | No | The 1-based character index at which to insert the table. When omitted, the table is appended to the end of the document. | +| `index` | number | No | The zero-based character index at which to insert the table. When omitted, the table is appended to the end of the document. | #### Output @@ -181,7 +181,7 @@ Insert an inline image from a public URL into a Google Docs document. The image | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert the image into | | `imageUrl` | string | Yes | The publicly accessible URL of the image to insert | -| `index` | number | No | The 1-based character index at which to insert the image. When omitted, the image is appended to the end of the document. | +| `index` | number | No | The zero-based character index at which to insert the image. When omitted, the image is appended to the end of the document. | | `width` | number | No | Optional image width in points \(PT\) | | `height` | number | No | Optional image height in points \(PT\) | @@ -205,7 +205,7 @@ Insert a page break into a Google Docs document. When no index is provided, the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert the page break into | -| `index` | number | No | The 1-based character index at which to insert the page break. When omitted, the page break is appended to the end of the document. | +| `index` | number | No | The zero-based character index at which to insert the page break. When omitted, the page break is appended to the end of the document. | #### Output @@ -227,7 +227,7 @@ Apply bold, italic, underline, and/or font size to a range of text in a Google D | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The 1-based start character index of the range to style \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range to style \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to style \(exclusive\) | | `bold` | boolean | No | Whether to make the text bold | | `italic` | boolean | No | Whether to make the text italic | @@ -254,7 +254,7 @@ Apply a named paragraph style (such as a heading or title) and/or alignment to t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The 1-based start character index of the range to style \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range to style \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to style \(exclusive\) | | `namedStyleType` | string | No | The named paragraph style to apply. One of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6. | | `alignment` | string | No | The paragraph alignment to apply. One of: LEFT, CENTER, RIGHT, JUSTIFY. | @@ -279,7 +279,7 @@ Add bulleted or numbered list formatting to the paragraphs overlapping a range o | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The 1-based start character index of the range to bullet \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range to bullet \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to bullet \(exclusive\) | | `bulletPreset` | string | No | The bullet glyph preset to apply. Defaults to BULLET_DISC_CIRCLE_SQUARE. Examples: BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED. | @@ -303,7 +303,7 @@ Remove bullet or numbered list formatting from the paragraphs overlapping a rang | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The 1-based start character index of the range to clear bullets from \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range to clear bullets from \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to clear bullets from \(exclusive\) | #### Output @@ -326,7 +326,7 @@ Delete all content between a start and end character index in a Google Docs docu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to delete content from | -| `startIndex` | number | Yes | The 1-based start character index of the range to delete \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range to delete \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to delete \(exclusive\) | #### Output @@ -350,7 +350,7 @@ Create a named range over a span of content in a Google Docs document so it can | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | | `name` | string | Yes | The name of the range to create \(1-256 characters\) | -| `startIndex` | number | Yes | The 1-based start character index of the range \(inclusive\) | +| `startIndex` | number | Yes | The zero-based start character index of the range \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range \(exclusive\) | #### Output diff --git a/apps/sim/app/api/tools/asana/create-project/route.ts b/apps/sim/app/api/tools/asana/create-project/route.ts index c0632307d2d..1af9133f375 100644 --- a/apps/sim/app/api/tools/asana/create-project/route.ts +++ b/apps/sim/app/api/tools/asana/create-project/route.ts @@ -32,15 +32,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => { projectData.notes = notes } - const response = await fetch('https://app.asana.com/api/1.0/projects', { - method: 'POST', - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: projectData }), - }) + const response = await fetch( + 'https://app.asana.com/api/1.0/projects?opt_fields=name,notes,archived,color,created_at,modified_at,permalink_url', + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ data: projectData }), + } + ) if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/app/api/tools/asana/create-subtask/route.ts b/apps/sim/app/api/tools/asana/create-subtask/route.ts index 6f5df17ed97..a1ce4676a07 100644 --- a/apps/sim/app/api/tools/asana/create-subtask/route.ts +++ b/apps/sim/app/api/tools/asana/create-subtask/route.ts @@ -38,7 +38,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { subtaskData.due_on = due_on } - const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/subtasks` + const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/subtasks?opt_fields=name,notes,completed,created_at,permalink_url` const response = await fetch(url, { method: 'POST', diff --git a/apps/sim/app/api/tools/asana/create-task/route.ts b/apps/sim/app/api/tools/asana/create-task/route.ts index df188ea652c..39521ca9d04 100644 --- a/apps/sim/app/api/tools/asana/create-task/route.ts +++ b/apps/sim/app/api/tools/asana/create-task/route.ts @@ -27,7 +27,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ error: workspaceValidation.error }, { status: 400 }) } - const url = 'https://app.asana.com/api/1.0/tasks' + const url = + 'https://app.asana.com/api/1.0/tasks?opt_fields=name,notes,completed,created_at,permalink_url' const taskData: Record = { name, diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index bd2ff64c1a8..bd3f2b46874 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -357,6 +357,28 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n value: ['add_followers'], }, }, + { + id: 'projects', + title: 'Projects', + type: 'short-input', + placeholder: 'Comma-separated project GIDs to filter by', + mode: 'advanced', + condition: { + field: 'operation', + value: ['search_tasks'], + }, + }, + { + id: 'completed', + title: 'Completion', + type: 'checkbox-list', + options: [{ label: 'Completed', id: 'completed' }], + mode: 'advanced', + condition: { + field: 'operation', + value: ['update_task', 'search_tasks'], + }, + }, ], tools: { access: [ diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index 7243937d258..578ec180b0a 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -362,6 +362,7 @@ Return ONLY the text to insert - no explanations, no extra text.`, title: 'Paragraph Style', type: 'dropdown', options: [ + { label: 'Default (unchanged)', id: '' }, { label: 'Normal Text', id: 'NORMAL_TEXT' }, { label: 'Title', id: 'TITLE' }, { label: 'Subtitle', id: 'SUBTITLE' }, diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index fa54796bd78..78dc74e72d8 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -1686,6 +1686,9 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te if (isDM && dmSupportedOperations.includes(operation)) { baseParams.userId = effectiveUserId + } else if (isDM && operation === 'schedule_message' && effectiveUserId) { + // chat.scheduleMessage opens a DM when the channel is set to a user ID + baseParams.channel = effectiveUserId } else if (effectiveChannel) { baseParams.channel = effectiveChannel } diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 28276bc28fd..17ff65e51fb 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -277,8 +277,10 @@ export const SCOPE_DESCRIPTIONS: Record = { // Slack scopes 'channels:read': 'View public channels', 'channels:history': 'Read channel messages', + 'channels:manage': 'Create, archive, and rename public channels', 'groups:read': 'View private channels', 'groups:history': 'Read private messages', + 'groups:write': 'Create, archive, and manage private channels', 'chat:write': 'Send messages', 'chat:write.public': 'Post to public channels', 'assistant:write': 'Set assistant thread status, title, and suggested prompts', @@ -292,6 +294,7 @@ export const SCOPE_DESCRIPTIONS: Record = { 'canvases:read': 'Read canvas sections', 'canvases:write': 'Create, edit, and delete canvas documents', 'reactions:write': 'Add emoji reactions to messages', + 'reactions:read': 'View emoji reactions on messages', // Webflow scopes 'sites:read': 'View Webflow sites', diff --git a/apps/sim/tools/google_docs/create-named-range.ts b/apps/sim/tools/google_docs/create-named-range.ts index 566520ada3a..b8fd17df4a5 100644 --- a/apps/sim/tools/google_docs/create-named-range.ts +++ b/apps/sim/tools/google_docs/create-named-range.ts @@ -45,7 +45,7 @@ export const createNamedRangeTool: ToolConfig< type: 'number', required: true, visibility: 'user-or-llm', - description: 'The 1-based start character index of the range (inclusive)', + description: 'The zero-based start character index of the range (inclusive)', }, endIndex: { type: 'number', diff --git a/apps/sim/tools/google_docs/create-paragraph-bullets.ts b/apps/sim/tools/google_docs/create-paragraph-bullets.ts index 6d42cc8d76c..c3e1ecc3105 100644 --- a/apps/sim/tools/google_docs/create-paragraph-bullets.ts +++ b/apps/sim/tools/google_docs/create-paragraph-bullets.ts @@ -56,7 +56,7 @@ export const createParagraphBulletsTool: ToolConfig< type: 'number', required: true, visibility: 'user-or-llm', - description: 'The 1-based start character index of the range to bullet (inclusive)', + description: 'The zero-based start character index of the range to bullet (inclusive)', }, endIndex: { type: 'number', diff --git a/apps/sim/tools/google_docs/delete-content-range.ts b/apps/sim/tools/google_docs/delete-content-range.ts index 03dd0e58659..32fe6fda689 100644 --- a/apps/sim/tools/google_docs/delete-content-range.ts +++ b/apps/sim/tools/google_docs/delete-content-range.ts @@ -39,7 +39,7 @@ export const deleteContentRangeTool: ToolConfig< type: 'number', required: true, visibility: 'user-or-llm', - description: 'The 1-based start character index of the range to delete (inclusive)', + description: 'The zero-based start character index of the range to delete (inclusive)', }, endIndex: { type: 'number', diff --git a/apps/sim/tools/google_docs/delete-paragraph-bullets.ts b/apps/sim/tools/google_docs/delete-paragraph-bullets.ts index 2085aabb5a3..b27c4519287 100644 --- a/apps/sim/tools/google_docs/delete-paragraph-bullets.ts +++ b/apps/sim/tools/google_docs/delete-paragraph-bullets.ts @@ -40,7 +40,7 @@ export const deleteParagraphBulletsTool: ToolConfig< required: true, visibility: 'user-or-llm', description: - 'The 1-based start character index of the range to clear bullets from (inclusive)', + 'The zero-based start character index of the range to clear bullets from (inclusive)', }, endIndex: { type: 'number', diff --git a/apps/sim/tools/google_docs/insert-image.ts b/apps/sim/tools/google_docs/insert-image.ts index 82818e49e90..5327b0b9889 100644 --- a/apps/sim/tools/google_docs/insert-image.ts +++ b/apps/sim/tools/google_docs/insert-image.ts @@ -40,7 +40,7 @@ export const insertImageTool: ToolConfig = { + LEFT: 'START', + START: 'START', + CENTER: 'CENTER', + RIGHT: 'END', + END: 'END', + JUSTIFY: 'JUSTIFIED', + JUSTIFIED: 'JUSTIFIED', +} export const updateParagraphStyleTool: ToolConfig< GoogleDocsToolParams, @@ -53,7 +65,7 @@ export const updateParagraphStyleTool: ToolConfig< type: 'number', required: true, visibility: 'user-or-llm', - description: 'The 1-based start character index of the range to style (inclusive)', + description: 'The zero-based start character index of the range to style (inclusive)', }, endIndex: { type: 'number', @@ -108,8 +120,8 @@ export const updateParagraphStyleTool: ToolConfig< } if (params.alignment != null && String(params.alignment).trim() !== '') { - const alignment = String(params.alignment).trim().toUpperCase() - if (!ALIGNMENTS.has(alignment)) { + const alignment = ALIGNMENT_MAP[String(params.alignment).trim().toUpperCase()] + if (!alignment) { throw new Error('alignment must be one of: LEFT, CENTER, RIGHT, JUSTIFY') } paragraphStyle.alignment = alignment diff --git a/apps/sim/tools/google_docs/update-text-style.ts b/apps/sim/tools/google_docs/update-text-style.ts index 435737659d4..b9b1434c3e6 100644 --- a/apps/sim/tools/google_docs/update-text-style.ts +++ b/apps/sim/tools/google_docs/update-text-style.ts @@ -39,7 +39,7 @@ export const updateTextStyleTool: ToolConfig< type: 'number', required: true, visibility: 'user-or-llm', - description: 'The 1-based start character index of the range to style (inclusive)', + description: 'The zero-based start character index of the range to style (inclusive)', }, endIndex: { type: 'number', diff --git a/apps/sim/tools/jira/list_projects.ts b/apps/sim/tools/jira/list_projects.ts index 75f42366110..fd892bdf46a 100644 --- a/apps/sim/tools/jira/list_projects.ts +++ b/apps/sim/tools/jira/list_projects.ts @@ -23,6 +23,8 @@ function transformProject(project: any) { function buildSearchUrl(cloudId: string, params: JiraListProjectsParams): string { const queryParams = new URLSearchParams() + // `lead` is not returned by default on project/search — expand it so the lead outputs populate. + queryParams.append('expand', 'lead') if (params.query) queryParams.append('query', params.query) if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt)) if (params.maxResults !== undefined) queryParams.append('maxResults', String(params.maxResults)) diff --git a/apps/sim/tools/monday/search_items.ts b/apps/sim/tools/monday/search_items.ts index 0349fe56150..c1e2953e227 100644 --- a/apps/sim/tools/monday/search_items.ts +++ b/apps/sim/tools/monday/search_items.ts @@ -66,19 +66,36 @@ export const mondaySearchItemsTool: ToolConfig { + const column = entry as { column_id?: unknown; column_values?: unknown } + const columnId = JSON.stringify(String(column.column_id ?? '')) + const rawValues = Array.isArray(column.column_values) + ? column.column_values + : [column.column_values] + const columnValues = JSON.stringify(rawValues.map((value) => String(value))) + return `{ column_id: ${columnId}, column_values: ${columnValues} }` + }) + .join(', ')}]` 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 } } }`, + query: `query { items_page_by_column_values(limit: ${limit}, board_id: ${boardId}, columns: ${columnsLiteral}) { cursor items { id name state board { id } group { id title } column_values { id text value type } created_at updated_at url } } }`, } }, }, diff --git a/apps/sim/tools/slack/list_scheduled_messages.ts b/apps/sim/tools/slack/list_scheduled_messages.ts index 5c2a067d30f..81fb8d04529 100644 --- a/apps/sim/tools/slack/list_scheduled_messages.ts +++ b/apps/sim/tools/slack/list_scheduled_messages.ts @@ -139,6 +139,7 @@ export const slackListScheduledMessagesTool: ToolConfig< nextCursor: { type: 'string', description: 'Cursor for the next page (null when there are no more pages)', + optional: true, }, }, } diff --git a/apps/sim/tools/trello/get_actions.ts b/apps/sim/tools/trello/get_actions.ts index f660f289587..00857a5bede 100644 --- a/apps/sim/tools/trello/get_actions.ts +++ b/apps/sim/tools/trello/get_actions.ts @@ -84,7 +84,7 @@ export const trelloGetActionsTool: ToolConfig Date: Tue, 30 Jun 2026 10:58:00 -0700 Subject: [PATCH 4/7] fix(integrations): clamp context.dev search bounds; precise Google Docs index wording - context_dev/search: clamp numResults to the documented 10-100 range; normalize country to trimmed uppercase - google_docs: replace ambiguous '1-based'/'zero-based' index wording with the concrete fact (the document body starts at index 1), matching buildInsertLocation (index<1 appends) and buildContentRange --- apps/sim/tools/context_dev/search.ts | 10 ++++++++-- apps/sim/tools/google_docs/create-named-range.ts | 3 ++- apps/sim/tools/google_docs/create-paragraph-bullets.ts | 3 ++- apps/sim/tools/google_docs/delete-content-range.ts | 3 ++- apps/sim/tools/google_docs/delete-paragraph-bullets.ts | 2 +- apps/sim/tools/google_docs/insert-image.ts | 2 +- apps/sim/tools/google_docs/insert-page-break.ts | 2 +- apps/sim/tools/google_docs/insert-table.ts | 2 +- apps/sim/tools/google_docs/insert-text.ts | 2 +- apps/sim/tools/google_docs/update-paragraph-style.ts | 3 ++- apps/sim/tools/google_docs/update-text-style.ts | 3 ++- 11 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/sim/tools/context_dev/search.ts b/apps/sim/tools/context_dev/search.ts index 575dbbe32a7..3d9792e9d9d 100644 --- a/apps/sim/tools/context_dev/search.ts +++ b/apps/sim/tools/context_dev/search.ts @@ -87,8 +87,14 @@ export const contextDevSearchTool: ToolConfig Date: Tue, 30 Jun 2026 11:04:25 -0700 Subject: [PATCH 5/7] fix(integrations): validate context.dev country (ISO-2); regenerate google_docs docs - context_dev/search: reject non-2-letter country values with a clear error instead of forwarding them - docs: regenerate google_docs.mdx so the public index-contract wording matches the updated tool descriptions (body starts at index 1) --- .../docs/en/integrations/google_docs.mdx | 20 +++++++++---------- apps/sim/tools/context_dev/search.ts | 8 +++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/google_docs.mdx b/apps/docs/content/docs/en/integrations/google_docs.mdx index 89694b6d6eb..1b8c137a1b7 100644 --- a/apps/docs/content/docs/en/integrations/google_docs.mdx +++ b/apps/docs/content/docs/en/integrations/google_docs.mdx @@ -110,7 +110,7 @@ Insert text at a specific index in a Google Docs document. When no index is prov | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert text into | | `text` | string | Yes | The text to insert | -| `index` | number | No | The zero-based character index at which to insert the text. When omitted, text is appended to the end of the document. | +| `index` | number | No | The character index \(the document body starts at index 1\) at which to insert the text. When omitted, text is appended to the end of the document. | #### Output @@ -158,7 +158,7 @@ Insert an empty table with the given number of rows and columns into a Google Do | `documentId` | string | Yes | The ID of the document to insert the table into | | `rows` | number | Yes | The number of rows in the table | | `columns` | number | Yes | The number of columns in the table | -| `index` | number | No | The zero-based character index at which to insert the table. When omitted, the table is appended to the end of the document. | +| `index` | number | No | The character index \(the document body starts at index 1\) at which to insert the table. When omitted, the table is appended to the end of the document. | #### Output @@ -181,7 +181,7 @@ Insert an inline image from a public URL into a Google Docs document. The image | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert the image into | | `imageUrl` | string | Yes | The publicly accessible URL of the image to insert | -| `index` | number | No | The zero-based character index at which to insert the image. When omitted, the image is appended to the end of the document. | +| `index` | number | No | The character index \(the document body starts at index 1\) at which to insert the image. When omitted, the image is appended to the end of the document. | | `width` | number | No | Optional image width in points \(PT\) | | `height` | number | No | Optional image height in points \(PT\) | @@ -205,7 +205,7 @@ Insert a page break into a Google Docs document. When no index is provided, the | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to insert the page break into | -| `index` | number | No | The zero-based character index at which to insert the page break. When omitted, the page break is appended to the end of the document. | +| `index` | number | No | The character index \(the document body starts at index 1\) at which to insert the page break. When omitted, the page break is appended to the end of the document. | #### Output @@ -227,7 +227,7 @@ Apply bold, italic, underline, and/or font size to a range of text in a Google D | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The zero-based start character index of the range to style \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range to style \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to style \(exclusive\) | | `bold` | boolean | No | Whether to make the text bold | | `italic` | boolean | No | Whether to make the text italic | @@ -254,7 +254,7 @@ Apply a named paragraph style (such as a heading or title) and/or alignment to t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The zero-based start character index of the range to style \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range to style \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to style \(exclusive\) | | `namedStyleType` | string | No | The named paragraph style to apply. One of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6. | | `alignment` | string | No | The paragraph alignment to apply. One of: LEFT, CENTER, RIGHT, JUSTIFY. | @@ -279,7 +279,7 @@ Add bulleted or numbered list formatting to the paragraphs overlapping a range o | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The zero-based start character index of the range to bullet \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range to bullet \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to bullet \(exclusive\) | | `bulletPreset` | string | No | The bullet glyph preset to apply. Defaults to BULLET_DISC_CIRCLE_SQUARE. Examples: BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED. | @@ -303,7 +303,7 @@ Remove bullet or numbered list formatting from the paragraphs overlapping a rang | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | -| `startIndex` | number | Yes | The zero-based start character index of the range to clear bullets from \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range to clear bullets from \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to clear bullets from \(exclusive\) | #### Output @@ -326,7 +326,7 @@ Delete all content between a start and end character index in a Google Docs docu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to delete content from | -| `startIndex` | number | Yes | The zero-based start character index of the range to delete \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range to delete \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range to delete \(exclusive\) | #### Output @@ -350,7 +350,7 @@ Create a named range over a span of content in a Google Docs document so it can | --------- | ---- | -------- | ----------- | | `documentId` | string | Yes | The ID of the document to update | | `name` | string | Yes | The name of the range to create \(1-256 characters\) | -| `startIndex` | number | Yes | The zero-based start character index of the range \(inclusive\) | +| `startIndex` | number | Yes | The start character index \(the document body starts at index 1\) of the range \(inclusive\) | | `endIndex` | number | Yes | The end character index of the range \(exclusive\) | #### Output diff --git a/apps/sim/tools/context_dev/search.ts b/apps/sim/tools/context_dev/search.ts index 3d9792e9d9d..debfa0b546b 100644 --- a/apps/sim/tools/context_dev/search.ts +++ b/apps/sim/tools/context_dev/search.ts @@ -94,7 +94,13 @@ export const contextDevSearchTool: ToolConfig Date: Tue, 30 Jun 2026 11:17:29 -0700 Subject: [PATCH 6/7] fix(asana): omit completed unless explicitly set (don't send false on unchecked) The new completion checkbox mapped an unchecked/untouched state to completed:false, which made update_task silently un-complete tasks and search_tasks filter to incomplete. Now only sends completed when the box is checked (undefined otherwise). --- apps/sim/blocks/blocks/asana.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index bd3f2b46874..e7a3ef9ed34 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -442,6 +442,15 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n .filter((p: string) => p.length > 0) : undefined + // Only send a completion value when the user actually checked the box; an + // empty/untouched checkbox must omit the field (not send `false`), so + // update_task doesn't silently un-complete a task and search_tasks doesn't + // implicitly filter to incomplete tasks. + const completedValue = + Array.isArray(params.completed) && params.completed.length > 0 + ? params.completed.includes('completed') + : undefined + const baseParams = { accessToken: oauthCredential?.accessToken, } @@ -471,7 +480,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n name: params.name, notes: params.notes, assignee: params.assignee, - completed: params.completed?.includes('completed'), + completed: completedValue, due_on: params.due_on, } case 'get_projects': @@ -486,7 +495,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n text: params.searchText, assignee: params.assignee, projects: projectsArray, - completed: params.completed?.includes('completed'), + completed: completedValue, } case 'add_comment': return { From 199540f93a52ee5d46d8ab095d365b024ced95ea Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 11:29:12 -0700 Subject: [PATCH 7/7] fix(slack): expose Destination toggle for schedule_message so DM scheduling is reachable The mapper already routes schedule_message DMs (user-id-as-channel); add schedule_message to the destinationType condition so users can deliberately choose Channel vs DM instead of it only triggering via leftover state. --- apps/sim/blocks/blocks/slack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 78dc74e72d8..ae3da995b79 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -94,7 +94,7 @@ export const SlackBlock: BlockConfig = { value: () => 'channel', condition: { field: 'operation', - value: ['send', 'read'], + value: ['send', 'read', 'schedule_message'], }, }, {