diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4bfdcfe76c2..165a6db7556 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -5730,6 +5730,31 @@ export function McpIcon(props: SVGProps) { ) } +export function A2AIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function WordpressIcon(props: SVGProps) { return ( diff --git a/apps/docs/content/docs/en/integrations/airtable.mdx b/apps/docs/content/docs/en/integrations/airtable.mdx index e501d9b6498..63314699c05 100644 --- a/apps/docs/content/docs/en/integrations/airtable.mdx +++ b/apps/docs/content/docs/en/integrations/airtable.mdx @@ -26,7 +26,7 @@ In Sim, the Airtable integration enables your agents to interact with your Airta ## Usage Instructions -Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table. +Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, update, upsert, or delete records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table. @@ -203,6 +203,58 @@ Update multiple existing records in an Airtable table | ↳ `recordCount` | number | Number of records updated | | ↳ `updatedRecordIds` | array | List of updated record IDs | +### `airtable_upsert_records` + +Update existing records or create new ones in an Airtable table, matching on the specified merge fields + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) | +| `tableId` | string | Yes | Table ID \(starts with "tbl"\) or table name | +| `records` | json | Yes | Array of records to upsert, each with a `fields` object | +| `fieldsToMergeOn` | json | Yes | Array of field names used to match existing records \(max 3\). A record is updated when all merge fields match, otherwise it is created. Example: \["Name"\] | +| `typecast` | boolean | No | When true, Airtable automatically converts string values to the field type | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `records` | array | Array of upserted Airtable records | +| ↳ `id` | string | Record ID | +| ↳ `createdTime` | string | Record creation timestamp | +| ↳ `fields` | json | Record field values | +| `createdRecords` | array | IDs of records that were created | +| `updatedRecords` | array | IDs of records that were updated | +| `metadata` | json | Operation metadata | +| ↳ `recordCount` | number | Total number of records returned | +| ↳ `createdCount` | number | Number of records created | +| ↳ `updatedCount` | number | Number of records updated | + +### `airtable_delete_records` + +Delete one or more records from an Airtable table by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) | +| `tableId` | string | Yes | Table ID \(starts with "tbl"\) or table name | +| `recordIds` | json | Yes | Array of record IDs to delete \(each starts with "rec", e.g., \["recXXXXXXXXXXXXXX"\]\). Pass a single-element array to delete one record. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `records` | array | Array of deleted Airtable records | +| ↳ `id` | string | Record ID | +| ↳ `deleted` | boolean | Whether the record was deleted | +| `metadata` | json | Operation metadata | +| ↳ `recordCount` | number | Number of records deleted | +| ↳ `deletedRecordIds` | array | List of deleted record IDs | + ### `airtable_get_base_schema` Get the schema of all tables, fields, and views in an Airtable base diff --git a/apps/docs/content/docs/en/integrations/clerk.mdx b/apps/docs/content/docs/en/integrations/clerk.mdx index f912d0e1f4e..7a9df1a41fd 100644 --- a/apps/docs/content/docs/en/integrations/clerk.mdx +++ b/apps/docs/content/docs/en/integrations/clerk.mdx @@ -440,3 +440,207 @@ Revoke a session to immediately invalidate it | `success` | boolean | Operation success status | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Clerk Organization Created + +Trigger workflow when a Clerk organization is created + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `organizationId` | string | Clerk organization ID \(data.id\) | +| `name` | string | Organization name \(data.name\) | +| `slug` | string | Organization slug \(data.slug\) | +| `createdBy` | string | User ID of the creator \(data.created_by\) | +| `membersCount` | number | Number of members \(data.members_count\) | +| `maxAllowedMemberships` | number | Maximum allowed memberships \(data.max_allowed_memberships\) | +| `createdAt` | number | Organization creation timestamp \(data.created_at\) | + + +--- + +### Clerk Organization Membership Created + +Trigger workflow when a Clerk organization membership is created + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `membershipId` | string | Membership ID \(data.id\) | +| `role` | string | Membership role, e.g. org:admin \(data.role\) | +| `organizationId` | string | Organization ID \(data.organization.id\) | +| `userId` | string | User ID of the member \(data.public_user_data.user_id\) | +| `createdAt` | number | Membership creation timestamp \(data.created_at\) | + + +--- + +### Clerk Session Created + +Trigger workflow when a Clerk session is created + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `sessionId` | string | Clerk session ID \(data.id\) | +| `userId` | string | User the session belongs to \(data.user_id\) | +| `clientId` | string | Client ID for the session \(data.client_id\) | +| `status` | string | Session status \(data.status\) | +| `createdAt` | number | Session creation timestamp \(data.created_at\) | + + +--- + +### Clerk User Created + +Trigger workflow when a Clerk user is created + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `userId` | string | Clerk user ID \(data.id\) | +| `firstName` | string | User's first name | +| `lastName` | string | User's last name | +| `username` | string | User's username | +| `imageUrl` | string | Profile image URL | +| `primaryEmailAddressId` | string | Primary email address ID | +| `emailAddresses` | json | Array of email address objects | +| `phoneNumbers` | json | Array of phone number objects | +| `externalId` | string | External system ID linked to the user | +| `createdAt` | number | User creation timestamp \(data.created_at\) | +| `updatedAt` | number | User last update timestamp \(data.updated_at\) | + + +--- + +### Clerk User Deleted + +Trigger workflow when a Clerk user is deleted + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `userId` | string | Deleted Clerk user ID \(data.id\) | +| `deleted` | boolean | Whether the user was deleted \(data.deleted\) | + + +--- + +### Clerk User Updated + +Trigger workflow when a Clerk user is updated + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | +| `userId` | string | Clerk user ID \(data.id\) | +| `firstName` | string | User's first name | +| `lastName` | string | User's last name | +| `username` | string | User's username | +| `imageUrl` | string | Profile image URL | +| `primaryEmailAddressId` | string | Primary email address ID | +| `emailAddresses` | json | Array of email address objects | +| `phoneNumbers` | json | Array of phone number objects | +| `externalId` | string | External system ID linked to the user | +| `createdAt` | number | User creation timestamp \(data.created_at\) | +| `updatedAt` | number | User last update timestamp \(data.updated_at\) | + + +--- + +### Clerk Webhook + +Trigger workflow on any Clerk webhook event + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Copy this from your Clerk webhook endpoint to verify event signatures. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., user.created, session.created\) | +| `object` | string | Always "event" | +| `timestamp` | number | Timestamp in milliseconds when the event occurred | +| `instance_id` | string | Identifier of your Clerk instance | +| `data` | json | Raw event `data` object \(shape varies by event type\) | + diff --git a/apps/docs/content/docs/en/integrations/google_docs.mdx b/apps/docs/content/docs/en/integrations/google_docs.mdx index e666fcc2ad5..9dc591ccb0a 100644 --- a/apps/docs/content/docs/en/integrations/google_docs.mdx +++ b/apps/docs/content/docs/en/integrations/google_docs.mdx @@ -1,6 +1,6 @@ --- title: Google Docs -description: Read, write, and create documents +description: Read, write, create, and edit documents --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -27,7 +27,7 @@ In Sim, the Google Docs integration allows your agents to read document content, ## Usage Instructions -Integrate Google Docs into the workflow. Can read, write, and create documents. +Integrate Google Docs into the workflow. Read, write, and create documents, insert text, tables, images, and page breaks, find and replace text, and apply text styling. @@ -100,4 +100,149 @@ Create a new Google Docs document | ↳ `mimeType` | string | Document MIME type | | ↳ `url` | string | Document URL | +### `google_docs_insert_text` + +Insert text at a specific index in a Google Docs document. When no index is provided, text is appended to the end of the document. Text is inserted literally; Markdown is not interpreted. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if text was inserted 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_replace_text` + +Replace all occurrences of a search string with new text across a Google Docs document. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `documentId` | string | Yes | The ID of the document to update | +| `searchText` | string | Yes | The text to find | +| `replaceText` | string | No | The text to replace matches with. Use an empty string to delete matches. | +| `matchCase` | boolean | No | Whether the search should be case sensitive. Defaults to false. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `occurrencesChanged` | number | The number of occurrences that were replaced | +| `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_insert_table` + +Insert an empty table with the given number of rows and columns into a Google Docs document. When no index is provided, the table is appended to the end of the document. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the table was inserted 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_insert_image` + +Insert an inline image from a public URL into a Google Docs document. The image must be publicly accessible and under 50 MB. When no index is provided, the image is appended to the end of the document. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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. | +| `width` | number | No | Optional image width in points \(PT\) | +| `height` | number | No | Optional image height in points \(PT\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `objectId` | string | The ID of the inserted inline image object | +| `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_insert_page_break` + +Insert a page break into a Google Docs document. When no index is provided, the page break is appended to the end of the document. + +#### Input + +| 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. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the page break was inserted 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_update_text_style` + +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. + +#### 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\) | +| `bold` | boolean | No | Whether to make the text bold | +| `italic` | boolean | No | Whether to make the text italic | +| `underline` | boolean | No | Whether to underline the text | +| `fontSize` | number | No | The font size to apply, in points \(PT\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updatedContent` | boolean | Indicates if the text 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 | + diff --git a/apps/docs/content/docs/en/integrations/incidentio.mdx b/apps/docs/content/docs/en/integrations/incidentio.mdx index 919f8cf00f0..da6c2dce200 100644 --- a/apps/docs/content/docs/en/integrations/incidentio.mdx +++ b/apps/docs/content/docs/en/integrations/incidentio.mdx @@ -1388,3 +1388,141 @@ Delete an escalation path in incident.io | `message` | string | Success message | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### incident.io Alert Created + +Trigger workflow when an alert is created in incident.io + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | The signing secret from your incident.io webhook endpoint. Used to verify events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event_type` | string | incident.io event type \(e.g., public_incident.incident_created_v2\). Top-level `event_type` field. | +| `payload` | json | Full raw webhook body as delivered by incident.io \(the entire Svix envelope\). | +| `alert` | json | The full alert object from the webhook payload. | +| `alert_id` | string | Unique alert ID. | +| `title` | string | Alert title. | +| `description` | string | Alert description, when set. | +| `status` | string | Alert status \(e.g., firing, resolved\). | +| `alert_source_id` | string | ID of the alert source that raised the alert. | +| `deduplication_key` | string | Deduplication key for the alert, when set. | +| `source_url` | string | URL to the alert in the originating system, when set. | +| `created_at` | string | ISO 8601 timestamp when the alert was created. | +| `updated_at` | string | ISO 8601 timestamp when the alert was last updated. | +| `resolved_at` | string | ISO 8601 timestamp when the alert was resolved, when applicable. | + + +--- + +### incident.io Incident Created + +Trigger workflow when an incident is created in incident.io + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | The signing secret from your incident.io webhook endpoint. Used to verify events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event_type` | string | incident.io event type \(e.g., public_incident.incident_created_v2\). Top-level `event_type` field. | +| `payload` | json | Full raw webhook body as delivered by incident.io \(the entire Svix envelope\). | +| `incident` | json | The full incident object from the webhook payload. | +| `incident_id` | string | Unique incident ID \(e.g., 01FDAG4SAP5TYPT98WGR2N7W91\). | +| `name` | string | Incident name. | +| `reference` | string | Human-readable incident reference \(e.g., INC-123\). | +| `summary` | string | Incident summary, when set. | +| `incident_status` | json | The incident status object \(id, name, category, rank\). | +| `severity` | json | The incident severity object \(id, name, rank\), when set. | +| `mode` | string | Incident mode \(standard, retrospective, test, tutorial, stream\). | +| `visibility` | string | Incident visibility \(public or private\). | +| `permalink` | string | Link to the incident in incident.io, when present. | +| `created_at` | string | ISO 8601 timestamp when the incident was created. | +| `updated_at` | string | ISO 8601 timestamp when the incident was last updated. | +| `new_status` | json | New status object \(status-updated events only; null otherwise\). | +| `previous_status` | json | Previous status object \(status-updated events only; null otherwise\). | +| `update_message` | string | Update message accompanying a status change \(status-updated events only; null otherwise\). | + + +--- + +### incident.io Incident Status Updated + +Trigger workflow when an incident + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | The signing secret from your incident.io webhook endpoint. Used to verify events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event_type` | string | incident.io event type \(e.g., public_incident.incident_created_v2\). Top-level `event_type` field. | +| `payload` | json | Full raw webhook body as delivered by incident.io \(the entire Svix envelope\). | +| `incident` | json | The full incident object from the webhook payload. | +| `incident_id` | string | Unique incident ID \(e.g., 01FDAG4SAP5TYPT98WGR2N7W91\). | +| `name` | string | Incident name. | +| `reference` | string | Human-readable incident reference \(e.g., INC-123\). | +| `summary` | string | Incident summary, when set. | +| `incident_status` | json | The incident status object \(id, name, category, rank\). | +| `severity` | json | The incident severity object \(id, name, rank\), when set. | +| `mode` | string | Incident mode \(standard, retrospective, test, tutorial, stream\). | +| `visibility` | string | Incident visibility \(public or private\). | +| `permalink` | string | Link to the incident in incident.io, when present. | +| `created_at` | string | ISO 8601 timestamp when the incident was created. | +| `updated_at` | string | ISO 8601 timestamp when the incident was last updated. | +| `new_status` | json | New status object \(status-updated events only; null otherwise\). | +| `previous_status` | json | Previous status object \(status-updated events only; null otherwise\). | +| `update_message` | string | Update message accompanying a status change \(status-updated events only; null otherwise\). | + + +--- + +### incident.io Incident Updated + +Trigger workflow when an incident is updated in incident.io + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | The signing secret from your incident.io webhook endpoint. Used to verify events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event_type` | string | incident.io event type \(e.g., public_incident.incident_created_v2\). Top-level `event_type` field. | +| `payload` | json | Full raw webhook body as delivered by incident.io \(the entire Svix envelope\). | +| `incident` | json | The full incident object from the webhook payload. | +| `incident_id` | string | Unique incident ID \(e.g., 01FDAG4SAP5TYPT98WGR2N7W91\). | +| `name` | string | Incident name. | +| `reference` | string | Human-readable incident reference \(e.g., INC-123\). | +| `summary` | string | Incident summary, when set. | +| `incident_status` | json | The incident status object \(id, name, category, rank\). | +| `severity` | json | The incident severity object \(id, name, rank\), when set. | +| `mode` | string | Incident mode \(standard, retrospective, test, tutorial, stream\). | +| `visibility` | string | Incident visibility \(public or private\). | +| `permalink` | string | Link to the incident in incident.io, when present. | +| `created_at` | string | ISO 8601 timestamp when the incident was created. | +| `updated_at` | string | ISO 8601 timestamp when the incident was last updated. | +| `new_status` | json | New status object \(status-updated events only; null otherwise\). | +| `previous_status` | json | Previous status object \(status-updated events only; null otherwise\). | +| `update_message` | string | Update message accompanying a status change \(status-updated events only; null otherwise\). | + diff --git a/apps/docs/content/docs/en/integrations/loops.mdx b/apps/docs/content/docs/en/integrations/loops.mdx index 98b1df23c2f..d46ec807111 100644 --- a/apps/docs/content/docs/en/integrations/loops.mdx +++ b/apps/docs/content/docs/en/integrations/loops.mdx @@ -271,3 +271,275 @@ Retrieve a list of contact properties from your Loops account. Returns each prop | ↳ `type` | string | The property data type \(string, number, boolean, date\) | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Loops Campaign Email Sent + +Trigger workflow when a Loops campaign email is sent + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., campaign.email.sent, loop.email.sent, transactional.email.sent\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `campaignId` | string | Campaign ID, present on campaign.email.sent | +| `campaignName` | string | Campaign name, present on campaign.email.sent | +| `loopId` | string | Loop \(workflow\) ID, present on loop.email.sent | +| `loopName` | string | Loop \(workflow\) name, present on loop.email.sent | +| `transactionalId` | string | Transactional email ID, present on transactional.email.sent | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | +| `mailingLists` | json | Mailing lists the send targeted \(id, name, description, isPublic\); present on campaign and loop sends | + + +--- + +### Loops Email Clicked + +Trigger workflow when a link in a Loops email is clicked + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., email.delivered, email.opened, email.clicked\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `sourceType` | string | Source of the email: "campaign", "loop", or "transactional" | +| `campaignId` | string | Campaign ID, present when sourceType is "campaign" | +| `loopId` | string | Loop \(workflow\) ID, present when sourceType is "loop" | +| `transactionalId` | string | Transactional email ID, present when sourceType is "transactional" | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | + + +--- + +### Loops Email Delivered + +Trigger workflow when a Loops email is delivered + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., email.delivered, email.opened, email.clicked\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `sourceType` | string | Source of the email: "campaign", "loop", or "transactional" | +| `campaignId` | string | Campaign ID, present when sourceType is "campaign" | +| `loopId` | string | Loop \(workflow\) ID, present when sourceType is "loop" | +| `transactionalId` | string | Transactional email ID, present when sourceType is "transactional" | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | + + +--- + +### Loops Email Hard Bounced + +Trigger workflow when a Loops email hard bounces + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., email.delivered, email.opened, email.clicked\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `sourceType` | string | Source of the email: "campaign", "loop", or "transactional" | +| `campaignId` | string | Campaign ID, present when sourceType is "campaign" | +| `loopId` | string | Loop \(workflow\) ID, present when sourceType is "loop" | +| `transactionalId` | string | Transactional email ID, present when sourceType is "transactional" | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | + + +--- + +### Loops Email Opened + +Trigger workflow when a Loops email is opened + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., email.delivered, email.opened, email.clicked\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `sourceType` | string | Source of the email: "campaign", "loop", or "transactional" | +| `campaignId` | string | Campaign ID, present when sourceType is "campaign" | +| `loopId` | string | Loop \(workflow\) ID, present when sourceType is "loop" | +| `transactionalId` | string | Transactional email ID, present when sourceType is "transactional" | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | + + +--- + +### Loops Email Soft Bounced + +Trigger workflow when a Loops email soft bounces + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., email.delivered, email.opened, email.clicked\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `sourceType` | string | Source of the email: "campaign", "loop", or "transactional" | +| `campaignId` | string | Campaign ID, present when sourceType is "campaign" | +| `loopId` | string | Loop \(workflow\) ID, present when sourceType is "loop" | +| `transactionalId` | string | Transactional email ID, present when sourceType is "transactional" | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | + + +--- + +### Loops Loop Email Sent + +Trigger workflow when a Loops loop email is sent + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., campaign.email.sent, loop.email.sent, transactional.email.sent\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `campaignId` | string | Campaign ID, present on campaign.email.sent | +| `campaignName` | string | Campaign name, present on campaign.email.sent | +| `loopId` | string | Loop \(workflow\) ID, present on loop.email.sent | +| `loopName` | string | Loop \(workflow\) name, present on loop.email.sent | +| `transactionalId` | string | Transactional email ID, present on transactional.email.sent | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | +| `mailingLists` | json | Mailing lists the send targeted \(id, name, description, isPublic\); present on campaign and loop sends | + + +--- + +### Loops Transactional Email Sent + +Trigger workflow when a Loops transactional email is sent + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signingSecret` | string | Yes | Required to verify the webhook signature from Loops. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventName` | string | Event type \(e.g., campaign.email.sent, loop.email.sent, transactional.email.sent\) | +| `eventTime` | number | Unix timestamp \(seconds\) when the event occurred | +| `webhookSchemaVersion` | string | Webhook schema version \(e.g., "1.0.0"\) | +| `campaignId` | string | Campaign ID, present on campaign.email.sent | +| `campaignName` | string | Campaign name, present on campaign.email.sent | +| `loopId` | string | Loop \(workflow\) ID, present on loop.email.sent | +| `loopName` | string | Loop \(workflow\) name, present on loop.email.sent | +| `transactionalId` | string | Transactional email ID, present on transactional.email.sent | +| `email` | json | Email object from the payload \(id, emailMessageId, subject\) | +| `emailId` | string | Unique email ID \(payload `email.id`\) | +| `emailMessageId` | string | Sent email message ID \(payload `email.emailMessageId`\) | +| `subject` | string | Email subject line \(payload `email.subject`\) | +| `contactIdentity` | json | Contact identity object from the payload \(id, email, userId\) | +| `contactId` | string | Contact ID \(payload `contactIdentity.id`\) | +| `contactEmail` | string | Contact email address \(payload `contactIdentity.email`\) | +| `userId` | string | Contact user ID, when set \(payload `contactIdentity.userId`\) | +| `mailingLists` | json | Mailing lists the send targeted \(id, name, description, isPublic\); present on campaign and loop sends | + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 4e6c5b255c2..f9f8e3a7c26 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -216,6 +216,7 @@ "tinybird", "trello", "trigger_dev", + "twilio", "twilio_sms", "twilio_voice", "typeform", diff --git a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx index 61b1158144c..818186cb31a 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_excel.mdx @@ -88,4 +88,145 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet | ↳ `spreadsheetId` | string | Microsoft Excel spreadsheet ID | | ↳ `spreadsheetUrl` | string | Spreadsheet URL | +### `microsoft_excel_clear_range` + +Clear the values and/or formatting of a range in a Microsoft Excel worksheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | +| `sheetName` | string | No | The name of the worksheet \(e.g., "Sheet1"\). If omitted, the range must use the combined "Sheet1!A1:B2" format. | +| `range` | string | Yes | The cell range to clear \(e.g., "A1:D10" or "Sheet1!A1:D10"\) | +| `applyTo` | string | No | What to clear: "All", "Formats", or "Contents". Defaults to "All". | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cleared` | boolean | Whether the range was cleared | +| `range` | string | The range that was cleared | +| `applyTo` | string | What was cleared \(All, Formats, or Contents\) | +| `metadata` | object | Spreadsheet metadata | +| ↳ `spreadsheetId` | string | The ID of the spreadsheet | +| ↳ `spreadsheetUrl` | string | URL to access the spreadsheet | + +### `microsoft_excel_format_range` + +Apply fill color and/or font formatting to a range in a Microsoft Excel worksheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | +| `sheetName` | string | No | The name of the worksheet \(e.g., "Sheet1"\). If omitted, the range must use the combined "Sheet1!A1:B2" format. | +| `range` | string | Yes | The cell range to format \(e.g., "A1:D10" or "Sheet1!A1:D10"\) | +| `fillColor` | string | No | Background fill color as an HTML hex code \(e.g., "#FFFF00"\). | +| `fontBold` | boolean | No | Whether the font is bold. | +| `fontItalic` | boolean | No | Whether the font is italic. | +| `fontColor` | string | No | Font color as an HTML hex code \(e.g., "#FF0000"\). | +| `fontSize` | number | No | Font size in points \(e.g., 12\). | +| `fontName` | string | No | Font name \(e.g., "Calibri"\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `formatted` | boolean | Whether the formatting was applied | +| `range` | string | The range that was formatted | +| `fill` | object | The applied fill, or null if no fill was set | +| ↳ `color` | string | The applied fill color | +| `font` | object | The applied font properties, or null if no font was set | +| ↳ `bold` | boolean | Whether the font is bold | +| ↳ `italic` | boolean | Whether the font is italic | +| ↳ `color` | string | The font color | +| ↳ `name` | string | The font name | +| ↳ `size` | number | The font size in points | +| `metadata` | object | Spreadsheet metadata | +| ↳ `spreadsheetId` | string | The ID of the spreadsheet | +| ↳ `spreadsheetUrl` | string | URL to access the spreadsheet | + +### `microsoft_excel_create_table` + +Create a new table over a range of cells in a Microsoft Excel workbook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | +| `address` | string | Yes | The range address for the table data source \(e.g., "Sheet1!A1:D5"\). If no sheet name is included, the active sheet is used. | +| `hasHeaders` | boolean | No | Whether the first row of the range contains column headers. Defaults to true. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `table` | object | Details of the newly created table | +| ↳ `id` | string | The unique ID of the table | +| ↳ `name` | string | The name of the table | +| ↳ `showHeaders` | boolean | Whether the header row is shown | +| ↳ `showTotals` | boolean | Whether the totals row is shown | +| ↳ `style` | string | The table style name | +| `metadata` | object | Spreadsheet metadata | +| ↳ `spreadsheetId` | string | The ID of the spreadsheet | +| ↳ `spreadsheetUrl` | string | URL to access the spreadsheet | + +### `microsoft_excel_delete_worksheet` + +Delete a worksheet (sheet) from a Microsoft Excel workbook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | +| `worksheetName` | string | Yes | The name of the worksheet to delete \(e.g., "Sheet1", "Old Data"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the worksheet was deleted | +| `worksheetName` | string | The name of the deleted worksheet | +| `metadata` | object | Spreadsheet metadata | +| ↳ `spreadsheetId` | string | The ID of the spreadsheet | +| ↳ `spreadsheetUrl` | string | URL to access the spreadsheet | + +### `microsoft_excel_sort_range` + +Sort a range or table by a column in a Microsoft Excel worksheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | +| `tableName` | string | No | The name of the table to sort. When provided, the table is sorted and range/sheetName are ignored. | +| `sheetName` | string | No | The name of the worksheet \(e.g., "Sheet1"\). Used for range sorts when the range does not include a sheet name. | +| `range` | string | No | The cell range to sort \(e.g., "A1:D10" or "Sheet1!A1:D10"\). Required when no table name is provided. | +| `sortColumn` | number | Yes | The zero-based column index within the range or table to sort on \(0 = first column\). | +| `sortAscending` | boolean | No | Whether to sort in ascending order. Defaults to true. | +| `hasHeaders` | boolean | No | Whether the range has a header row that should be excluded from sorting. Only applies to range sorts. Defaults to false. | +| `matchCase` | boolean | No | Whether casing affects string ordering. Defaults to false. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sorted` | boolean | Whether the sort was applied | +| `target` | string | The range or table name that was sorted | +| `sortColumn` | number | The zero-based column index that was sorted on | +| `ascending` | boolean | Whether the sort was ascending | +| `metadata` | object | Spreadsheet metadata | +| ↳ `spreadsheetId` | string | The ID of the spreadsheet | +| ↳ `spreadsheetUrl` | string | URL to access the spreadsheet | + diff --git a/apps/docs/content/docs/en/integrations/revenuecat.mdx b/apps/docs/content/docs/en/integrations/revenuecat.mdx index 5822fc971ac..beed6b324d9 100644 --- a/apps/docs/content/docs/en/integrations/revenuecat.mdx +++ b/apps/docs/content/docs/en/integrations/revenuecat.mdx @@ -455,3 +455,329 @@ Immediately revoke access to a Google Play subscription and issue a refund (Goog | ↳ `subscriber_attributes` | object | Custom attributes set on the subscriber. Only returned when using a secret API key | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### RevenueCat Cancellation + +Trigger workflow when a subscriber cancels a RevenueCat subscription + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + + +--- + +### RevenueCat Expiration + +Trigger workflow when a RevenueCat subscription expires + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + + +--- + +### RevenueCat Initial Purchase + +Trigger workflow when a subscriber makes their first purchase in RevenueCat + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + + +--- + +### RevenueCat Non-Renewing Purchase + +Trigger workflow when a subscriber makes a non-renewing purchase in RevenueCat + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + + +--- + +### RevenueCat Product Change + +Trigger workflow when a subscriber changes their RevenueCat subscription product + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + + +--- + +### RevenueCat Renewal + +Trigger workflow when a RevenueCat subscription renews + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat. | +| `projectId` | string | Yes | RevenueCat project identifier the webhook integration is created in. | +| `environment` | string | No | Restrict events to a single environment, or receive all of them. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `type` | string | Event type \(e.g., INITIAL_PURCHASE, RENEWAL\) | +| `id` | string | Unique event identifier | +| `app_id` | string | RevenueCat app public identifier | +| `event_timestamp_ms` | number | Timestamp \(ms since epoch\) when the event was generated | +| `app_user_id` | string | Current App User ID | +| `original_app_user_id` | string | First App User ID ever used | +| `aliases` | json | All App User IDs ever used by the subscriber | +| `product_id` | string | Product identifier | +| `new_product_id` | string | Product identifier changed to \(PRODUCT_CHANGE events only\) | +| `period_type` | string | Period type \(TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID\) | +| `purchased_at_ms` | number | Purchase timestamp \(ms since epoch\) | +| `expiration_at_ms` | number | Expiration timestamp \(ms since epoch\), nullable | +| `environment` | string | Environment \(SANDBOX or PRODUCTION\) | +| `entitlement_id` | string | Deprecated single entitlement identifier | +| `entitlement_ids` | json | Associated entitlement identifiers | +| `presented_offering_id` | string | Identifier of the offering presented to the user | +| `transaction_id` | string | Store transaction ID | +| `original_transaction_id` | string | Original subscription transaction ID | +| `is_family_share` | boolean | Whether the purchase was family shared | +| `country_code` | string | ISO country code of the subscriber | +| `currency` | string | ISO 4217 currency code | +| `price` | number | Price in USD | +| `price_in_purchased_currency` | number | Price in the currency the purchase was made in | +| `store` | string | Store the purchase was made on \(e.g., APP_STORE\) | +| `takehome_percentage` | number | Estimated percentage of the price taken home after store commission | +| `tax_percentage` | number | Estimated percentage taken as tax | +| `commission_percentage` | number | Estimated percentage taken by the store as commission | +| `offer_code` | string | Offer code applied to the purchase, if any | +| `subscriber_attributes` | json | Subscriber attributes at the time of the event | +| `experiments` | json | Experiments the subscriber was enrolled in | +| `cancel_reason` | string | Reason for cancellation \(CANCELLATION events only\) | +| `expiration_reason` | string | Reason for expiration \(EXPIRATION events only\) | +| `api_version` | string | RevenueCat webhook API version | +| `event` | json | Full RevenueCat event object | + diff --git a/apps/docs/content/docs/en/integrations/rootly.mdx b/apps/docs/content/docs/en/integrations/rootly.mdx index 5b45b01fe44..6ffb1303de5 100644 --- a/apps/docs/content/docs/en/integrations/rootly.mdx +++ b/apps/docs/content/docs/en/integrations/rootly.mdx @@ -1340,3 +1340,232 @@ List incident roles configured in Rootly (e.g. commander, scribe). | `totalCount` | number | Total number of incident roles returned | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Rootly Alert Created + +Trigger workflow when a new alert is created in Rootly + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Unique webhook event ID | +| `eventType` | string | Rootly event type \(e.g. alert.created\) | +| `issuedAt` | string | When the event was issued \(ISO 8601\) | +| `data` | object | data output from the tool | +| ↳ `id` | string | Alert ID | +| ↳ `team_id` | number | Team ID | +| ↳ `source` | string | Alert source \(e.g. pagerduty\) | +| ↳ `summary` | string | Alert summary | +| ↳ `labels` | json | Alert labels | +| ↳ `data` | json | Raw alert payload data | +| ↳ `external_id` | string | External alert ID | +| ↳ `external_url` | string | External alert URL | +| ↳ `webhook_type` | string | Webhook type | +| ↳ `webhook_id` | string | Webhook ID | +| ↳ `webhook_idempotency_key` | string | Webhook idempotency key | +| ↳ `started_at` | string | When the alert started | +| ↳ `ended_at` | string | When the alert ended | +| ↳ `deleted_at` | string | When the alert was deleted | +| ↳ `created_at` | string | Alert creation timestamp | +| ↳ `updated_at` | string | Alert last update timestamp | + + +--- + +### Rootly Incident Created + +Trigger workflow when a new incident is created in Rootly + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Unique webhook event ID | +| `eventType` | string | Rootly event type \(e.g. incident.created\) | +| `issuedAt` | string | When the event was issued \(ISO 8601\) | +| `data` | object | data output from the tool | +| ↳ `id` | string | Incident ID | +| ↳ `sequential_id` | number | Sequential incident number | +| ↳ `title` | string | Incident title | +| ↳ `public_title` | string | Public-facing incident title | +| ↳ `slug` | string | Incident slug | +| ↳ `kind` | string | Incident kind \(normal, test, etc.\) | +| ↳ `private` | boolean | Whether the incident is private | +| ↳ `summary` | string | Incident summary | +| ↳ `status` | string | Incident status | +| ↳ `url` | string | Incident URL in Rootly | +| ↳ `short_url` | string | Shortened incident URL | +| ↳ `mitigation_message` | string | Mitigation message | +| ↳ `resolution_message` | string | Resolution message | +| ↳ `cancellation_message` | string | Cancellation message | +| ↳ `slack_channel_name` | string | Linked Slack channel name | +| ↳ `slack_channel_id` | string | Linked Slack channel ID | +| ↳ `slack_channel_url` | string | Linked Slack channel URL | +| ↳ `started_at` | string | When the incident started | +| ↳ `detected_at` | string | When the incident was detected | +| ↳ `acknowledged_at` | string | When the incident was acknowledged | +| ↳ `mitigated_at` | string | When the incident was mitigated | +| ↳ `resolved_at` | string | When the incident was resolved | +| ↳ `cancelled_at` | string | When the incident was cancelled | +| ↳ `created_at` | string | Incident creation timestamp | +| ↳ `updated_at` | string | Incident last update timestamp | +| ↳ `labels` | json | Incident labels \(key-value pairs\) | +| ↳ `severity` | json | Incident severity object | +| ↳ `user` | json | User who owns the incident | +| ↳ `started_by` | json | User who started the incident | +| ↳ `mitigated_by` | json | User who mitigated the incident | +| ↳ `resolved_by` | json | User who resolved the incident | +| ↳ `cancelled_by` | json | User who cancelled the incident | +| ↳ `roles` | json | Assigned incident roles | +| ↳ `environments` | json | Affected environments | +| ↳ `incident_types` | json | Incident types | +| ↳ `services` | json | Affected services | +| ↳ `functionalities` | json | Affected functionalities | +| ↳ `groups` | json | Associated teams/groups | +| ↳ `events` | json | Timeline events | +| ↳ `action_items` | json | Action items | +| ↳ `incident_post_mortem` | json | Retrospective/post-mortem object | + + +--- + +### Rootly Incident Resolved + +Trigger workflow when an incident is resolved in Rootly + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Unique webhook event ID | +| `eventType` | string | Rootly event type \(e.g. incident.created\) | +| `issuedAt` | string | When the event was issued \(ISO 8601\) | +| `data` | object | data output from the tool | +| ↳ `id` | string | Incident ID | +| ↳ `sequential_id` | number | Sequential incident number | +| ↳ `title` | string | Incident title | +| ↳ `public_title` | string | Public-facing incident title | +| ↳ `slug` | string | Incident slug | +| ↳ `kind` | string | Incident kind \(normal, test, etc.\) | +| ↳ `private` | boolean | Whether the incident is private | +| ↳ `summary` | string | Incident summary | +| ↳ `status` | string | Incident status | +| ↳ `url` | string | Incident URL in Rootly | +| ↳ `short_url` | string | Shortened incident URL | +| ↳ `mitigation_message` | string | Mitigation message | +| ↳ `resolution_message` | string | Resolution message | +| ↳ `cancellation_message` | string | Cancellation message | +| ↳ `slack_channel_name` | string | Linked Slack channel name | +| ↳ `slack_channel_id` | string | Linked Slack channel ID | +| ↳ `slack_channel_url` | string | Linked Slack channel URL | +| ↳ `started_at` | string | When the incident started | +| ↳ `detected_at` | string | When the incident was detected | +| ↳ `acknowledged_at` | string | When the incident was acknowledged | +| ↳ `mitigated_at` | string | When the incident was mitigated | +| ↳ `resolved_at` | string | When the incident was resolved | +| ↳ `cancelled_at` | string | When the incident was cancelled | +| ↳ `created_at` | string | Incident creation timestamp | +| ↳ `updated_at` | string | Incident last update timestamp | +| ↳ `labels` | json | Incident labels \(key-value pairs\) | +| ↳ `severity` | json | Incident severity object | +| ↳ `user` | json | User who owns the incident | +| ↳ `started_by` | json | User who started the incident | +| ↳ `mitigated_by` | json | User who mitigated the incident | +| ↳ `resolved_by` | json | User who resolved the incident | +| ↳ `cancelled_by` | json | User who cancelled the incident | +| ↳ `roles` | json | Assigned incident roles | +| ↳ `environments` | json | Affected environments | +| ↳ `incident_types` | json | Incident types | +| ↳ `services` | json | Affected services | +| ↳ `functionalities` | json | Affected functionalities | +| ↳ `groups` | json | Associated teams/groups | +| ↳ `events` | json | Timeline events | +| ↳ `action_items` | json | Action items | +| ↳ `incident_post_mortem` | json | Retrospective/post-mortem object | + + +--- + +### Rootly Incident Updated + +Trigger workflow when an incident is updated in Rootly + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Unique webhook event ID | +| `eventType` | string | Rootly event type \(e.g. incident.created\) | +| `issuedAt` | string | When the event was issued \(ISO 8601\) | +| `data` | object | data output from the tool | +| ↳ `id` | string | Incident ID | +| ↳ `sequential_id` | number | Sequential incident number | +| ↳ `title` | string | Incident title | +| ↳ `public_title` | string | Public-facing incident title | +| ↳ `slug` | string | Incident slug | +| ↳ `kind` | string | Incident kind \(normal, test, etc.\) | +| ↳ `private` | boolean | Whether the incident is private | +| ↳ `summary` | string | Incident summary | +| ↳ `status` | string | Incident status | +| ↳ `url` | string | Incident URL in Rootly | +| ↳ `short_url` | string | Shortened incident URL | +| ↳ `mitigation_message` | string | Mitigation message | +| ↳ `resolution_message` | string | Resolution message | +| ↳ `cancellation_message` | string | Cancellation message | +| ↳ `slack_channel_name` | string | Linked Slack channel name | +| ↳ `slack_channel_id` | string | Linked Slack channel ID | +| ↳ `slack_channel_url` | string | Linked Slack channel URL | +| ↳ `started_at` | string | When the incident started | +| ↳ `detected_at` | string | When the incident was detected | +| ↳ `acknowledged_at` | string | When the incident was acknowledged | +| ↳ `mitigated_at` | string | When the incident was mitigated | +| ↳ `resolved_at` | string | When the incident was resolved | +| ↳ `cancelled_at` | string | When the incident was cancelled | +| ↳ `created_at` | string | Incident creation timestamp | +| ↳ `updated_at` | string | Incident last update timestamp | +| ↳ `labels` | json | Incident labels \(key-value pairs\) | +| ↳ `severity` | json | Incident severity object | +| ↳ `user` | json | User who owns the incident | +| ↳ `started_by` | json | User who started the incident | +| ↳ `mitigated_by` | json | User who mitigated the incident | +| ↳ `resolved_by` | json | User who resolved the incident | +| ↳ `cancelled_by` | json | User who cancelled the incident | +| ↳ `roles` | json | Assigned incident roles | +| ↳ `environments` | json | Affected environments | +| ↳ `incident_types` | json | Incident types | +| ↳ `services` | json | Affected services | +| ↳ `functionalities` | json | Affected functionalities | +| ↳ `groups` | json | Associated teams/groups | +| ↳ `events` | json | Timeline events | +| ↳ `action_items` | json | Action items | +| ↳ `incident_post_mortem` | json | Retrospective/post-mortem object | + diff --git a/apps/docs/content/docs/en/integrations/sentry.mdx b/apps/docs/content/docs/en/integrations/sentry.mdx index 23bd02c3025..df3a9733acf 100644 --- a/apps/docs/content/docs/en/integrations/sentry.mdx +++ b/apps/docs/content/docs/en/integrations/sentry.mdx @@ -687,3 +687,223 @@ List all teams in a Sentry organization. Useful for discovering the team slug re | ↳ `hasMore` | boolean | Whether there are more results available | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Sentry Error Created + +Trigger workflow when a new error event is created in Sentry + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientSecret` | string | Yes | Client Secret | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `action` | string | The action that triggered the webhook \(e.g., created, resolved, triggered\) | +| `installation` | json | Installation object containing the integration installation uuid | +| `actor` | json | Who triggered the webhook \(user, the integration application, or Sentry\) | +| `error` | object | error output from the tool | +| ↳ `event_id` | string | Unique event ID | +| ↳ `issue_id` | string | ID of the issue this error belongs to | +| ↳ `issue_url` | string | API URL of the issue | +| ↳ `project` | number | Project ID | +| ↳ `key_id` | string | Project key ID | +| ↳ `level` | string | Error level | +| ↳ `title` | string | Error title | +| ↳ `eventType` | string | Event type \(the payload's `type` field; `type` is reserved\) | +| ↳ `message` | string | Error message | +| ↳ `culprit` | string | Error culprit \(location/transaction\) | +| ↳ `platform` | string | Platform | +| ↳ `logger` | string | Logger name | +| ↳ `timestamp` | number | Event timestamp \(epoch seconds\) | +| ↳ `datetime` | string | Event datetime \(ISO 8601\) | +| ↳ `received` | number | Received timestamp \(epoch seconds\) | +| ↳ `dist` | string | Distribution identifier | +| ↳ `release` | string | Release version | +| ↳ `fingerprint` | json | Grouping fingerprint | +| ↳ `tags` | json | Event tags | +| ↳ `user` | json | User context | +| ↳ `request` | json | HTTP request context | +| ↳ `contexts` | json | Additional contexts \(browser, os, device\) | +| ↳ `sdk` | json | SDK information | +| ↳ `exception` | json | Exception details including stack frames | +| ↳ `metadata` | json | Error metadata | +| ↳ `url` | string | API URL for the event | +| ↳ `web_url` | string | Browser URL for the event | + + +--- + +### Sentry Issue Alert + +Trigger workflow when a Sentry issue alert rule fires + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientSecret` | string | Yes | Client Secret | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `action` | string | The action that triggered the webhook \(e.g., created, resolved, triggered\) | +| `installation` | json | Installation object containing the integration installation uuid | +| `actor` | json | Who triggered the webhook \(user, the integration application, or Sentry\) | +| `event` | json | The event that triggered the alert rule | +| `triggered_rule` | string | Label of the alert rule that was triggered | +| `issue_alert` | object | issue_alert output from the tool | +| ↳ `title` | string | Alert rule name | +| ↳ `settings` | json | Alert rule action settings \(name/value pairs\) | + + +--- + +### Sentry Issue Created + +Trigger workflow when a new issue is created in Sentry + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientSecret` | string | Yes | Client Secret | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `action` | string | The action that triggered the webhook \(e.g., created, resolved, triggered\) | +| `installation` | json | Installation object containing the integration installation uuid | +| `actor` | json | Who triggered the webhook \(user, the integration application, or Sentry\) | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Issue ID | +| ↳ `shortId` | string | Short human-readable issue ID | +| ↳ `shareId` | string | Share ID for the issue | +| ↳ `title` | string | Issue title | +| ↳ `culprit` | string | Issue culprit \(location/transaction\) | +| ↳ `logger` | string | Logger name | +| ↳ `level` | string | Issue level \(error, warning, etc.\) | +| ↳ `status` | string | Issue status \(unresolved, resolved, ignored\) | +| ↳ `substatus` | string | Issue substatus | +| ↳ `statusDetails` | json | Status details \(inRelease, inCommit, ignore*\) | +| ↳ `platform` | string | Platform of the issue | +| ↳ `eventType` | string | Issue type \(the payload's `type` field; `type` is reserved\) | +| ↳ `issueType` | string | Specific issue type classification | +| ↳ `issueCategory` | string | Issue category | +| ↳ `isUnhandled` | boolean | Whether the issue is unhandled | +| ↳ `isPublic` | boolean | Whether the issue is public | +| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked | +| ↳ `isSubscribed` | boolean | Whether the viewer is subscribed | +| ↳ `hasSeen` | boolean | Whether the issue has been seen | +| ↳ `numComments` | number | Number of comments on the issue | +| ↳ `count` | string | Total event count | +| ↳ `userCount` | number | Number of affected users | +| ↳ `firstSeen` | string | Timestamp when first seen | +| ↳ `lastSeen` | string | Timestamp when last seen | +| ↳ `priority` | string | Issue priority | +| ↳ `assignedTo` | json | Assignee \(user or team\), or null | +| ↳ `annotations` | json | Issue annotations | +| ↳ `metadata` | json | Issue metadata \(title, type, value, sdk, severity\) | +| ↳ `project` | object | project output from the tool | +| ↳ `id` | string | Project ID | +| ↳ `name` | string | Project name | +| ↳ `slug` | string | Project slug | +| ↳ `platform` | string | Project platform | +| ↳ `url` | string | API URL for the issue | +| ↳ `web_url` | string | Browser URL for the issue | +| ↳ `project_url` | string | Browser URL for the project | +| ↳ `permalink` | string | Permalink to the issue | + + +--- + +### Sentry Issue Resolved + +Trigger workflow when an issue is resolved in Sentry + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientSecret` | string | Yes | Client Secret | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `action` | string | The action that triggered the webhook \(e.g., created, resolved, triggered\) | +| `installation` | json | Installation object containing the integration installation uuid | +| `actor` | json | Who triggered the webhook \(user, the integration application, or Sentry\) | +| `issue` | object | issue output from the tool | +| ↳ `id` | string | Issue ID | +| ↳ `shortId` | string | Short human-readable issue ID | +| ↳ `shareId` | string | Share ID for the issue | +| ↳ `title` | string | Issue title | +| ↳ `culprit` | string | Issue culprit \(location/transaction\) | +| ↳ `logger` | string | Logger name | +| ↳ `level` | string | Issue level \(error, warning, etc.\) | +| ↳ `status` | string | Issue status \(unresolved, resolved, ignored\) | +| ↳ `substatus` | string | Issue substatus | +| ↳ `statusDetails` | json | Status details \(inRelease, inCommit, ignore*\) | +| ↳ `platform` | string | Platform of the issue | +| ↳ `eventType` | string | Issue type \(the payload's `type` field; `type` is reserved\) | +| ↳ `issueType` | string | Specific issue type classification | +| ↳ `issueCategory` | string | Issue category | +| ↳ `isUnhandled` | boolean | Whether the issue is unhandled | +| ↳ `isPublic` | boolean | Whether the issue is public | +| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked | +| ↳ `isSubscribed` | boolean | Whether the viewer is subscribed | +| ↳ `hasSeen` | boolean | Whether the issue has been seen | +| ↳ `numComments` | number | Number of comments on the issue | +| ↳ `count` | string | Total event count | +| ↳ `userCount` | number | Number of affected users | +| ↳ `firstSeen` | string | Timestamp when first seen | +| ↳ `lastSeen` | string | Timestamp when last seen | +| ↳ `priority` | string | Issue priority | +| ↳ `assignedTo` | json | Assignee \(user or team\), or null | +| ↳ `annotations` | json | Issue annotations | +| ↳ `metadata` | json | Issue metadata \(title, type, value, sdk, severity\) | +| ↳ `project` | object | project output from the tool | +| ↳ `id` | string | Project ID | +| ↳ `name` | string | Project name | +| ↳ `slug` | string | Project slug | +| ↳ `platform` | string | Project platform | +| ↳ `url` | string | API URL for the issue | +| ↳ `web_url` | string | Browser URL for the issue | +| ↳ `project_url` | string | Browser URL for the project | +| ↳ `permalink` | string | Permalink to the issue | + + +--- + +### Sentry Metric Alert + +Trigger workflow when a Sentry metric alert changes state (critical, warning, resolved) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientSecret` | string | Yes | Client Secret | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `action` | string | The action that triggered the webhook \(e.g., created, resolved, triggered\) | +| `installation` | json | Installation object containing the integration installation uuid | +| `actor` | json | Who triggered the webhook \(user, the integration application, or Sentry\) | +| `metric_alert` | json | Metric alert object \(alert_rule + incident details\) | +| `description_text` | string | Human-friendly description of the alert | +| `description_title` | string | Human-friendly title of the alert | +| `web_url` | string | API URL for the incident | + diff --git a/apps/docs/content/docs/en/integrations/twilio.mdx b/apps/docs/content/docs/en/integrations/twilio.mdx new file mode 100644 index 00000000000..6de83ac0136 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/twilio.mdx @@ -0,0 +1,95 @@ +--- +title: Twilio +description: Twilio triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Twilio Message Status + +Trigger workflow when a Twilio message status changes (sent, delivered, failed) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accountSid` | string | Yes | Your Twilio Account SID from the Twilio Console | +| `authToken` | string | Yes | Your Twilio Auth Token, used to verify the X-Twilio-Signature header | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messageSid` | string | Unique 34-character identifier for the message | +| `accountSid` | string | Twilio Account SID | +| `messagingServiceSid` | string | Messaging Service SID, if the message was sent through one | +| `from` | string | Phone number or channel address that sent the message \(E.164 format\) | +| `to` | string | Phone number or channel address of the recipient \(E.164 format\) | +| `body` | string | Text body of the message \(up to 1600 characters\) | +| `numMedia` | string | Number of media items attached to the message | +| `numSegments` | string | Number of segments that make up the message | +| `media` | json | Array of attached media as \{ url, contentType \} objects \(MMS\) | +| `smsStatus` | string | SMS status \(e.g., received, sent, delivered, undelivered, failed\) | +| `messageStatus` | string | Message status for status callbacks \(sent, delivered, undelivered, failed\) | +| `errorCode` | string | Twilio error code, present when the status is failed or undelivered | +| `apiVersion` | string | Twilio API version used to process the message | +| `fromCity` | string | City of the sender, when available | +| `fromState` | string | State/province of the sender, when available | +| `fromZip` | string | Zip/postal code of the sender, when available | +| `fromCountry` | string | Country of the sender, when available | +| `toCity` | string | City of the recipient, when available | +| `toState` | string | State/province of the recipient, when available | +| `toZip` | string | Zip/postal code of the recipient, when available | +| `toCountry` | string | Country of the recipient, when available | +| `raw` | string | Complete raw webhook payload from Twilio as a JSON string | + + +--- + +### Twilio SMS Received + +Trigger workflow when an inbound SMS or MMS message is received via Twilio + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accountSid` | string | Yes | Your Twilio Account SID from the Twilio Console | +| `authToken` | string | Yes | Your Twilio Auth Token, used to verify the X-Twilio-Signature header | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messageSid` | string | Unique 34-character identifier for the message | +| `accountSid` | string | Twilio Account SID | +| `messagingServiceSid` | string | Messaging Service SID, if the message was sent through one | +| `from` | string | Phone number or channel address that sent the message \(E.164 format\) | +| `to` | string | Phone number or channel address of the recipient \(E.164 format\) | +| `body` | string | Text body of the message \(up to 1600 characters\) | +| `numMedia` | string | Number of media items attached to the message | +| `numSegments` | string | Number of segments that make up the message | +| `media` | json | Array of attached media as \{ url, contentType \} objects \(MMS\) | +| `smsStatus` | string | SMS status \(e.g., received, sent, delivered, undelivered, failed\) | +| `messageStatus` | string | Message status for status callbacks \(sent, delivered, undelivered, failed\) | +| `errorCode` | string | Twilio error code, present when the status is failed or undelivered | +| `apiVersion` | string | Twilio API version used to process the message | +| `fromCity` | string | City of the sender, when available | +| `fromState` | string | State/province of the sender, when available | +| `fromZip` | string | Zip/postal code of the sender, when available | +| `fromCountry` | string | Country of the sender, when available | +| `toCity` | string | City of the recipient, when available | +| `toState` | string | State/province of the recipient, when available | +| `toZip` | string | Zip/postal code of the recipient, when available | +| `toCountry` | string | Country of the recipient, when available | +| `raw` | string | Complete raw webhook payload from Twilio as a JSON string | + diff --git a/apps/docs/content/docs/en/integrations/uptimerobot.mdx b/apps/docs/content/docs/en/integrations/uptimerobot.mdx index f3876ecaf34..eb875883b30 100644 --- a/apps/docs/content/docs/en/integrations/uptimerobot.mdx +++ b/apps/docs/content/docs/en/integrations/uptimerobot.mdx @@ -146,7 +146,7 @@ Create a new monitor in UptimeRobot | `type` | string | Yes | Monitor type: HTTP, KEYWORD, PING, PORT, HEARTBEAT, DNS, API, or UDP | | `url` | string | No | URL or host to monitor \(not required for Heartbeat monitors\) | | `interval` | number | Yes | Check interval in seconds \(minimum 30\) | -| `timeout` | number | No | Check timeout in seconds, 0-60 \(HTTP, Keyword and Port monitors only\) | +| `checkTimeout` | number | No | Check timeout in seconds, 0-60 \(HTTP, Keyword and Port monitors only\) | | `port` | number | No | Port to check, 1-65535 \(required for Port and UDP monitors\) | | `keywordType` | string | No | Keyword match type for Keyword monitors: ALERT_EXISTS or ALERT_NOT_EXISTS | | `keywordValue` | string | No | Keyword to look for \(Keyword monitors only\) | @@ -223,7 +223,7 @@ Update an existing UptimeRobot monitor. Only the provided fields are changed. | `friendlyName` | string | No | New friendly name | | `url` | string | No | New URL or host to monitor | | `interval` | number | No | New check interval in seconds \(minimum 30\) | -| `timeout` | number | No | New check timeout in seconds, 0-60 | +| `checkTimeout` | number | No | New check timeout in seconds, 0-60 | | `port` | number | No | New port, 1-65535 \(Port and UDP monitors\) | | `keywordType` | string | No | Keyword match type: ALERT_EXISTS or ALERT_NOT_EXISTS | | `keywordValue` | string | No | New keyword to look for | @@ -583,7 +583,6 @@ Update an existing maintenance window. Only the provided fields are changed. | `date` | string | No | Start date in YYYY-MM-DD format | | `time` | string | No | Start time in HH:mm:ss format | | `duration` | number | No | Duration in minutes \(minimum 1\) | -| `autoAddMonitors` | boolean | No | Whether to automatically add all monitors to this window | | `days` | string | No | Comma-separated days for weekly \(1-7\) or monthly \(day-of-month, -1 for last day\) windows | | `monitorIds` | string | No | Comma-separated monitor IDs to assign to the window | | `status` | string | No | Set to "active" to enable or "paused" to disable the maintenance window | diff --git a/apps/docs/content/docs/en/integrations/whatsapp.mdx b/apps/docs/content/docs/en/integrations/whatsapp.mdx index d7db0f573a1..d6c758ec6ec 100644 --- a/apps/docs/content/docs/en/integrations/whatsapp.mdx +++ b/apps/docs/content/docs/en/integrations/whatsapp.mdx @@ -26,7 +26,7 @@ In Sim, the WhatsApp integration enables your agents to leverage these messaging ## Usage Instructions -Integrate WhatsApp into the workflow. Can send messages. +Integrate WhatsApp into the workflow. Send text, template, media, and interactive messages, react to messages, and mark messages as read through the WhatsApp Cloud API. @@ -59,6 +59,199 @@ Send a text message through the WhatsApp Cloud API. | ↳ `input` | string | Input phone number sent to the API | | ↳ `wa_id` | string | WhatsApp user ID associated with the recipient | +### `whatsapp_send_template` + +Send a pre-approved WhatsApp template message with a language and optional variable components. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `phoneNumber` | string | Yes | Recipient phone number with country code \(e.g., +14155552671\) | +| `templateName` | string | Yes | Name of the approved message template | +| `languageCode` | string | Yes | Template language/locale code \(e.g., en_US\) | +| `components` | json | No | Template components array with parameters for header/body/button variables, per the WhatsApp template message schema | +| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID \(from Meta Business Suite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Send success status | +| `messageId` | string | WhatsApp message identifier | +| `messageStatus` | string | Initial delivery state returned by the send API, such as accepted or paused | +| `messagingProduct` | string | Messaging product returned by the send API | +| `inputPhoneNumber` | string | Recipient phone number echoed by the send API | +| `whatsappUserId` | string | Resolved WhatsApp user ID for the recipient | +| `contacts` | array | Recipient contacts returned by the send API \(each item includes input and wa_id\) | +| `eventType` | string | Webhook classification such as incoming_message, message_status, or mixed | +| `from` | string | Sender phone number from the first incoming message | +| `recipientId` | string | Recipient phone number from the first status update in the batch | +| `phoneNumberId` | string | Business phone number ID from the first message or status item in the batch | +| `displayPhoneNumber` | string | Business display phone number from the first message or status item in the batch | +| `text` | string | Text body from the first incoming text message | +| `timestamp` | string | Timestamp from the first message or status item in the batch | +| `messageType` | string | Type of the first incoming message in the batch, such as text, image, or system | +| `status` | string | First outgoing message status in the batch, such as sent, delivered, or read | +| `contact` | json | First sender contact in the webhook batch \(wa_id, profile.name\) | +| `messages` | json | All incoming message objects from the webhook batch, flattened across entries/changes | +| `statuses` | json | All message status objects from the webhook batch, flattened across entries/changes | +| `webhookContacts` | json | All sender contact profiles from the webhook batch | +| `conversation` | json | Conversation metadata from the first status update in the batch \(id, expiration_timestamp, origin.type\) | +| `pricing` | json | Pricing metadata from the first status update in the batch \(billable, pricing_model, category\) | +| `raw` | json | Full structured WhatsApp webhook payload | +| `error` | string | Error information if sending fails | + +### `whatsapp_send_media` + +Send an image, document, video, or audio message via a public link or an uploaded media ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `phoneNumber` | string | Yes | Recipient phone number with country code \(e.g., +14155552671\) | +| `mediaType` | string | Yes | Type of media to send: image, document, video, or audio | +| `mediaLink` | string | No | Public HTTPS URL of the media \(provide this or mediaId\) | +| `mediaId` | string | No | ID of media previously uploaded to WhatsApp \(provide this or mediaLink\) | +| `caption` | string | No | Optional caption for image, video, or document media | +| `filename` | string | No | Optional file name shown to the recipient for document media | +| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID \(from Meta Business Suite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Send success status | +| `messageId` | string | WhatsApp message identifier | +| `messageStatus` | string | Initial delivery state returned by the send API, such as accepted or paused | +| `messagingProduct` | string | Messaging product returned by the send API | +| `inputPhoneNumber` | string | Recipient phone number echoed by the send API | +| `whatsappUserId` | string | Resolved WhatsApp user ID for the recipient | +| `contacts` | array | Recipient contacts returned by the send API \(each item includes input and wa_id\) | +| `eventType` | string | Webhook classification such as incoming_message, message_status, or mixed | +| `from` | string | Sender phone number from the first incoming message | +| `recipientId` | string | Recipient phone number from the first status update in the batch | +| `phoneNumberId` | string | Business phone number ID from the first message or status item in the batch | +| `displayPhoneNumber` | string | Business display phone number from the first message or status item in the batch | +| `text` | string | Text body from the first incoming text message | +| `timestamp` | string | Timestamp from the first message or status item in the batch | +| `messageType` | string | Type of the first incoming message in the batch, such as text, image, or system | +| `status` | string | First outgoing message status in the batch, such as sent, delivered, or read | +| `contact` | json | First sender contact in the webhook batch \(wa_id, profile.name\) | +| `messages` | json | All incoming message objects from the webhook batch, flattened across entries/changes | +| `statuses` | json | All message status objects from the webhook batch, flattened across entries/changes | +| `webhookContacts` | json | All sender contact profiles from the webhook batch | +| `conversation` | json | Conversation metadata from the first status update in the batch \(id, expiration_timestamp, origin.type\) | +| `pricing` | json | Pricing metadata from the first status update in the batch \(billable, pricing_model, category\) | +| `raw` | json | Full structured WhatsApp webhook payload | +| `error` | string | Error information if sending fails | + +### `whatsapp_send_interactive` + +Send an interactive WhatsApp message with reply buttons or a selectable list. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `phoneNumber` | string | Yes | Recipient phone number with country code \(e.g., +14155552671\) | +| `bodyText` | string | Yes | Main body text of the interactive message | +| `headerText` | string | No | Optional plain-text header shown above the body | +| `footerText` | string | No | Optional footer text shown below the body | +| `buttons` | json | No | Reply buttons array \(max 3\), each item: \{ "type": "reply", "reply": \{ "id": "...", "title": "..." \} \}. Provide buttons or sections. | +| `listButtonText` | string | No | Label for the menu button that opens the list \(required when sending a list\) | +| `sections` | json | No | List sections array, each item: \{ "title": "...", "rows": \[\{ "id": "...", "title": "...", "description": "..." \}\] \}. Provide sections or buttons. | +| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID \(from Meta Business Suite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Send success status | +| `messageId` | string | WhatsApp message identifier | +| `messageStatus` | string | Initial delivery state returned by the send API, such as accepted or paused | +| `messagingProduct` | string | Messaging product returned by the send API | +| `inputPhoneNumber` | string | Recipient phone number echoed by the send API | +| `whatsappUserId` | string | Resolved WhatsApp user ID for the recipient | +| `contacts` | array | Recipient contacts returned by the send API \(each item includes input and wa_id\) | +| `eventType` | string | Webhook classification such as incoming_message, message_status, or mixed | +| `from` | string | Sender phone number from the first incoming message | +| `recipientId` | string | Recipient phone number from the first status update in the batch | +| `phoneNumberId` | string | Business phone number ID from the first message or status item in the batch | +| `displayPhoneNumber` | string | Business display phone number from the first message or status item in the batch | +| `text` | string | Text body from the first incoming text message | +| `timestamp` | string | Timestamp from the first message or status item in the batch | +| `messageType` | string | Type of the first incoming message in the batch, such as text, image, or system | +| `status` | string | First outgoing message status in the batch, such as sent, delivered, or read | +| `contact` | json | First sender contact in the webhook batch \(wa_id, profile.name\) | +| `messages` | json | All incoming message objects from the webhook batch, flattened across entries/changes | +| `statuses` | json | All message status objects from the webhook batch, flattened across entries/changes | +| `webhookContacts` | json | All sender contact profiles from the webhook batch | +| `conversation` | json | Conversation metadata from the first status update in the batch \(id, expiration_timestamp, origin.type\) | +| `pricing` | json | Pricing metadata from the first status update in the batch \(billable, pricing_model, category\) | +| `raw` | json | Full structured WhatsApp webhook payload | +| `error` | string | Error information if sending fails | + +### `whatsapp_send_reaction` + +React to a WhatsApp message with an emoji. Send an empty emoji to remove an existing reaction. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `phoneNumber` | string | Yes | Recipient phone number with country code \(e.g., +14155552671\) | +| `messageId` | string | Yes | ID \(wamid\) of the message to react to | +| `emoji` | string | No | Emoji to react with. Leave empty to remove an existing reaction. | +| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID \(from Meta Business Suite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Send success status | +| `messageId` | string | WhatsApp message identifier | +| `messageStatus` | string | Initial delivery state returned by the send API, such as accepted or paused | +| `messagingProduct` | string | Messaging product returned by the send API | +| `inputPhoneNumber` | string | Recipient phone number echoed by the send API | +| `whatsappUserId` | string | Resolved WhatsApp user ID for the recipient | +| `contacts` | array | Recipient contacts returned by the send API \(each item includes input and wa_id\) | +| `eventType` | string | Webhook classification such as incoming_message, message_status, or mixed | +| `from` | string | Sender phone number from the first incoming message | +| `recipientId` | string | Recipient phone number from the first status update in the batch | +| `phoneNumberId` | string | Business phone number ID from the first message or status item in the batch | +| `displayPhoneNumber` | string | Business display phone number from the first message or status item in the batch | +| `text` | string | Text body from the first incoming text message | +| `timestamp` | string | Timestamp from the first message or status item in the batch | +| `messageType` | string | Type of the first incoming message in the batch, such as text, image, or system | +| `status` | string | First outgoing message status in the batch, such as sent, delivered, or read | +| `contact` | json | First sender contact in the webhook batch \(wa_id, profile.name\) | +| `messages` | json | All incoming message objects from the webhook batch, flattened across entries/changes | +| `statuses` | json | All message status objects from the webhook batch, flattened across entries/changes | +| `webhookContacts` | json | All sender contact profiles from the webhook batch | +| `conversation` | json | Conversation metadata from the first status update in the batch \(id, expiration_timestamp, origin.type\) | +| `pricing` | json | Pricing metadata from the first status update in the batch \(billable, pricing_model, category\) | +| `raw` | json | Full structured WhatsApp webhook payload | +| `error` | string | Error information if sending fails | + +### `whatsapp_mark_read` + +Mark a received WhatsApp message as read so the sender sees blue checkmarks. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `messageId` | string | Yes | ID \(wamid\) of the incoming message to mark as read | +| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID \(from Meta Business Suite\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the message was successfully marked as read | + ## Triggers diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 6f33716c3ca..faedd66e8c5 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -11,7 +11,7 @@ export const AirtableBlock: BlockConfig = { description: 'Read, create, and update Airtable', authMode: AuthMode.OAuth, longDescription: - 'Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.', + 'Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, update, upsert, or delete records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.', docsLink: 'https://docs.sim.ai/integrations/airtable', category: 'tools', integrationType: IntegrationType.Databases, @@ -31,6 +31,8 @@ export const AirtableBlock: BlockConfig = { { label: 'Create Records', id: 'create' }, { label: 'Update Record', id: 'update' }, { label: 'Update Multiple Records', id: 'updateMultiple' }, + { label: 'Upsert Records', id: 'upsert' }, + { label: 'Delete Records', id: 'delete' }, ], value: () => 'list', }, @@ -162,7 +164,7 @@ Return ONLY the formula - no explanations, no quotes around the entire formula.` title: 'Records (JSON Array)', type: 'code', placeholder: 'For Create: `[{ "fields": { ... } }]`\n', - condition: { field: 'operation', value: ['create', 'updateMultiple'] }, + condition: { field: 'operation', value: ['create', 'updateMultiple', 'upsert'] }, required: true, wandConfig: { enabled: true, @@ -237,6 +239,58 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, generationType: 'json-object', }, }, + { + id: 'fieldsToMergeOn', + title: 'Fields to Merge On (JSON Array)', + type: 'code', + placeholder: 'Field names to match existing records on, e.g., `["Name"]`', + condition: { field: 'operation', value: 'upsert' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate an Airtable fieldsToMergeOn JSON array based on the user's description. +This is a list of field names (max 3) used to match existing records during an upsert. +A record is updated when all of these fields match an existing record, otherwise it is created. + +Format: +["Field Name", "Another Field"] + +Examples: +- "match on email" -> ["Email"] +- "match on name and company" -> ["Name", "Company"] + +Return ONLY the valid JSON array of field name strings - no explanations, no markdown.`, + placeholder: 'Describe which fields uniquely identify a record...', + generationType: 'json-object', + }, + }, + { + id: 'typecast', + title: 'Typecast', + type: 'switch', + condition: { field: 'operation', value: 'upsert' }, + mode: 'advanced', + }, + { + id: 'recordIds', + title: 'Record IDs (JSON Array)', + type: 'code', + placeholder: 'IDs of records to delete, e.g., `["recXXXXXXXXXXXXXX"]`', + condition: { field: 'operation', value: 'delete' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate an Airtable record IDs JSON array based on the user's description. +Each record ID starts with "rec". + +Format: +["recXXXXXXXXXXXXXX", "recYYYYYYYYYYYYYY"] + +Return ONLY the valid JSON array of record ID strings - no explanations, no markdown.`, + placeholder: 'Describe which records to delete...', + generationType: 'json-object', + }, + }, ...getTrigger('airtable_webhook').subBlocks, ], tools: { @@ -248,6 +302,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, 'airtable_create_records', 'airtable_update_record', 'airtable_update_multiple_records', + 'airtable_upsert_records', + 'airtable_delete_records', 'airtable_get_base_schema', ], config: { @@ -267,6 +323,10 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, return 'airtable_update_record' case 'updateMultiple': return 'airtable_update_multiple_records' + case 'upsert': + return 'airtable_upsert_records' + case 'delete': + return 'airtable_delete_records' case 'getSchema': return 'airtable_get_base_schema' default: @@ -274,18 +334,32 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, } }, params: (params) => { - const { oauthCredential, records, fields, ...rest } = params + const { oauthCredential, records, fields, fieldsToMergeOn, recordIds, typecast, ...rest } = + params let parsedRecords: any | undefined let parsedFields: any | undefined + let parsedFieldsToMergeOn: any | undefined + let parsedRecordIds: any | undefined // Parse JSON inputs safely try { - if (records && (params.operation === 'create' || params.operation === 'updateMultiple')) { + if ( + records && + (params.operation === 'create' || + params.operation === 'updateMultiple' || + params.operation === 'upsert') + ) { parsedRecords = JSON.parse(records) } if (fields && params.operation === 'update') { parsedFields = JSON.parse(fields) } + if (fieldsToMergeOn && params.operation === 'upsert') { + parsedFieldsToMergeOn = JSON.parse(fieldsToMergeOn) + } + if (recordIds && params.operation === 'delete') { + parsedRecordIds = JSON.parse(recordIds) + } } catch (error: any) { throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`) } @@ -300,6 +374,15 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, case 'create': case 'updateMultiple': return { ...baseParams, records: parsedRecords } + case 'upsert': + return { + ...baseParams, + records: parsedRecords, + fieldsToMergeOn: parsedFieldsToMergeOn, + ...(typecast != null ? { typecast: typecast === true || typecast === 'true' } : {}), + } + case 'delete': + return { ...baseParams, recordIds: parsedRecordIds } case 'update': return { ...baseParams, fields: parsedFields } default: @@ -317,8 +400,11 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, recordId: { type: 'string', description: 'Record identifier' }, // Required for get/update maxRecords: { type: 'number', description: 'Maximum records to return' }, // Optional for list filterFormula: { type: 'string', description: 'Filter formula expression' }, // Optional for list - records: { type: 'json', description: 'Record data array' }, // Required for create/updateMultiple + records: { type: 'json', description: 'Record data array' }, // Required for create/updateMultiple/upsert fields: { type: 'json', description: 'Field data object' }, // Required for update single + fieldsToMergeOn: { type: 'json', description: 'Field names to match records on' }, // Required for upsert + typecast: { type: 'boolean', description: 'Auto-convert string values to field types' }, // Optional for upsert + recordIds: { type: 'json', description: 'Record IDs to delete' }, // Required for delete }, // Output structure depends on the operation, covered by AirtableResponse union type outputs: { @@ -326,6 +412,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, tables: { type: 'json', description: 'Table schemas with fields and views' }, records: { type: 'json', description: 'Retrieved record data' }, record: { type: 'json', description: 'Single record data' }, + createdRecords: { type: 'json', description: 'IDs of records created during upsert' }, + updatedRecords: { type: 'json', description: 'IDs of records updated during upsert' }, metadata: { type: 'json', description: 'Operation metadata' }, // Trigger outputs event_type: { type: 'string', description: 'Type of Airtable event' }, diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index 9cece1fb364..36834c53985 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -8,10 +8,10 @@ import type { GoogleDocsResponse } from '@/tools/google_docs/types' export const GoogleDocsBlock: BlockConfig = { type: 'google_docs', name: 'Google Docs', - description: 'Read, write, and create documents', + description: 'Read, write, create, and edit documents', authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Docs into the workflow. Can read, write, and create documents.', + 'Integrate Google Docs into the workflow. Read, write, and create documents, insert text, tables, images, and page breaks, find and replace text, and apply text styling.', docsLink: 'https://docs.sim.ai/integrations/google_docs', category: 'tools', integrationType: IntegrationType.Documents, @@ -27,6 +27,12 @@ export const GoogleDocsBlock: BlockConfig = { { label: 'Read Document', id: 'read' }, { label: 'Write to Document', id: 'write' }, { label: 'Create Document', id: 'create' }, + { label: 'Insert Text', id: 'insert_text' }, + { label: 'Find & Replace Text', id: 'replace_text' }, + { label: 'Insert Table', id: 'insert_table' }, + { label: 'Insert Image', id: 'insert_image' }, + { label: 'Insert Page Break', id: 'insert_page_break' }, + { label: 'Apply Text Style', id: 'update_text_style' }, ], value: () => 'read', }, @@ -65,7 +71,19 @@ export const GoogleDocsBlock: BlockConfig = { placeholder: 'Select a document', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: ['read', 'write'] }, + condition: { + field: 'operation', + value: [ + 'read', + 'write', + 'insert_text', + 'replace_text', + 'insert_table', + 'insert_image', + 'insert_page_break', + 'update_text_style', + ], + }, }, // Manual document ID input (advanced mode) { @@ -76,7 +94,19 @@ export const GoogleDocsBlock: BlockConfig = { placeholder: 'Enter document ID', dependsOn: ['credential'], mode: 'advanced', - condition: { field: 'operation', value: ['read', 'write'] }, + condition: { + field: 'operation', + value: [ + 'read', + 'write', + 'insert_text', + 'replace_text', + 'insert_table', + 'insert_image', + 'insert_page_break', + 'update_text_style', + ], + }, }, // Create-specific Fields { @@ -163,9 +193,156 @@ Return ONLY the document content - no explanations, no extra text.`, description: 'Convert headings, bold/italic, lists, tables, links, code, and blockquotes into formatted Google Docs content. When off, content is inserted as plain text.', }, + // Insert Text fields + { + id: 'text', + title: 'Text', + type: 'long-input', + placeholder: 'Enter text to insert', + condition: { field: 'operation', value: 'insert_text' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate text to insert into a Google Doc based on the user's request. +The text should be well-structured and appropriate for the document. + +Return ONLY the text to insert - no explanations, no extra text.`, + placeholder: 'Describe the text you want to insert...', + }, + }, + // Find & Replace fields + { + id: 'searchText', + title: 'Find', + type: 'short-input', + placeholder: 'Text to find', + condition: { field: 'operation', value: 'replace_text' }, + required: true, + }, + { + id: 'replaceText', + title: 'Replace With', + type: 'short-input', + placeholder: 'Replacement text (leave empty to delete matches)', + condition: { field: 'operation', value: 'replace_text' }, + }, + { + id: 'matchCase', + title: 'Match Case', + type: 'switch', + condition: { field: 'operation', value: 'replace_text' }, + description: 'When on, only case-sensitive matches are replaced.', + }, + // Insert Table fields + { + id: 'rows', + title: 'Rows', + type: 'short-input', + placeholder: 'e.g., 3', + condition: { field: 'operation', value: 'insert_table' }, + required: true, + }, + { + id: 'columns', + title: 'Columns', + type: 'short-input', + placeholder: 'e.g., 2', + condition: { field: 'operation', value: 'insert_table' }, + required: true, + }, + // Insert Image fields + { + id: 'imageUrl', + title: 'Image URL', + type: 'short-input', + placeholder: 'Public URL of the image to insert', + condition: { field: 'operation', value: 'insert_image' }, + required: true, + }, + { + id: 'width', + title: 'Width (PT)', + type: 'short-input', + placeholder: 'Optional width in points', + condition: { field: 'operation', value: 'insert_image' }, + mode: 'advanced', + }, + { + id: 'height', + title: 'Height (PT)', + type: 'short-input', + placeholder: 'Optional height in points', + condition: { field: 'operation', value: 'insert_image' }, + mode: 'advanced', + }, + // Apply Text Style fields + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Start character index (inclusive)', + condition: { field: 'operation', value: 'update_text_style' }, + required: true, + }, + { + id: 'endIndex', + title: 'End Index', + type: 'short-input', + placeholder: 'End character index (exclusive)', + condition: { field: 'operation', value: 'update_text_style' }, + required: true, + }, + { + id: 'bold', + title: 'Bold', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'italic', + title: 'Italic', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'underline', + title: 'Underline', + type: 'switch', + condition: { field: 'operation', value: 'update_text_style' }, + }, + { + id: 'fontSize', + title: 'Font Size (PT)', + type: 'short-input', + placeholder: 'Optional font size in points', + condition: { field: 'operation', value: 'update_text_style' }, + mode: 'advanced', + }, + // Shared insertion index (advanced) for the insert operations + { + id: 'index', + title: 'Insertion Index', + type: 'short-input', + placeholder: 'Character index (leave empty to append at end)', + condition: { + field: 'operation', + value: ['insert_text', 'insert_table', 'insert_image', 'insert_page_break'], + }, + mode: 'advanced', + }, ], tools: { - access: ['google_docs_read', 'google_docs_write', 'google_docs_create'], + access: [ + 'google_docs_read', + 'google_docs_write', + 'google_docs_create', + 'google_docs_insert_text', + 'google_docs_replace_text', + 'google_docs_insert_table', + 'google_docs_insert_image', + 'google_docs_insert_page_break', + 'google_docs_update_text_style', + ], config: { tool: (params) => { switch (params.operation) { @@ -175,6 +352,18 @@ Return ONLY the document content - no explanations, no extra text.`, return 'google_docs_write' case 'create': return 'google_docs_create' + case 'insert_text': + return 'google_docs_insert_text' + case 'replace_text': + return 'google_docs_replace_text' + case 'insert_table': + return 'google_docs_insert_table' + case 'insert_image': + return 'google_docs_insert_image' + case 'insert_page_break': + return 'google_docs_insert_page_break' + case 'update_text_style': + return 'google_docs_update_text_style' default: throw new Error(`Invalid Google Docs operation: ${params.operation}`) } @@ -185,8 +374,34 @@ Return ONLY the document content - no explanations, no extra text.`, const effectiveDocumentId = documentId ? String(documentId).trim() : '' const effectiveFolderId = folderId ? String(folderId).trim() : '' + const toNumber = (value: unknown): number | undefined => { + if (value === undefined || value === null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + + const numericFields = [ + 'index', + 'rows', + 'columns', + 'width', + 'height', + 'startIndex', + 'endIndex', + 'fontSize', + ] as const + + const coerced: Record = {} + for (const field of numericFields) { + const value = (rest as Record)[field] + const num = toNumber(value) + if (num !== undefined) coerced[field] = num + else delete (rest as Record)[field] + } + return { ...rest, + ...coerced, documentId: effectiveDocumentId || undefined, folderId: effectiveFolderId || undefined, oauthCredential, @@ -205,11 +420,32 @@ Return ONLY the document content - no explanations, no extra text.`, type: 'boolean', description: 'Interpret content as Markdown when creating a document', }, + text: { type: 'string', description: 'Text to insert' }, + index: { type: 'number', description: 'Insertion character index' }, + searchText: { type: 'string', description: 'Text to find' }, + replaceText: { type: 'string', description: 'Replacement text' }, + matchCase: { type: 'boolean', description: 'Case-sensitive find & replace' }, + rows: { type: 'number', description: 'Number of table rows' }, + columns: { type: 'number', description: 'Number of table columns' }, + imageUrl: { type: 'string', description: 'Public URL of image to insert' }, + width: { type: 'number', description: 'Image width in points' }, + height: { type: 'number', description: 'Image height in points' }, + startIndex: { type: 'number', description: 'Start character index of style range' }, + endIndex: { type: 'number', description: 'End character index of style range' }, + bold: { type: 'boolean', description: 'Apply bold styling' }, + italic: { type: 'boolean', description: 'Apply italic styling' }, + underline: { type: 'boolean', description: 'Apply underline styling' }, + fontSize: { type: 'number', description: 'Font size in points' }, }, outputs: { content: { type: 'string', description: 'Document content' }, metadata: { type: 'json', description: 'Document metadata' }, updatedContent: { type: 'boolean', description: 'Content update status' }, + occurrencesChanged: { + type: 'number', + description: 'Number of occurrences replaced during find & replace', + }, + objectId: { type: 'string', description: 'ID of an inserted inline image object' }, }, } diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index da57efcb1a0..4d05101cbed 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -8,6 +8,52 @@ import type { MicrosoftExcelV2Response, } from '@/tools/microsoft_excel/types' +/** Maps the read/write operation to its `_v2` tool id, falling back to read on unknown ops. */ +const versionedReadWriteSelector = createVersionedToolSelector>({ + baseToolSelector: (params) => { + switch (params.operation) { + case 'read': + return 'microsoft_excel_read' + case 'write': + return 'microsoft_excel_write' + default: + throw new Error(`Invalid Microsoft Excel operation: ${params.operation}`) + } + }, + suffix: '_v2', + fallbackToolId: 'microsoft_excel_read_v2', +}) + +/** Normalizes an empty/whitespace dropdown or input value to `undefined`, otherwise a trimmed string. */ +function optionalString(value: unknown): string | undefined { + if (value === undefined || value === null) return undefined + const trimmed = String(value).trim() + return trimmed.length > 0 ? trimmed : undefined +} + +/** Coerces a 'true'/'false' dropdown value to boolean, or `undefined` when unset. */ +function optionalBoolean(value: unknown): boolean | undefined { + const str = optionalString(value) + if (str === undefined) return undefined + if (str === 'true') return true + if (str === 'false') return false + return undefined +} + +/** Coerces a numeric input value to number, or `undefined` when unset or invalid. */ +function optionalNumber(value: unknown): number | undefined { + const str = optionalString(value) + if (str === undefined) return undefined + const num = Number(str) + return Number.isNaN(num) ? undefined : num +} + +/** Wraps a worksheet name in single quotes when it contains characters that require escaping in an address. */ +function quoteSheetName(sheetName: string): string { + if (/^[A-Za-z0-9_]+$/.test(sheetName)) return sheetName + return `'${sheetName.replace(/'/g, "''")}'` +} + export const MicrosoftExcelBlock: BlockConfig = { type: 'microsoft_excel', name: 'Microsoft Excel (Legacy)', @@ -31,6 +77,11 @@ export const MicrosoftExcelBlock: BlockConfig = { { label: 'Write/Update Data', id: 'write' }, { label: 'Add to Table', id: 'table_add' }, { label: 'Add Worksheet', id: 'worksheet_add' }, + { label: 'Clear Range', id: 'clear_range' }, + { label: 'Format Range', id: 'format_range' }, + { label: 'Create Table', id: 'create_table' }, + { label: 'Sort Range', id: 'sort_range' }, + { label: 'Delete Worksheet', id: 'delete_worksheet' }, ], value: () => 'read', }, @@ -88,7 +139,22 @@ export const MicrosoftExcelBlock: BlockConfig = { title: 'Range', type: 'short-input', placeholder: 'Sheet name and cell range (e.g., Sheet1!A1:D10)', - condition: { field: 'operation', value: ['read', 'write', 'update'] }, + condition: { + field: 'operation', + value: [ + 'read', + 'write', + 'update', + 'clear_range', + 'format_range', + 'create_table', + 'sort_range', + ], + }, + required: { + field: 'operation', + value: ['clear_range', 'format_range', 'create_table'], + }, wandConfig: { enabled: true, prompt: `Generate a valid Microsoft Excel range based on the user's description. @@ -129,8 +195,8 @@ Return ONLY the range string - no explanations, no quotes around the entire outp id: 'worksheetName', title: 'Worksheet Name', type: 'short-input', - placeholder: 'Name of the new worksheet (max 31 characters)', - condition: { field: 'operation', value: ['worksheet_add'] }, + placeholder: 'Name of the worksheet (max 31 characters)', + condition: { field: 'operation', value: ['worksheet_add', 'delete_worksheet'] }, required: true, }, { @@ -231,6 +297,139 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, generationType: 'json-object', }, }, + // Clear Range + { + id: 'applyTo', + title: 'Clear', + type: 'dropdown', + options: [ + { label: 'All (contents and formats)', id: 'All' }, + { label: 'Contents only', id: 'Contents' }, + { label: 'Formats only', id: 'Formats' }, + ], + value: () => 'All', + condition: { field: 'operation', value: 'clear_range' }, + }, + // Format Range + { + id: 'fillColor', + title: 'Fill Color', + type: 'short-input', + placeholder: 'Hex color (e.g., #FFFF00)', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontBold', + title: 'Bold', + type: 'dropdown', + options: [ + { label: 'No change', id: '' }, + { label: 'Bold', id: 'true' }, + { label: 'Not bold', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontColor', + title: 'Font Color', + type: 'short-input', + placeholder: 'Hex color (e.g., #FF0000)', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontItalic', + title: 'Italic', + type: 'dropdown', + options: [ + { label: 'No change', id: '' }, + { label: 'Italic', id: 'true' }, + { label: 'Not italic', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + { + id: 'fontSize', + title: 'Font Size', + type: 'short-input', + placeholder: 'Font size in points (e.g., 12)', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + { + id: 'fontName', + title: 'Font Name', + type: 'short-input', + placeholder: 'Font name (e.g., Calibri)', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + // Create Table + { + id: 'tableHasHeaders', + title: 'First Row Has Headers', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'create_table' }, + }, + // Sort Range + { + id: 'sortTableName', + title: 'Table Name', + type: 'short-input', + placeholder: 'Optional: sort a table instead of the cell range', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, + { + id: 'sortColumn', + title: 'Sort Column Index', + type: 'short-input', + placeholder: 'Zero-based column index (0 = first column)', + condition: { field: 'operation', value: 'sort_range' }, + required: { field: 'operation', value: 'sort_range' }, + }, + { + id: 'sortAscending', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'true' }, + { label: 'Descending', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'sort_range' }, + }, + { + id: 'sortHasHeaders', + title: 'Range Has Header Row', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, + { + id: 'sortMatchCase', + title: 'Match Case', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -238,6 +437,11 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, 'microsoft_excel_write', 'microsoft_excel_table_add', 'microsoft_excel_worksheet_add', + 'microsoft_excel_clear_range', + 'microsoft_excel_format_range', + 'microsoft_excel_create_table', + 'microsoft_excel_delete_worksheet', + 'microsoft_excel_sort_range', ], config: { tool: (params) => { @@ -250,6 +454,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, return 'microsoft_excel_table_add' case 'worksheet_add': return 'microsoft_excel_worksheet_add' + case 'clear_range': + return 'microsoft_excel_clear_range' + case 'format_range': + return 'microsoft_excel_format_range' + case 'create_table': + return 'microsoft_excel_create_table' + case 'delete_worksheet': + return 'microsoft_excel_delete_worksheet' + case 'sort_range': + return 'microsoft_excel_sort_range' default: throw new Error(`Invalid Microsoft Excel operation: ${params.operation}`) } @@ -262,54 +476,122 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, tableName, worksheetName, driveId, + range, + operation, + applyTo, + fillColor, + fontBold, + fontItalic, + fontColor, + fontSize, + fontName, + tableHasHeaders, + sortTableName, + sortColumn, + sortAscending, + sortHasHeaders, + sortMatchCase, siteId: _siteId, - ...rest + valueInputOption, } = params const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : '' - - let parsedValues - try { - parsedValues = values ? JSON.parse(values as string) : undefined - } catch (error) { - throw new Error('Invalid JSON format for values') - } - if (!effectiveSpreadsheetId) { throw new Error('Spreadsheet ID is required.') } - if (params.operation === 'table_add' && !tableName) { - throw new Error('Table name is required for table operations.') - } - - if (params.operation === 'worksheet_add' && !worksheetName) { - throw new Error('Worksheet name is required for worksheet operations.') - } + const effectiveDriveId = driveId ? String(driveId).trim() : undefined + const trimmedRange = range ? String(range).trim() : undefined - const baseParams = { - ...rest, + const base = { spreadsheetId: effectiveSpreadsheetId, - driveId: driveId ? String(driveId).trim() : undefined, - values: parsedValues, + driveId: effectiveDriveId, oauthCredential, } - if (params.operation === 'table_add') { - return { - ...baseParams, - tableName, + switch (operation) { + case 'clear_range': + if (!trimmedRange) { + throw new Error('A range is required to clear cells.') + } + return { ...base, range: trimmedRange, applyTo: optionalString(applyTo) } + case 'format_range': + if (!trimmedRange) { + throw new Error('A range is required to format cells.') + } + return { + ...base, + range: trimmedRange, + fillColor: optionalString(fillColor), + fontBold: optionalBoolean(fontBold), + fontItalic: optionalBoolean(fontItalic), + fontColor: optionalString(fontColor), + fontSize: optionalNumber(fontSize), + fontName: optionalString(fontName), + } + case 'create_table': { + if (!trimmedRange) { + throw new Error('A range is required to create a table.') + } + return { + ...base, + address: trimmedRange, + hasHeaders: optionalBoolean(tableHasHeaders) ?? true, + } } - } + case 'delete_worksheet': { + const effectiveWorksheetName = worksheetName ? String(worksheetName).trim() : '' + if (!effectiveWorksheetName) { + throw new Error('Worksheet name is required to delete a worksheet.') + } + return { ...base, worksheetName: effectiveWorksheetName } + } + case 'sort_range': { + const effectiveTableName = optionalString(sortTableName) + if (!effectiveTableName && !trimmedRange) { + throw new Error('A range or table name is required to sort.') + } + return { + ...base, + range: trimmedRange, + tableName: effectiveTableName, + sortColumn: optionalNumber(sortColumn), + sortAscending: optionalBoolean(sortAscending) ?? true, + hasHeaders: optionalBoolean(sortHasHeaders), + matchCase: optionalBoolean(sortMatchCase), + } + } + default: { + let parsedValues + try { + parsedValues = values ? JSON.parse(values as string) : undefined + } catch { + throw new Error('Invalid JSON format for values') + } - if (params.operation === 'worksheet_add') { - return { - ...baseParams, - worksheetName, + if (operation === 'table_add' && !tableName) { + throw new Error('Table name is required for table operations.') + } + if (operation === 'worksheet_add' && !worksheetName) { + throw new Error('Worksheet name is required for worksheet operations.') + } + + const baseParams = { + ...base, + range: trimmedRange, + values: parsedValues, + valueInputOption, + } + + if (operation === 'table_add') { + return { ...baseParams, tableName } + } + if (operation === 'worksheet_add') { + return { ...baseParams, worksheetName } + } + return baseParams } } - - return baseParams }, }, }, @@ -323,6 +605,19 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, worksheetName: { type: 'string', description: 'Worksheet name' }, values: { type: 'string', description: 'Cell values data' }, valueInputOption: { type: 'string', description: 'Value input option' }, + applyTo: { type: 'string', description: 'What to clear (All, Contents, Formats)' }, + fillColor: { type: 'string', description: 'Background fill color' }, + fontBold: { type: 'string', description: 'Bold font toggle' }, + fontItalic: { type: 'string', description: 'Italic font toggle' }, + fontColor: { type: 'string', description: 'Font color' }, + fontSize: { type: 'string', description: 'Font size in points' }, + fontName: { type: 'string', description: 'Font name' }, + tableHasHeaders: { type: 'string', description: 'Whether the table range has a header row' }, + sortTableName: { type: 'string', description: 'Table name to sort (optional)' }, + sortColumn: { type: 'string', description: 'Zero-based column index to sort on' }, + sortAscending: { type: 'string', description: 'Sort order (ascending/descending)' }, + sortHasHeaders: { type: 'string', description: 'Whether the range has a header row' }, + sortMatchCase: { type: 'string', description: 'Whether casing affects ordering' }, }, outputs: { data: { type: 'json', description: 'Excel range data with sheet information and cell values' }, @@ -343,6 +638,28 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, type: 'json', description: 'Details of the newly created worksheet (worksheet_add operations)', }, + cleared: { type: 'boolean', description: 'Whether the range was cleared (clear_range)' }, + applyTo: { type: 'string', description: 'What was cleared (clear_range)' }, + formatted: { type: 'boolean', description: 'Whether formatting was applied (format_range)' }, + fill: { type: 'json', description: 'Applied fill ({color}) or null (format_range)' }, + font: { + type: 'json', + description: 'Applied font ({bold, italic, color, name, size}) or null (format_range)', + }, + table: { + type: 'json', + description: 'Created table ({id, name, showHeaders, showTotals, style}) (create_table)', + }, + deleted: { + type: 'boolean', + description: 'Whether the worksheet was deleted (delete_worksheet)', + }, + worksheetName: { + type: 'string', + description: 'Name of the deleted worksheet (delete_worksheet)', + }, + sorted: { type: 'boolean', description: 'Whether the sort was applied (sort_range)' }, + target: { type: 'string', description: 'The range or table name that was sorted (sort_range)' }, }, } @@ -368,6 +685,11 @@ export const MicrosoftExcelV2Block: BlockConfig = { options: [ { label: 'Read Data', id: 'read' }, { label: 'Write Data', id: 'write' }, + { label: 'Clear Range', id: 'clear_range' }, + { label: 'Format Range', id: 'format_range' }, + { label: 'Create Table', id: 'create_table' }, + { label: 'Sort Range', id: 'sort_range' }, + { label: 'Delete Worksheet', id: 'delete_worksheet' }, ], value: () => 'read', }, @@ -497,12 +819,17 @@ export const MicrosoftExcelV2Block: BlockConfig = { }, mode: 'advanced', }, - // Cell Range (optional for read/write) + // Cell Range (used by read/write/clear/format/create_table/sort) { id: 'cellRange', title: 'Cell Range', type: 'short-input', placeholder: 'Cell range (e.g., A1:D10). Defaults to used range for read, A1 for write.', + condition: { field: 'operation', value: 'delete_worksheet', not: true }, + required: { + field: 'operation', + value: ['clear_range', 'format_range', 'create_table'], + }, wandConfig: { enabled: true, prompt: `Generate a valid cell range based on the user's description. @@ -566,24 +893,167 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, ], condition: { field: 'operation', value: 'write' }, }, + // Clear Range + { + id: 'applyTo', + title: 'Clear', + type: 'dropdown', + options: [ + { label: 'All (contents and formats)', id: 'All' }, + { label: 'Contents only', id: 'Contents' }, + { label: 'Formats only', id: 'Formats' }, + ], + value: () => 'All', + condition: { field: 'operation', value: 'clear_range' }, + }, + // Format Range + { + id: 'fillColor', + title: 'Fill Color', + type: 'short-input', + placeholder: 'Hex color (e.g., #FFFF00)', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontBold', + title: 'Bold', + type: 'dropdown', + options: [ + { label: 'No change', id: '' }, + { label: 'Bold', id: 'true' }, + { label: 'Not bold', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontColor', + title: 'Font Color', + type: 'short-input', + placeholder: 'Hex color (e.g., #FF0000)', + condition: { field: 'operation', value: 'format_range' }, + }, + { + id: 'fontItalic', + title: 'Italic', + type: 'dropdown', + options: [ + { label: 'No change', id: '' }, + { label: 'Italic', id: 'true' }, + { label: 'Not italic', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + { + id: 'fontSize', + title: 'Font Size', + type: 'short-input', + placeholder: 'Font size in points (e.g., 12)', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + { + id: 'fontName', + title: 'Font Name', + type: 'short-input', + placeholder: 'Font name (e.g., Calibri)', + condition: { field: 'operation', value: 'format_range' }, + mode: 'advanced', + }, + // Create Table + { + id: 'tableHasHeaders', + title: 'First Row Has Headers', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'create_table' }, + }, + // Sort Range + { + id: 'sortTableName', + title: 'Table Name', + type: 'short-input', + placeholder: 'Optional: sort a table instead of the cell range', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, + { + id: 'sortColumn', + title: 'Sort Column Index', + type: 'short-input', + placeholder: 'Zero-based column index (0 = first column)', + condition: { field: 'operation', value: 'sort_range' }, + required: { field: 'operation', value: 'sort_range' }, + }, + { + id: 'sortAscending', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'true' }, + { label: 'Descending', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'sort_range' }, + }, + { + id: 'sortHasHeaders', + title: 'Range Has Header Row', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, + { + id: 'sortMatchCase', + title: 'Match Case', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'sort_range' }, + mode: 'advanced', + }, ], tools: { - access: ['microsoft_excel_read_v2', 'microsoft_excel_write_v2'], + access: [ + 'microsoft_excel_read_v2', + 'microsoft_excel_write_v2', + 'microsoft_excel_clear_range', + 'microsoft_excel_format_range', + 'microsoft_excel_create_table', + 'microsoft_excel_delete_worksheet', + 'microsoft_excel_sort_range', + ], config: { - tool: createVersionedToolSelector({ - baseToolSelector: (params) => { - switch (params.operation) { - case 'read': - return 'microsoft_excel_read' - case 'write': - return 'microsoft_excel_write' - default: - throw new Error(`Invalid Microsoft Excel operation: ${params.operation}`) - } - }, - suffix: '_v2', - fallbackToolId: 'microsoft_excel_read_v2', - }), + tool: (params) => { + switch (params.operation) { + case 'clear_range': + return 'microsoft_excel_clear_range' + case 'format_range': + return 'microsoft_excel_format_range' + case 'create_table': + return 'microsoft_excel_create_table' + case 'delete_worksheet': + return 'microsoft_excel_delete_worksheet' + case 'sort_range': + return 'microsoft_excel_sort_range' + default: + return versionedReadWriteSelector(params) + } + }, params: (params) => { const { oauthCredential, @@ -594,31 +1064,109 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, driveId, siteId: _siteId, fileSource: _fileSource, - ...rest + operation, + applyTo, + fillColor, + fontBold, + fontItalic, + fontColor, + fontSize, + fontName, + tableHasHeaders, + sortTableName, + sortColumn, + sortAscending, + sortHasHeaders, + sortMatchCase, + valueInputOption, } = params - const parsedValues = values ? JSON.parse(values as string) : undefined - const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : '' const effectiveSheetName = sheetName ? String(sheetName).trim() : '' + const effectiveDriveId = driveId ? String(driveId).trim() : undefined + const trimmedRange = cellRange ? String(cellRange).trim() : undefined if (!effectiveSpreadsheetId) { throw new Error('Spreadsheet ID is required.') } - if (!effectiveSheetName) { - throw new Error('Sheet name is required. Please select or enter a sheet name.') - } - - return { - ...rest, + const base = { spreadsheetId: effectiveSpreadsheetId, - sheetName: effectiveSheetName, - cellRange: cellRange ? (cellRange as string).trim() : undefined, - driveId: driveId ? String(driveId).trim() : undefined, - values: parsedValues, + driveId: effectiveDriveId, oauthCredential, } + + switch (operation) { + case 'clear_range': + return { + ...base, + sheetName: effectiveSheetName || undefined, + range: trimmedRange, + applyTo: optionalString(applyTo), + } + case 'format_range': + return { + ...base, + sheetName: effectiveSheetName || undefined, + range: trimmedRange, + fillColor: optionalString(fillColor), + fontBold: optionalBoolean(fontBold), + fontItalic: optionalBoolean(fontItalic), + fontColor: optionalString(fontColor), + fontSize: optionalNumber(fontSize), + fontName: optionalString(fontName), + } + case 'create_table': { + if (!effectiveSheetName) { + throw new Error('Sheet name is required to create a table.') + } + if (!trimmedRange) { + throw new Error('A cell range is required to create a table.') + } + return { + ...base, + address: `${quoteSheetName(effectiveSheetName)}!${trimmedRange}`, + hasHeaders: optionalBoolean(tableHasHeaders) ?? true, + } + } + case 'delete_worksheet': + if (!effectiveSheetName) { + throw new Error('Sheet name is required to delete a worksheet.') + } + return { + ...base, + worksheetName: effectiveSheetName, + } + case 'sort_range': { + const tableName = optionalString(sortTableName) + if (!tableName && !trimmedRange) { + throw new Error('A cell range or table name is required to sort.') + } + return { + ...base, + sheetName: effectiveSheetName || undefined, + range: trimmedRange, + tableName, + sortColumn: optionalNumber(sortColumn), + sortAscending: optionalBoolean(sortAscending) ?? true, + hasHeaders: optionalBoolean(sortHasHeaders), + matchCase: optionalBoolean(sortMatchCase), + } + } + default: { + if (!effectiveSheetName) { + throw new Error('Sheet name is required. Please select or enter a sheet name.') + } + const parsedValues = values ? JSON.parse(values as string) : undefined + return { + ...base, + sheetName: effectiveSheetName, + cellRange: trimmedRange, + values: parsedValues, + valueInputOption, + } + } + } }, }, }, @@ -633,6 +1181,19 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, values: { type: 'string', description: 'Cell values data' }, valueInputOption: { type: 'string', description: 'Value input option' }, + applyTo: { type: 'string', description: 'What to clear (All, Contents, Formats)' }, + fillColor: { type: 'string', description: 'Background fill color' }, + fontBold: { type: 'string', description: 'Bold font toggle' }, + fontItalic: { type: 'string', description: 'Italic font toggle' }, + fontColor: { type: 'string', description: 'Font color' }, + fontSize: { type: 'string', description: 'Font size in points' }, + fontName: { type: 'string', description: 'Font name' }, + tableHasHeaders: { type: 'string', description: 'Whether the table range has a header row' }, + sortTableName: { type: 'string', description: 'Table name to sort (optional)' }, + sortColumn: { type: 'string', description: 'Zero-based column index to sort on' }, + sortAscending: { type: 'string', description: 'Sort order (ascending/descending)' }, + sortHasHeaders: { type: 'string', description: 'Whether the range has a header row' }, + sortMatchCase: { type: 'string', description: 'Whether casing affects ordering' }, }, outputs: { sheetName: { @@ -670,6 +1231,56 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, description: 'Updated cells count', condition: { field: 'operation', value: 'write' }, }, + cleared: { + type: 'boolean', + description: 'Whether the range was cleared', + condition: { field: 'operation', value: 'clear_range' }, + }, + applyTo: { + type: 'string', + description: 'What was cleared (All, Contents, or Formats)', + condition: { field: 'operation', value: 'clear_range' }, + }, + formatted: { + type: 'boolean', + description: 'Whether the formatting was applied', + condition: { field: 'operation', value: 'format_range' }, + }, + fill: { + type: 'json', + description: 'Applied fill ({color}) or null', + condition: { field: 'operation', value: 'format_range' }, + }, + font: { + type: 'json', + description: 'Applied font ({bold, italic, color, name, size}) or null', + condition: { field: 'operation', value: 'format_range' }, + }, + table: { + type: 'json', + description: 'Created table ({id, name, showHeaders, showTotals, style})', + condition: { field: 'operation', value: 'create_table' }, + }, + deleted: { + type: 'boolean', + description: 'Whether the worksheet was deleted', + condition: { field: 'operation', value: 'delete_worksheet' }, + }, + worksheetName: { + type: 'string', + description: 'Name of the deleted worksheet', + condition: { field: 'operation', value: 'delete_worksheet' }, + }, + sorted: { + type: 'boolean', + description: 'Whether the sort was applied', + condition: { field: 'operation', value: 'sort_range' }, + }, + target: { + type: 'string', + description: 'The range or table name that was sorted', + condition: { field: 'operation', value: 'sort_range' }, + }, metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' }, }, } @@ -776,6 +1387,20 @@ export const MicrosoftExcelBlockMeta = { content: '# Add Worksheet\n\nCreate a fresh worksheet inside a workbook.\n\n## Steps\n1. Identify the workbook to add the sheet to.\n2. Choose a name for the new worksheet.\n3. Use Add Worksheet to create it, then write headers or data with Write/Update Data if needed.\n\n## Output\nThe new worksheet name and confirmation it was created in the workbook.', }, + { + name: 'build-formatted-table', + description: + 'Write data to a range, convert it into a formatted Excel table, and highlight the header row.', + content: + '# Build Formatted Table\n\nTurn raw rows into a structured, readable Excel table.\n\n## Steps\n1. Use Write/Update Data to put the rows into a range (e.g. A1:D20).\n2. Use Create Table over that range with headers enabled so filtering and references work.\n3. Use Format Range on the header row to apply a fill color and bold font for emphasis.\n\n## Output\nThe created table details plus confirmation the header formatting was applied.', + }, + { + name: 'sort-and-clean-range', + description: + 'Sort a range or table by a column and clear stale cells so the sheet stays tidy.', + content: + '# Sort and Clean Range\n\nKeep a worksheet ordered and free of leftover data.\n\n## Steps\n1. Use Sort Range with the target range or table name and the column index to sort on.\n2. Choose ascending or descending order.\n3. Use Clear Range on any obsolete cells, choosing to clear contents, formats, or both.\n\n## Output\nConfirmation of the sort and which cells were cleared.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/whatsapp.ts b/apps/sim/blocks/blocks/whatsapp.ts index 1cf9601a22d..16eba080700 100644 --- a/apps/sim/blocks/blocks/whatsapp.ts +++ b/apps/sim/blocks/blocks/whatsapp.ts @@ -9,7 +9,8 @@ export const WhatsAppBlock: BlockConfig = { name: 'WhatsApp', description: 'Send WhatsApp messages', authMode: AuthMode.ApiKey, - longDescription: 'Integrate WhatsApp into the workflow. Can send messages.', + longDescription: + 'Integrate WhatsApp into the workflow. Send text, template, media, and interactive messages, react to messages, and mark messages as read through the WhatsApp Cloud API.', docsLink: 'https://docs.sim.ai/integrations/whatsapp', category: 'tools', integrationType: IntegrationType.Communication, @@ -18,19 +19,35 @@ export const WhatsAppBlock: BlockConfig = { icon: WhatsAppIcon, triggerAllowed: true, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Message', id: 'send_message' }, + { label: 'Send Template', id: 'send_template' }, + { label: 'Send Media', id: 'send_media' }, + { label: 'Send Interactive', id: 'send_interactive' }, + { label: 'Send Reaction', id: 'send_reaction' }, + { label: 'Mark As Read', id: 'mark_read' }, + ], + defaultValue: 'send_message', + }, { id: 'phoneNumber', title: 'Recipient Phone Number', type: 'short-input', placeholder: 'Enter phone number with country code (e.g., +1234567890)', - required: true, + condition: { field: 'operation', value: 'mark_read', not: true }, + required: { field: 'operation', value: 'mark_read', not: true }, }, { id: 'message', title: 'Message', type: 'long-input', placeholder: 'Enter your message', - required: true, + condition: { field: 'operation', value: 'send_message' }, + required: { field: 'operation', value: 'send_message' }, }, { id: 'previewUrl', @@ -43,9 +60,157 @@ export const WhatsAppBlock: BlockConfig = { defaultValue: 'false', description: 'Have WhatsApp attempt to render a link preview for the first URL in the message.', + condition: { field: 'operation', value: 'send_message' }, + required: false, + mode: 'advanced', + }, + { + id: 'templateName', + title: 'Template Name', + type: 'short-input', + placeholder: 'Name of the approved template', + condition: { field: 'operation', value: 'send_template' }, + required: { field: 'operation', value: 'send_template' }, + }, + { + id: 'languageCode', + title: 'Template Language', + type: 'short-input', + placeholder: 'e.g., en_US', + defaultValue: 'en_US', + condition: { field: 'operation', value: 'send_template' }, + required: { field: 'operation', value: 'send_template' }, + }, + { + id: 'components', + title: 'Template Components', + type: 'long-input', + placeholder: '[{"type":"body","parameters":[{"type":"text","text":"value"}]}]', + description: 'JSON array of template components with variable parameters.', + condition: { field: 'operation', value: 'send_template' }, + required: false, + mode: 'advanced', + }, + { + id: 'mediaType', + title: 'Media Type', + type: 'dropdown', + options: [ + { label: 'Image', id: 'image' }, + { label: 'Document', id: 'document' }, + { label: 'Video', id: 'video' }, + { label: 'Audio', id: 'audio' }, + ], + defaultValue: 'image', + condition: { field: 'operation', value: 'send_media' }, + required: { field: 'operation', value: 'send_media' }, + }, + { + id: 'mediaLink', + title: 'Media Link', + type: 'short-input', + placeholder: 'Public HTTPS URL of the media', + condition: { field: 'operation', value: 'send_media' }, + required: false, + }, + { + id: 'mediaId', + title: 'Media ID', + type: 'short-input', + placeholder: 'ID of media uploaded to WhatsApp', + description: 'Provide a Media Link or a Media ID.', + condition: { field: 'operation', value: 'send_media' }, required: false, mode: 'advanced', }, + { + id: 'caption', + title: 'Caption', + type: 'long-input', + placeholder: 'Optional caption (image, video, or document)', + condition: { field: 'operation', value: 'send_media' }, + required: false, + mode: 'advanced', + }, + { + id: 'filename', + title: 'File Name', + type: 'short-input', + placeholder: 'Optional file name for documents', + condition: { field: 'operation', value: 'send_media' }, + required: false, + mode: 'advanced', + }, + { + id: 'bodyText', + title: 'Body Text', + type: 'long-input', + placeholder: 'Main message body', + condition: { field: 'operation', value: 'send_interactive' }, + required: { field: 'operation', value: 'send_interactive' }, + }, + { + id: 'buttons', + title: 'Reply Buttons', + type: 'long-input', + placeholder: '[{"type":"reply","reply":{"id":"yes","title":"Yes"}}]', + description: 'JSON array of reply buttons (max 3). Provide buttons or sections.', + condition: { field: 'operation', value: 'send_interactive' }, + required: false, + }, + { + id: 'listButtonText', + title: 'List Button Text', + type: 'short-input', + placeholder: 'e.g., Menu', + description: 'Label for the button that opens the list. Required when sending a list.', + condition: { field: 'operation', value: 'send_interactive' }, + required: false, + }, + { + id: 'sections', + title: 'List Sections', + type: 'long-input', + placeholder: '[{"title":"Section","rows":[{"id":"r1","title":"Row 1"}]}]', + description: 'JSON array of list sections. Provide sections or buttons.', + condition: { field: 'operation', value: 'send_interactive' }, + required: false, + mode: 'advanced', + }, + { + id: 'headerText', + title: 'Header Text', + type: 'short-input', + placeholder: 'Optional plain-text header', + condition: { field: 'operation', value: 'send_interactive' }, + required: false, + mode: 'advanced', + }, + { + id: 'footerText', + title: 'Footer Text', + type: 'short-input', + placeholder: 'Optional footer text', + condition: { field: 'operation', value: 'send_interactive' }, + required: false, + mode: 'advanced', + }, + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + placeholder: 'wamid of the target message', + condition: { field: 'operation', value: ['send_reaction', 'mark_read'] }, + required: { field: 'operation', value: ['send_reaction', 'mark_read'] }, + }, + { + id: 'emoji', + title: 'Emoji', + type: 'short-input', + placeholder: 'e.g., 👍 (leave empty to remove a reaction)', + condition: { field: 'operation', value: 'send_reaction' }, + required: false, + }, { id: 'phoneNumberId', title: 'WhatsApp Phone Number ID', @@ -64,9 +229,16 @@ export const WhatsAppBlock: BlockConfig = { ...getTrigger('whatsapp_webhook').subBlocks, ], tools: { - access: ['whatsapp_send_message'], + access: [ + 'whatsapp_send_message', + 'whatsapp_send_template', + 'whatsapp_send_media', + 'whatsapp_send_interactive', + 'whatsapp_send_reaction', + 'whatsapp_mark_read', + ], config: { - tool: () => 'whatsapp_send_message', + tool: (params) => `whatsapp_${params.operation || 'send_message'}`, params: (params) => ({ ...params, previewUrl: @@ -75,9 +247,26 @@ export const WhatsAppBlock: BlockConfig = { }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, phoneNumber: { type: 'string', description: 'Recipient phone number' }, message: { type: 'string', description: 'Message text' }, previewUrl: { type: 'boolean', description: 'Whether to render a preview for the first URL' }, + templateName: { type: 'string', description: 'Approved template name' }, + languageCode: { type: 'string', description: 'Template language code (e.g., en_US)' }, + components: { type: 'json', description: 'Template components with variable parameters' }, + mediaType: { type: 'string', description: 'Media type: image, document, video, or audio' }, + mediaLink: { type: 'string', description: 'Public HTTPS URL of the media' }, + mediaId: { type: 'string', description: 'ID of media uploaded to WhatsApp' }, + caption: { type: 'string', description: 'Caption for image, video, or document media' }, + filename: { type: 'string', description: 'File name for document media' }, + bodyText: { type: 'string', description: 'Interactive message body text' }, + headerText: { type: 'string', description: 'Interactive message header text' }, + footerText: { type: 'string', description: 'Interactive message footer text' }, + buttons: { type: 'json', description: 'Reply buttons for an interactive message' }, + listButtonText: { type: 'string', description: 'Label for the list menu button' }, + sections: { type: 'json', description: 'List sections for an interactive message' }, + messageId: { type: 'string', description: 'Target message ID (wamid)' }, + emoji: { type: 'string', description: 'Reaction emoji (empty to remove)' }, phoneNumberId: { type: 'string', description: 'WhatsApp phone number ID' }, accessToken: { type: 'string', description: 'WhatsApp access token' }, }, @@ -178,7 +367,7 @@ export const WhatsAppBlock: BlockConfig = { } export const WhatsAppBlockMeta = { - tags: ['messaging', 'automation'], + tags: ['messaging', 'automation', 'customer-support', 'marketing'], url: 'https://www.whatsapp.com', templates: [ { diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index c08e20fec69..7f9f88a0c49 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-27", + "updatedAt": "2026-06-29", "integrations": [ { "type": "onepassword", @@ -393,7 +393,7 @@ "slug": "airtable", "name": "Airtable", "description": "Read, create, and update Airtable", - "longDescription": "Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.", + "longDescription": "Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, update, upsert, or delete records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.", "bgColor": "#FFFFFF", "iconName": "AirtableIcon", "docsUrl": "https://docs.sim.ai/integrations/airtable", @@ -429,9 +429,17 @@ { "name": "Update Multiple Records", "description": "Update multiple existing records in an Airtable table" + }, + { + "name": "Upsert Records", + "description": "Update existing records or create new ones in an Airtable table, matching on the specified merge fields" + }, + { + "name": "Delete Records", + "description": "Delete one or more records from an Airtable table by ID" } ], - "operationCount": 8, + "operationCount": 10, "triggers": [ { "id": "airtable_webhook", @@ -2658,8 +2666,44 @@ } ], "operationCount": 11, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "clerk_user_created", + "name": "Clerk User Created", + "description": "Trigger workflow when a Clerk user is created" + }, + { + "id": "clerk_user_updated", + "name": "Clerk User Updated", + "description": "Trigger workflow when a Clerk user is updated" + }, + { + "id": "clerk_user_deleted", + "name": "Clerk User Deleted", + "description": "Trigger workflow when a Clerk user is deleted" + }, + { + "id": "clerk_session_created", + "name": "Clerk Session Created", + "description": "Trigger workflow when a Clerk session is created" + }, + { + "id": "clerk_organization_created", + "name": "Clerk Organization Created", + "description": "Trigger workflow when a Clerk organization is created" + }, + { + "id": "clerk_organization_membership_created", + "name": "Clerk Organization Membership Created", + "description": "Trigger workflow when a Clerk organization membership is created" + }, + { + "id": "clerk_webhook", + "name": "Clerk Webhook", + "description": "Trigger workflow on any Clerk webhook event" + } + ], + "triggerCount": 7, "authType": "none", "category": "tools", "integrationType": "security", @@ -4274,7 +4318,7 @@ "authType": "api-key", "category": "tools", "integrationType": "observability", - "tags": ["monitoring", "incident-management", "observability"] + "tags": ["monitoring", "incident-management"] }, { "type": "dropbox", @@ -6233,8 +6277,8 @@ "type": "google_docs", "slug": "google-docs", "name": "Google Docs", - "description": "Read, write, and create documents", - "longDescription": "Integrate Google Docs into the workflow. Can read, write, and create documents.", + "description": "Read, write, create, and edit documents", + "longDescription": "Integrate Google Docs into the workflow. Read, write, and create documents, insert text, tables, images, and page breaks, find and replace text, and apply text styling.", "bgColor": "#FFFFFF", "iconName": "GoogleDocsIcon", "docsUrl": "https://docs.sim.ai/integrations/google_docs", @@ -6250,9 +6294,33 @@ { "name": "Create Document", "description": "Create a new Google Docs document" + }, + { + "name": "Insert Text", + "description": "Insert text at a specific index in a Google Docs document. When no index is provided, text is appended to the end of the document. Text is inserted literally; Markdown is not interpreted." + }, + { + "name": "Find & Replace Text", + "description": "Replace all occurrences of a search string with new text across a Google Docs document." + }, + { + "name": "Insert Table", + "description": "Insert an empty table with the given number of rows and columns into a Google Docs document. When no index is provided, the table is appended to the end of the document." + }, + { + "name": "Insert Image", + "description": "Insert an inline image from a public URL into a Google Docs document. The image must be publicly accessible and under 50 MB. When no index is provided, the image is appended to the end of the document." + }, + { + "name": "Insert Page Break", + "description": "Insert a page break into a Google Docs document. When no index is provided, the page break is appended to the end of the document." + }, + { + "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." } ], - "operationCount": 3, + "operationCount": 9, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -8023,8 +8091,29 @@ } ], "operationCount": 46, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "incidentio_incident_created", + "name": "incident.io Incident Created", + "description": "Trigger workflow when an incident is created in incident.io" + }, + { + "id": "incidentio_incident_updated", + "name": "incident.io Incident Updated", + "description": "Trigger workflow when an incident is updated in incident.io" + }, + { + "id": "incidentio_incident_status_updated", + "name": "incident.io Incident Status Updated", + "description": "Trigger workflow when an incident" + }, + { + "id": "incidentio_alert_created", + "name": "incident.io Alert Created", + "description": "Trigger workflow when an alert is created in incident.io" + } + ], + "triggerCount": 4, "authType": "api-key", "category": "tools", "integrationType": "observability", @@ -9924,8 +10013,49 @@ } ], "operationCount": 10, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "loops_email_delivered", + "name": "Loops Email Delivered", + "description": "Trigger workflow when a Loops email is delivered" + }, + { + "id": "loops_email_opened", + "name": "Loops Email Opened", + "description": "Trigger workflow when a Loops email is opened" + }, + { + "id": "loops_email_clicked", + "name": "Loops Email Clicked", + "description": "Trigger workflow when a link in a Loops email is clicked" + }, + { + "id": "loops_email_hard_bounced", + "name": "Loops Email Hard Bounced", + "description": "Trigger workflow when a Loops email hard bounces" + }, + { + "id": "loops_email_soft_bounced", + "name": "Loops Email Soft Bounced", + "description": "Trigger workflow when a Loops email soft bounces" + }, + { + "id": "loops_campaign_email_sent", + "name": "Loops Campaign Email Sent", + "description": "Trigger workflow when a Loops campaign email is sent" + }, + { + "id": "loops_loop_email_sent", + "name": "Loops Loop Email Sent", + "description": "Trigger workflow when a Loops loop email is sent" + }, + { + "id": "loops_transactional_email_sent", + "name": "Loops Transactional Email Sent", + "description": "Trigger workflow when a Loops transactional email is sent" + } + ], + "triggerCount": 8, "authType": "api-key", "category": "tools", "integrationType": "email", @@ -10492,9 +10622,29 @@ { "name": "Write Data", "description": "Write data to a Microsoft Excel spreadsheet" + }, + { + "name": "Clear Range", + "description": "Clear the values and/or formatting of a range in a Microsoft Excel worksheet" + }, + { + "name": "Format Range", + "description": "Apply fill color and/or font formatting to a range in a Microsoft Excel worksheet" + }, + { + "name": "Create Table", + "description": "Create a new table over a range of cells in a Microsoft Excel workbook" + }, + { + "name": "Sort Range", + "description": "Sort a range or table by a column in a Microsoft Excel worksheet" + }, + { + "name": "Delete Worksheet", + "description": "Delete a worksheet (sheet) from a Microsoft Excel workbook" } ], - "operationCount": 2, + "operationCount": 7, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -13084,8 +13234,39 @@ } ], "operationCount": 10, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "revenuecat_initial_purchase", + "name": "RevenueCat Initial Purchase", + "description": "Trigger workflow when a subscriber makes their first purchase in RevenueCat" + }, + { + "id": "revenuecat_renewal", + "name": "RevenueCat Renewal", + "description": "Trigger workflow when a RevenueCat subscription renews" + }, + { + "id": "revenuecat_cancellation", + "name": "RevenueCat Cancellation", + "description": "Trigger workflow when a subscriber cancels a RevenueCat subscription" + }, + { + "id": "revenuecat_expiration", + "name": "RevenueCat Expiration", + "description": "Trigger workflow when a RevenueCat subscription expires" + }, + { + "id": "revenuecat_non_renewing_purchase", + "name": "RevenueCat Non-Renewing Purchase", + "description": "Trigger workflow when a subscriber makes a non-renewing purchase in RevenueCat" + }, + { + "id": "revenuecat_product_change", + "name": "RevenueCat Product Change", + "description": "Trigger workflow when a subscriber changes their RevenueCat subscription product" + } + ], + "triggerCount": 6, "authType": "api-key", "category": "tools", "integrationType": "commerce", @@ -13630,8 +13811,29 @@ } ], "operationCount": 41, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "rootly_incident_created", + "name": "Rootly Incident Created", + "description": "Trigger workflow when a new incident is created in Rootly" + }, + { + "id": "rootly_incident_updated", + "name": "Rootly Incident Updated", + "description": "Trigger workflow when an incident is updated in Rootly" + }, + { + "id": "rootly_incident_resolved", + "name": "Rootly Incident Resolved", + "description": "Trigger workflow when an incident is resolved in Rootly" + }, + { + "id": "rootly_alert_created", + "name": "Rootly Alert Created", + "description": "Trigger workflow when a new alert is created in Rootly" + } + ], + "triggerCount": 4, "authType": "api-key", "category": "tools", "integrationType": "observability", @@ -14554,8 +14756,34 @@ } ], "operationCount": 13, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "sentry_issue_created", + "name": "Sentry Issue Created", + "description": "Trigger workflow when a new issue is created in Sentry" + }, + { + "id": "sentry_issue_resolved", + "name": "Sentry Issue Resolved", + "description": "Trigger workflow when an issue is resolved in Sentry" + }, + { + "id": "sentry_error_created", + "name": "Sentry Error Created", + "description": "Trigger workflow when a new error event is created in Sentry" + }, + { + "id": "sentry_issue_alert", + "name": "Sentry Issue Alert", + "description": "Trigger workflow when a Sentry issue alert rule fires" + }, + { + "id": "sentry_metric_alert", + "name": "Sentry Metric Alert", + "description": "Trigger workflow when a Sentry metric alert changes state (critical, warning, resolved)" + } + ], + "triggerCount": 5, "authType": "api-key", "category": "tools", "integrationType": "observability", @@ -17417,8 +17645,19 @@ "docsUrl": "https://docs.sim.ai/integrations/twilio_sms", "operations": [], "operationCount": 0, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "twilio_sms_received", + "name": "Twilio SMS Received", + "description": "Trigger workflow when an inbound SMS or MMS message is received via Twilio" + }, + { + "id": "twilio_sms_status", + "name": "Twilio Message Status", + "description": "Trigger workflow when a Twilio message status changes (sent, delivered, failed)" + } + ], + "triggerCount": 2, "authType": "api-key", "category": "tools", "integrationType": "communication", @@ -18245,12 +18484,37 @@ "slug": "whatsapp", "name": "WhatsApp", "description": "Send WhatsApp messages", - "longDescription": "Integrate WhatsApp into the workflow. Can send messages.", + "longDescription": "Integrate WhatsApp into the workflow. Send text, template, media, and interactive messages, react to messages, and mark messages as read through the WhatsApp Cloud API.", "bgColor": "#25D366", "iconName": "WhatsAppIcon", "docsUrl": "https://docs.sim.ai/integrations/whatsapp", - "operations": [], - "operationCount": 0, + "operations": [ + { + "name": "Send Message", + "description": "Send a text message through the WhatsApp Cloud API." + }, + { + "name": "Send Template", + "description": "Send a pre-approved WhatsApp template message with a language and optional variable components." + }, + { + "name": "Send Media", + "description": "Send an image, document, video, or audio message via a public link or an uploaded media ID." + }, + { + "name": "Send Interactive", + "description": "Send an interactive WhatsApp message with reply buttons or a selectable list." + }, + { + "name": "Send Reaction", + "description": "React to a WhatsApp message with an emoji. Send an empty emoji to remove an existing reaction." + }, + { + "name": "Mark As Read", + "description": "Mark a received WhatsApp message as read so the sender sees blue checkmarks." + } + ], + "operationCount": 6, "triggers": [ { "id": "whatsapp_webhook", @@ -18262,7 +18526,7 @@ "authType": "api-key", "category": "tools", "integrationType": "communication", - "tags": ["messaging", "automation"] + "tags": ["messaging", "automation", "customer-support", "marketing"] }, { "type": "wikipedia", diff --git a/apps/sim/tools/airtable/delete_records.ts b/apps/sim/tools/airtable/delete_records.ts new file mode 100644 index 00000000000..46b202d6474 --- /dev/null +++ b/apps/sim/tools/airtable/delete_records.ts @@ -0,0 +1,106 @@ +import type { AirtableDeleteParams, AirtableDeleteResponse } from '@/tools/airtable/types' +import type { ToolConfig } from '@/tools/types' + +export const airtableDeleteRecordsTool: ToolConfig = { + id: 'airtable_delete_records', + name: 'Airtable Delete Records', + description: 'Delete one or more records from an Airtable table by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'airtable', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + baseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table ID (starts with "tbl") or table name', + }, + recordIds: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of record IDs to delete (each starts with "rec", e.g., ["recXXXXXXXXXXXXXX"]). Pass a single-element array to delete one record.', + }, + }, + + request: { + url: (params) => { + const base = `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}` + const ids = (params.recordIds ?? []) + .map((id) => (id == null ? '' : String(id).trim())) + .filter(Boolean) + if (ids.length === 0) { + throw new Error('At least one record ID is required to delete') + } + if (ids.length > 10) { + throw new Error( + `Airtable deletes at most 10 records per request (received ${ids.length}). Split the delete into batches of 10 or fewer.` + ) + } + const queryParams = new URLSearchParams() + for (const id of ids) { + queryParams.append('records[]', id as string) + } + return `${base}?${queryParams.toString()}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const records = data.records ?? [] + return { + success: true, + output: { + records, + metadata: { + recordCount: records.length, + deletedRecordIds: records.map((r: { id: string }) => r.id), + }, + }, + } + }, + + outputs: { + records: { + type: 'array', + description: 'Array of deleted Airtable records', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Record ID' }, + deleted: { type: 'boolean', description: 'Whether the record was deleted' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata', + properties: { + recordCount: { type: 'number', description: 'Number of records deleted' }, + deletedRecordIds: { type: 'array', description: 'List of deleted record IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/airtable/index.ts b/apps/sim/tools/airtable/index.ts index 8b1ce4b150e..14b4ab56dce 100644 --- a/apps/sim/tools/airtable/index.ts +++ b/apps/sim/tools/airtable/index.ts @@ -1,4 +1,5 @@ import { airtableCreateRecordsTool } from '@/tools/airtable/create_records' +import { airtableDeleteRecordsTool } from '@/tools/airtable/delete_records' import { airtableGetBaseSchemaTool } from '@/tools/airtable/get_base_schema' import { airtableGetRecordTool } from '@/tools/airtable/get_record' import { airtableListBasesTool } from '@/tools/airtable/list_bases' @@ -6,9 +7,11 @@ import { airtableListRecordsTool } from '@/tools/airtable/list_records' import { airtableListTablesTool } from '@/tools/airtable/list_tables' import { airtableUpdateMultipleRecordsTool } from '@/tools/airtable/update_multiple_records' import { airtableUpdateRecordTool } from '@/tools/airtable/update_record' +import { airtableUpsertRecordsTool } from '@/tools/airtable/upsert_records' export { airtableCreateRecordsTool, + airtableDeleteRecordsTool, airtableGetBaseSchemaTool, airtableGetRecordTool, airtableListBasesTool, @@ -16,6 +19,7 @@ export { airtableListTablesTool, airtableUpdateMultipleRecordsTool, airtableUpdateRecordTool, + airtableUpsertRecordsTool, } export * from './types' diff --git a/apps/sim/tools/airtable/types.ts b/apps/sim/tools/airtable/types.ts index 75781044a61..499b4c898ed 100644 --- a/apps/sim/tools/airtable/types.ts +++ b/apps/sim/tools/airtable/types.ts @@ -161,6 +161,46 @@ export interface AirtableUpdateMultipleResponse extends ToolResponse { } } +// Delete Records Types +export interface AirtableDeleteParams extends AirtableBaseParams { + recordIds: string[] +} + +interface AirtableDeletedRecord { + id: string + deleted: boolean +} + +export interface AirtableDeleteResponse extends ToolResponse { + output: { + records: AirtableDeletedRecord[] + metadata: { + recordCount: number + deletedRecordIds: string[] + } + } +} + +// Upsert Records Types +export interface AirtableUpsertParams extends AirtableBaseParams { + records: Array<{ fields: Record }> + fieldsToMergeOn: string[] + typecast?: boolean +} + +export interface AirtableUpsertResponse extends ToolResponse { + output: { + records: AirtableRecord[] + createdRecords: string[] + updatedRecords: string[] + metadata: { + recordCount: number + createdCount: number + updatedCount: number + } + } +} + export type AirtableResponse = | AirtableListBasesResponse | AirtableListTablesResponse @@ -169,6 +209,8 @@ export type AirtableResponse = | AirtableCreateResponse | AirtableUpdateResponse | AirtableUpdateMultipleResponse + | AirtableDeleteResponse + | AirtableUpsertResponse | AirtableListBasesResponse | AirtableGetBaseSchemaResponse diff --git a/apps/sim/tools/airtable/upsert_records.ts b/apps/sim/tools/airtable/upsert_records.ts new file mode 100644 index 00000000000..cf4b94885a0 --- /dev/null +++ b/apps/sim/tools/airtable/upsert_records.ts @@ -0,0 +1,144 @@ +import type { AirtableUpsertParams, AirtableUpsertResponse } from '@/tools/airtable/types' +import type { ToolConfig } from '@/tools/types' + +export const airtableUpsertRecordsTool: ToolConfig = { + id: 'airtable_upsert_records', + name: 'Airtable Upsert Records', + description: + 'Update existing records or create new ones in an Airtable table, matching on the specified merge fields', + version: '1.0.0', + + oauth: { + required: true, + provider: 'airtable', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + baseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table ID (starts with "tbl") or table name', + }, + records: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Array of records to upsert, each with a `fields` object', + }, + fieldsToMergeOn: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: ["Name"]', + }, + typecast: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, Airtable automatically converts string values to the field type', + }, + }, + + request: { + url: (params) => + `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const mergeFields = (params.fieldsToMergeOn ?? []) + .map((f) => (f == null ? '' : String(f).trim())) + .filter(Boolean) + if (mergeFields.length === 0) { + throw new Error('At least one field to merge on is required for upsert') + } + if (mergeFields.length > 3) { + throw new Error( + `Airtable upsert accepts at most 3 fields to merge on (received ${mergeFields.length}).` + ) + } + const records = params.records ?? [] + if (records.length > 10) { + throw new Error( + `Airtable upserts at most 10 records per request (received ${records.length}). Split the upsert into batches of 10 or fewer.` + ) + } + const body: Record = { + performUpsert: { fieldsToMergeOn: mergeFields }, + records, + } + if (params.typecast != null) body.typecast = params.typecast + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const records = data.records ?? [] + const createdRecords = data.createdRecords ?? [] + const updatedRecords = data.updatedRecords ?? [] + return { + success: true, + output: { + records, + createdRecords, + updatedRecords, + metadata: { + recordCount: records.length, + createdCount: createdRecords.length, + updatedCount: updatedRecords.length, + }, + }, + } + }, + + outputs: { + records: { + type: 'array', + description: 'Array of upserted Airtable records', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Record ID' }, + createdTime: { type: 'string', description: 'Record creation timestamp' }, + fields: { type: 'json', description: 'Record field values' }, + }, + }, + }, + createdRecords: { + type: 'array', + description: 'IDs of records that were created', + items: { type: 'string', description: 'Created record ID' }, + }, + updatedRecords: { + type: 'array', + description: 'IDs of records that were updated', + items: { type: 'string', description: 'Updated record ID' }, + }, + metadata: { + type: 'json', + description: 'Operation metadata', + properties: { + recordCount: { type: 'number', description: 'Total number of records returned' }, + createdCount: { type: 'number', description: 'Number of records created' }, + updatedCount: { type: 'number', description: 'Number of records updated' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_docs/index.ts b/apps/sim/tools/google_docs/index.ts index 4b13d9b2784..f69d6de914a 100644 --- a/apps/sim/tools/google_docs/index.ts +++ b/apps/sim/tools/google_docs/index.ts @@ -1,7 +1,21 @@ import { createTool } from '@/tools/google_docs/create' +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 { updateTextStyleTool } from '@/tools/google_docs/update-text-style' import { writeTool } from '@/tools/google_docs/write' export const googleDocsReadTool = readTool export const googleDocsWriteTool = writeTool export const googleDocsCreateTool = createTool +export const googleDocsInsertTextTool = insertTextTool +export const googleDocsReplaceTextTool = replaceTextTool +export const googleDocsInsertTableTool = insertTableTool +export const googleDocsInsertImageTool = insertImageTool +export const googleDocsInsertPageBreakTool = insertPageBreakTool +export const googleDocsUpdateTextStyleTool = updateTextStyleTool + +export * from './types' diff --git a/apps/sim/tools/google_docs/insert-image.ts b/apps/sim/tools/google_docs/insert-image.ts new file mode 100644 index 00000000000..82818e49e90 --- /dev/null +++ b/apps/sim/tools/google_docs/insert-image.ts @@ -0,0 +1,132 @@ +import type { GoogleDocsInsertImageResponse, GoogleDocsToolParams } from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildInsertLocation, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const insertImageTool: ToolConfig = { + id: 'google_docs_insert_image', + name: 'Insert Image into Google Docs Document', + description: + 'Insert an inline image from a public URL into a Google Docs document. The image must be publicly accessible and under 50 MB. When no index is provided, the image is appended to the end of the document.', + 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 insert the image into', + }, + imageUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The publicly accessible URL of the image to insert', + }, + index: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'The 1-based character index at which to insert the image. When omitted, the image is appended to the end of the document.', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Optional image width in points (PT)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Optional image height in points (PT)', + }, + }, + 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) => { + if (!params.imageUrl) { + throw new Error('Image URL is required') + } + + const insertInlineImage: Record = { + ...buildInsertLocation(params.index), + uri: params.imageUrl, + } + + const objectSize: Record = {} + if (typeof params.width === 'number' && Number.isFinite(params.width)) { + objectSize.width = { magnitude: params.width, unit: 'PT' } + } + if (typeof params.height === 'number' && Number.isFinite(params.height)) { + objectSize.height = { magnitude: params.height, unit: 'PT' } + } + if (Object.keys(objectSize).length > 0) { + insertInlineImage.objectSize = objectSize + } + + return { + requests: [{ insertInlineImage }], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + const objectId = data.replies?.[0]?.insertInlineImage?.objectId ?? null + + return { + success: true, + output: { + objectId, + metadata, + }, + } + }, + + outputs: { + objectId: { + type: 'string', + description: 'The ID of the inserted inline image object', + 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/insert-page-break.ts b/apps/sim/tools/google_docs/insert-page-break.ts new file mode 100644 index 00000000000..7577cc56575 --- /dev/null +++ b/apps/sim/tools/google_docs/insert-page-break.ts @@ -0,0 +1,104 @@ +import type { + GoogleDocsInsertPageBreakResponse, + GoogleDocsToolParams, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildInsertLocation, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const insertPageBreakTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsInsertPageBreakResponse +> = { + id: 'google_docs_insert_page_break', + name: 'Insert Page Break into Google Docs Document', + description: + 'Insert a page break into a Google Docs document. When no index is provided, the page break is appended to the end of the document.', + 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 insert the page break into', + }, + index: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + '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.', + }, + }, + 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) => { + return { + requests: [ + { + insertPageBreak: { + ...buildInsertLocation(params.index), + }, + }, + ], + } + }, + }, + + 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 page break was inserted 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/insert-table.ts b/apps/sim/tools/google_docs/insert-table.ts new file mode 100644 index 00000000000..4fbf292a52d --- /dev/null +++ b/apps/sim/tools/google_docs/insert-table.ts @@ -0,0 +1,120 @@ +import type { GoogleDocsInsertTableResponse, GoogleDocsToolParams } from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildInsertLocation, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const insertTableTool: ToolConfig = { + id: 'google_docs_insert_table', + name: 'Insert Table into Google Docs Document', + description: + 'Insert an empty table with the given number of rows and columns into a Google Docs document. When no index is provided, the table is appended to the end of the document.', + 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 insert the table into', + }, + rows: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The number of rows in the table', + }, + columns: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The number of columns in the table', + }, + index: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'The 1-based character index at which to insert the table. When omitted, the table is appended to the end of the document.', + }, + }, + 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 rows = Number(params.rows) + const columns = Number(params.columns) + if (!Number.isFinite(rows) || rows < 1) { + throw new Error('Rows must be a positive number') + } + if (!Number.isFinite(columns) || columns < 1) { + throw new Error('Columns must be a positive number') + } + return { + requests: [ + { + insertTable: { + ...buildInsertLocation(params.index), + rows, + columns, + }, + }, + ], + } + }, + }, + + 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 table was inserted 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/insert-text.ts b/apps/sim/tools/google_docs/insert-text.ts new file mode 100644 index 00000000000..38dbc294538 --- /dev/null +++ b/apps/sim/tools/google_docs/insert-text.ts @@ -0,0 +1,108 @@ +import type { GoogleDocsInsertTextResponse, GoogleDocsToolParams } from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + buildInsertLocation, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const insertTextTool: ToolConfig = { + id: 'google_docs_insert_text', + name: 'Insert Text into Google Docs Document', + description: + 'Insert text at a specific index in a Google Docs document. When no index is provided, text is appended to the end of the document. Text is inserted literally; Markdown is not interpreted.', + 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 insert text into', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text to insert', + }, + index: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'The 1-based character index at which to insert the text. When omitted, text is appended to the end of the document.', + }, + }, + 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) => { + if (!params.text) { + throw new Error('Text is required') + } + return { + requests: [ + { + insertText: { + ...buildInsertLocation(params.index), + text: params.text, + }, + }, + ], + } + }, + }, + + 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 text was inserted 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/replace-text.ts b/apps/sim/tools/google_docs/replace-text.ts new file mode 100644 index 00000000000..dfc68b3093c --- /dev/null +++ b/apps/sim/tools/google_docs/replace-text.ts @@ -0,0 +1,117 @@ +import type { GoogleDocsReplaceTextResponse, GoogleDocsToolParams } from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + parseOptionalBoolean, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const replaceTextTool: ToolConfig = { + id: 'google_docs_replace_text', + name: 'Find and Replace Text in Google Docs Document', + description: + 'Replace all occurrences of a search string with new text across a Google Docs document.', + 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', + }, + searchText: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text to find', + }, + replaceText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The text to replace matches with. Use an empty string to delete matches.', + }, + matchCase: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the search should be case sensitive. Defaults to false.', + }, + }, + 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) => { + if (!params.searchText) { + throw new Error('Search text is required') + } + return { + requests: [ + { + replaceAllText: { + containsText: { + text: params.searchText, + matchCase: parseOptionalBoolean(params.matchCase) ?? false, + }, + replaceText: params.replaceText ?? '', + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const data = responseText.trim() ? JSON.parse(responseText) : {} + const metadata = buildBatchUpdateMetadata(data, response.url) + const occurrencesChanged = Number(data.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0) + + return { + success: true, + output: { + occurrencesChanged, + metadata, + }, + } + }, + + outputs: { + occurrencesChanged: { + type: 'number', + description: 'The number of occurrences that were replaced', + }, + 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/types.ts b/apps/sim/tools/google_docs/types.ts index 474673008b3..aae94a98d41 100644 --- a/apps/sim/tools/google_docs/types.ts +++ b/apps/sim/tools/google_docs/types.ts @@ -29,6 +29,48 @@ export interface GoogleDocsCreateResponse extends ToolResponse { } } +export interface GoogleDocsInsertTextResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsReplaceTextResponse extends ToolResponse { + output: { + occurrencesChanged: number + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsInsertTableResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsInsertImageResponse extends ToolResponse { + output: { + objectId: string | null + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsInsertPageBreakResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + +export interface GoogleDocsUpdateTextStyleResponse extends ToolResponse { + output: { + updatedContent: boolean + metadata: GoogleDocsMetadata + } +} + export interface GoogleDocsToolParams { accessToken: string documentId?: string @@ -38,9 +80,31 @@ export interface GoogleDocsToolParams { folderId?: string folderSelector?: string markdown?: boolean + text?: string + index?: number + searchText?: string + replaceText?: string + matchCase?: boolean + rows?: number + columns?: number + imageUrl?: string + width?: number + height?: number + startIndex?: number + endIndex?: number + bold?: boolean + italic?: boolean + underline?: boolean + fontSize?: number } export type GoogleDocsResponse = | GoogleDocsReadResponse | GoogleDocsWriteResponse | GoogleDocsCreateResponse + | GoogleDocsInsertTextResponse + | GoogleDocsReplaceTextResponse + | GoogleDocsInsertTableResponse + | GoogleDocsInsertImageResponse + | GoogleDocsInsertPageBreakResponse + | GoogleDocsUpdateTextStyleResponse diff --git a/apps/sim/tools/google_docs/update-text-style.ts b/apps/sim/tools/google_docs/update-text-style.ts new file mode 100644 index 00000000000..435737659d4 --- /dev/null +++ b/apps/sim/tools/google_docs/update-text-style.ts @@ -0,0 +1,174 @@ +import type { + GoogleDocsToolParams, + GoogleDocsUpdateTextStyleResponse, +} from '@/tools/google_docs/types' +import { + buildBatchUpdateMetadata, + parseOptionalBoolean, + resolveDocumentId, +} from '@/tools/google_docs/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateTextStyleTool: ToolConfig< + GoogleDocsToolParams, + GoogleDocsUpdateTextStyleResponse +> = { + id: 'google_docs_update_text_style', + name: 'Apply Text Style in Google Docs Document', + 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.', + 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)', + }, + bold: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to make the text bold', + }, + italic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to make the text italic', + }, + underline: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to underline the text', + }, + fontSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'The font size to apply, in points (PT)', + }, + }, + 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 startIndex = Number(params.startIndex) + const endIndex = Number(params.endIndex) + if (!Number.isFinite(startIndex) || !Number.isFinite(endIndex)) { + throw new Error('startIndex and endIndex are required') + } + if (endIndex <= startIndex) { + throw new Error('endIndex must be greater than startIndex') + } + + const textStyle: Record = {} + const fields: string[] = [] + + const bold = parseOptionalBoolean(params.bold) + if (bold !== undefined) { + textStyle.bold = bold + fields.push('bold') + } + const italic = parseOptionalBoolean(params.italic) + if (italic !== undefined) { + textStyle.italic = italic + fields.push('italic') + } + const underline = parseOptionalBoolean(params.underline) + if (underline !== undefined) { + textStyle.underline = underline + fields.push('underline') + } + const fontSize = Number(params.fontSize) + if (params.fontSize != null && Number.isFinite(fontSize)) { + textStyle.fontSize = { magnitude: fontSize, unit: 'PT' } + fields.push('fontSize') + } + + if (fields.length === 0) { + throw new Error( + 'At least one style (bold, italic, underline, or fontSize) must be provided' + ) + } + + return { + requests: [ + { + updateTextStyle: { + range: { startIndex, endIndex }, + textStyle, + 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 text 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 676e09dd295..f4d040c2f35 100644 --- a/apps/sim/tools/google_docs/utils.ts +++ b/apps/sim/tools/google_docs/utils.ts @@ -1,3 +1,78 @@ +/** + * Resolve the document ID for a Docs API request, preferring the selected + * document ID and falling back to a manually entered one. Throws when neither + * is present so callers fail loudly before issuing a request. + */ +export function resolveDocumentId(params: { + documentId?: string + manualDocumentId?: string +}): string { + const documentId = params.documentId?.trim() || params.manualDocumentId?.trim() + if (!documentId) { + throw new Error('Document ID is required') + } + return documentId +} + +/** + * Normalize an optional boolean param that may arrive as a real boolean or as a + * string (`"true"`/`"false"`), which happens when values come from block inputs + * or agent tool-calls rather than a typed UI switch. Returns `undefined` when the + * value is absent or unrecognized so callers can skip the field entirely. + */ +export function parseOptionalBoolean(value: unknown): boolean | undefined { + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase() + if (normalized === 'true') return true + if (normalized === 'false') return false + } + return undefined +} + +/** + * Build a `Location` for a batchUpdate request when an insertion index is + * provided, otherwise fall back to `endOfSegmentLocation` (end of the body). + * The Docs API treats index 0 as invalid, so a positive index is required to + * use an explicit location. + */ +export function buildInsertLocation( + index?: number +): { location: { index: number } } | { endOfSegmentLocation: Record } { + if (typeof index === 'number' && Number.isFinite(index) && index >= 1) { + return { location: { index } } + } + return { endOfSegmentLocation: {} } +} + +/** + * Build canonical Google Docs metadata from a batchUpdate response. The + * `documentId` is taken from the response body when present, otherwise parsed + * from the request URL (`.../documents/{id}:batchUpdate`). + */ +export function buildBatchUpdateMetadata( + data: { documentId?: string }, + responseUrl: string +): { documentId: string; title: string; mimeType: string; url: string } { + let documentId = data.documentId ?? '' + if (!documentId) { + const urlParts = responseUrl.split('/') + for (let i = 0; i < urlParts.length; i++) { + if (urlParts[i] === 'documents' && i + 1 < urlParts.length) { + documentId = urlParts[i + 1].split(':')[0] + break + } + } + } + + return { + documentId, + title: 'Updated Document', + mimeType: 'application/vnd.google-apps.document', + url: `https://docs.google.com/document/d/${documentId}/edit`, + } +} + // Helper function to extract text content from Google Docs document structure export function extractTextFromDocument(document: any): string { let text = '' diff --git a/apps/sim/tools/microsoft_excel/clear_range.ts b/apps/sim/tools/microsoft_excel/clear_range.ts new file mode 100644 index 00000000000..37050ed34ed --- /dev/null +++ b/apps/sim/tools/microsoft_excel/clear_range.ts @@ -0,0 +1,134 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftExcelClearRangeParams, + MicrosoftExcelClearRangeResponse, +} from '@/tools/microsoft_excel/types' +import { + buildWorksheetRangeUrl, + getItemBasePath, + getSpreadsheetWebUrl, +} from '@/tools/microsoft_excel/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Clears the contents and/or formatting of a worksheet range. + * Uses Microsoft Graph: POST /workbook/worksheets/{name}/range(address='...')/clear + */ +export const clearRangeTool: ToolConfig< + MicrosoftExcelClearRangeParams, + MicrosoftExcelClearRangeResponse +> = { + id: 'microsoft_excel_clear_range', + name: 'Clear Microsoft Excel Range', + description: 'Clear the values and/or formatting of a range in a Microsoft Excel worksheet', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, + sheetName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the worksheet (e.g., "Sheet1"). If omitted, the range must use the combined "Sheet1!A1:B2" format.', + }, + range: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The cell range to clear (e.g., "A1:D10" or "Sheet1!A1:D10")', + }, + applyTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'What to clear: "All", "Formats", or "Contents". Defaults to "All".', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + const basePath = getItemBasePath(spreadsheetId, params.driveId) + return `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/clear` + }, + 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) => ({ + applyTo: params.applyTo || 'All', + }), + }, + + transformResponse: async (_response: Response, params?: MicrosoftExcelClearRangeParams) => { + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) + + return { + success: true, + output: { + cleared: true, + range: params?.range ?? '', + applyTo: params?.applyTo || 'All', + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + cleared: { type: 'boolean', description: 'Whether the range was cleared' }, + range: { type: 'string', description: 'The range that was cleared' }, + applyTo: { type: 'string', description: 'What was cleared (All, Formats, or Contents)' }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/create_table.ts b/apps/sim/tools/microsoft_excel/create_table.ts new file mode 100644 index 00000000000..ebc7b161919 --- /dev/null +++ b/apps/sim/tools/microsoft_excel/create_table.ts @@ -0,0 +1,145 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftExcelCreateTableParams, + MicrosoftExcelCreateTableResponse, +} from '@/tools/microsoft_excel/types' +import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Creates a new table over a range of cells. + * Uses Microsoft Graph: POST /workbook/tables/add with { address, hasHeaders }. + */ +export const createTableTool: ToolConfig< + MicrosoftExcelCreateTableParams, + MicrosoftExcelCreateTableResponse +> = { + id: 'microsoft_excel_create_table', + name: 'Create Microsoft Excel Table', + description: 'Create a new table over a range of cells in a Microsoft Excel workbook', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, + address: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The range address for the table data source (e.g., "Sheet1!A1:D5"). If no sheet name is included, the active sheet is used.', + }, + hasHeaders: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the first row of the range contains column headers. Defaults to true.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + const basePath = getItemBasePath(spreadsheetId, params.driveId) + return `${basePath}/workbook/tables/add` + }, + 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 address = params.address?.trim() + if (!address) { + throw new Error('A range address is required (e.g., "Sheet1!A1:D5")') + } + return { + address, + hasHeaders: params.hasHeaders ?? true, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftExcelCreateTableParams) => { + const data = await response.json() + + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) + + return { + success: true, + output: { + table: { + id: data.id ?? '', + name: data.name ?? '', + showHeaders: data.showHeaders ?? true, + showTotals: data.showTotals ?? false, + style: data.style ?? null, + }, + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + table: { + type: 'object', + description: 'Details of the newly created table', + properties: { + id: { type: 'string', description: 'The unique ID of the table' }, + name: { type: 'string', description: 'The name of the table' }, + showHeaders: { type: 'boolean', description: 'Whether the header row is shown' }, + showTotals: { type: 'boolean', description: 'Whether the totals row is shown' }, + style: { type: 'string', description: 'The table style name' }, + }, + }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/delete_worksheet.ts b/apps/sim/tools/microsoft_excel/delete_worksheet.ts new file mode 100644 index 00000000000..5626aafce23 --- /dev/null +++ b/apps/sim/tools/microsoft_excel/delete_worksheet.ts @@ -0,0 +1,119 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftExcelDeleteWorksheetParams, + MicrosoftExcelDeleteWorksheetResponse, +} from '@/tools/microsoft_excel/types' +import { + escapeODataString, + getItemBasePath, + getSpreadsheetWebUrl, +} from '@/tools/microsoft_excel/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Deletes a worksheet from a workbook. + * Uses Microsoft Graph: DELETE /workbook/worksheets/{name} + */ +export const deleteWorksheetTool: ToolConfig< + MicrosoftExcelDeleteWorksheetParams, + MicrosoftExcelDeleteWorksheetResponse +> = { + id: 'microsoft_excel_delete_worksheet', + name: 'Delete Microsoft Excel Worksheet', + description: 'Delete a worksheet (sheet) from a Microsoft Excel workbook', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, + worksheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the worksheet to delete (e.g., "Sheet1", "Old Data")', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + const worksheetName = params.worksheetName?.trim() + if (!worksheetName) { + throw new Error('Worksheet name is required') + } + const basePath = getItemBasePath(spreadsheetId, params.driveId) + return `${basePath}/workbook/worksheets('${encodeURIComponent(escapeODataString(worksheetName))}')` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (_response: Response, params?: MicrosoftExcelDeleteWorksheetParams) => { + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) + + return { + success: true, + output: { + deleted: true, + worksheetName: params?.worksheetName?.trim() ?? '', + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the worksheet was deleted' }, + worksheetName: { type: 'string', description: 'The name of the deleted worksheet' }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/format_range.ts b/apps/sim/tools/microsoft_excel/format_range.ts new file mode 100644 index 00000000000..9932a4cdec7 --- /dev/null +++ b/apps/sim/tools/microsoft_excel/format_range.ts @@ -0,0 +1,254 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftExcelFormatRangeParams, + MicrosoftExcelFormatRangeResponse, +} from '@/tools/microsoft_excel/types' +import { + buildWorksheetRangeUrl, + getItemBasePath, + getSpreadsheetWebUrl, + parseGraphErrorMessage, +} from '@/tools/microsoft_excel/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Builds the font PATCH body from the provided font params, omitting any unset fields. + * Returns null when no font property was supplied. + */ +function buildFontBody(params: MicrosoftExcelFormatRangeParams): Record | null { + const body: Record = {} + if (params.fontBold !== undefined) body.bold = params.fontBold + if (params.fontItalic !== undefined) body.italic = params.fontItalic + if (params.fontColor) body.color = params.fontColor + if (params.fontSize !== undefined) body.size = params.fontSize + if (params.fontName) body.name = params.fontName + return Object.keys(body).length > 0 ? body : null +} + +/** + * Formats a worksheet range by applying fill color and/or font properties. + * Uses Microsoft Graph PATCH on range(...)/format/fill and range(...)/format/font. + * The font update is the primary request; a fill update (when also requested) runs + * as a follow-up call so a single tool invocation can set both. + */ +export const formatRangeTool: ToolConfig< + MicrosoftExcelFormatRangeParams, + MicrosoftExcelFormatRangeResponse +> = { + id: 'microsoft_excel_format_range', + name: 'Format Microsoft Excel Range', + description: 'Apply fill color and/or font formatting to a range in a Microsoft Excel worksheet', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, + sheetName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the worksheet (e.g., "Sheet1"). If omitted, the range must use the combined "Sheet1!A1:B2" format.', + }, + range: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The cell range to format (e.g., "A1:D10" or "Sheet1!A1:D10")', + }, + fillColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Background fill color as an HTML hex code (e.g., "#FFFF00").', + }, + fontBold: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the font is bold.', + }, + fontItalic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the font is italic.', + }, + fontColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Font color as an HTML hex code (e.g., "#FF0000").', + }, + fontSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Font size in points (e.g., 12).', + }, + fontName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Font name (e.g., "Calibri").', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const fontBody = buildFontBody(params) + const hasFill = Boolean(params.fillColor) + if (!fontBody && !hasFill) { + throw new Error('Provide at least a fill color or a font property to format the range') + } + + const basePath = getItemBasePath(spreadsheetId, params.driveId) + const rangeUrl = buildWorksheetRangeUrl(basePath, params.range, params.sheetName) + return fontBody ? `${rangeUrl}/format/font` : `${rangeUrl}/format/fill` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const fontBody = buildFontBody(params) + if (fontBody) return fontBody + return { color: params.fillColor } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftExcelFormatRangeParams) => { + if (!params) { + throw new Error('Format parameters are required') + } + const accessToken = params.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + + const spreadsheetId = params.spreadsheetId?.trim() || '' + const driveId = params.driveId + + const fontBody = buildFontBody(params) + const hasFill = Boolean(params.fillColor) + + let fontResult: Record | null = null + let fillApplied = false + + if (fontBody) { + fontResult = await response.json().catch(() => null) + + if (hasFill) { + const basePath = getItemBasePath(spreadsheetId, driveId) + const fillUrl = `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/format/fill` + const fillResp = await fetch(fillUrl, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ color: params.fillColor }), + }) + if (!fillResp.ok) { + const errorText = await fillResp.text().catch(() => '') + const detail = parseGraphErrorMessage(fillResp.status, fillResp.statusText, errorText) + throw new Error( + `Font formatting was applied, but the fill color update failed: ${detail}. Re-run with only the fill color to finish.` + ) + } + fillApplied = true + } + } else if (hasFill) { + fillApplied = true + } + + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) + + return { + success: true, + output: { + formatted: true, + range: params.range ?? '', + fill: fillApplied ? { color: params.fillColor ?? null } : null, + font: fontBody + ? { + bold: (fontResult?.bold as boolean | undefined) ?? params.fontBold ?? null, + italic: (fontResult?.italic as boolean | undefined) ?? params.fontItalic ?? null, + color: (fontResult?.color as string | undefined) ?? params.fontColor ?? null, + name: (fontResult?.name as string | undefined) ?? params.fontName ?? null, + size: (fontResult?.size as number | undefined) ?? params.fontSize ?? null, + } + : null, + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + formatted: { type: 'boolean', description: 'Whether the formatting was applied' }, + range: { type: 'string', description: 'The range that was formatted' }, + fill: { + type: 'object', + description: 'The applied fill, or null if no fill was set', + properties: { + color: { type: 'string', description: 'The applied fill color' }, + }, + }, + font: { + type: 'object', + description: 'The applied font properties, or null if no font was set', + properties: { + bold: { type: 'boolean', description: 'Whether the font is bold' }, + italic: { type: 'boolean', description: 'Whether the font is italic' }, + color: { type: 'string', description: 'The font color' }, + name: { type: 'string', description: 'The font name' }, + size: { type: 'number', description: 'The font size in points' }, + }, + }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/index.ts b/apps/sim/tools/microsoft_excel/index.ts index bf1e8b1aad5..bbaac00ac1c 100644 --- a/apps/sim/tools/microsoft_excel/index.ts +++ b/apps/sim/tools/microsoft_excel/index.ts @@ -1,4 +1,9 @@ +import { clearRangeTool } from '@/tools/microsoft_excel/clear_range' +import { createTableTool } from '@/tools/microsoft_excel/create_table' +import { deleteWorksheetTool } from '@/tools/microsoft_excel/delete_worksheet' +import { formatRangeTool } from '@/tools/microsoft_excel/format_range' import { readTool, readV2Tool } from '@/tools/microsoft_excel/read' +import { sortRangeTool } from '@/tools/microsoft_excel/sort_range' import { tableAddTool } from '@/tools/microsoft_excel/table_add' import { worksheetAddTool } from '@/tools/microsoft_excel/worksheet_add' import { writeTool, writeV2Tool } from '@/tools/microsoft_excel/write' @@ -12,3 +17,12 @@ export const microsoftExcelWriteTool = writeTool // V2 exports export const microsoftExcelReadV2Tool = readV2Tool export const microsoftExcelWriteV2Tool = writeV2Tool + +// Workbook operations +export const microsoftExcelClearRangeTool = clearRangeTool +export const microsoftExcelCreateTableTool = createTableTool +export const microsoftExcelDeleteWorksheetTool = deleteWorksheetTool +export const microsoftExcelFormatRangeTool = formatRangeTool +export const microsoftExcelSortRangeTool = sortRangeTool + +export * from './types' diff --git a/apps/sim/tools/microsoft_excel/sort_range.ts b/apps/sim/tools/microsoft_excel/sort_range.ts new file mode 100644 index 00000000000..162e9492674 --- /dev/null +++ b/apps/sim/tools/microsoft_excel/sort_range.ts @@ -0,0 +1,197 @@ +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftExcelSortRangeParams, + MicrosoftExcelSortRangeResponse, +} from '@/tools/microsoft_excel/types' +import { + buildWorksheetRangeUrl, + escapeODataString, + getItemBasePath, + getSpreadsheetWebUrl, +} from '@/tools/microsoft_excel/utils' +import type { ToolConfig } from '@/tools/types' + +/** + * Sorts a worksheet range or a table by a single column. + * Uses Microsoft Graph: + * - Range: POST /workbook/worksheets/{name}/range(address='...')/sort/apply + * - Table: POST /workbook/tables('{name}')/sort/apply + */ +export const sortRangeTool: ToolConfig< + MicrosoftExcelSortRangeParams, + MicrosoftExcelSortRangeResponse +> = { + id: 'microsoft_excel_sort_range', + name: 'Sort Microsoft Excel Range', + description: 'Sort a range or table by a column in a Microsoft Excel worksheet', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, + tableName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the table to sort. When provided, the table is sorted and range/sheetName are ignored.', + }, + sheetName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the worksheet (e.g., "Sheet1"). Used for range sorts when the range does not include a sheet name.', + }, + range: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to sort (e.g., "A1:D10" or "Sheet1!A1:D10"). Required when no table name is provided.', + }, + sortColumn: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The zero-based column index within the range or table to sort on (0 = first column).', + }, + sortAscending: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to sort in ascending order. Defaults to true.', + }, + hasHeaders: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether the range has a header row that should be excluded from sorting. Only applies to range sorts. Defaults to false.', + }, + matchCase: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether casing affects string ordering. Defaults to false.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + const basePath = getItemBasePath(spreadsheetId, params.driveId) + + const tableName = params.tableName?.trim() + if (tableName) { + return `${basePath}/workbook/tables('${encodeURIComponent(escapeODataString(tableName))}')/sort/apply` + } + + return `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/sort/apply` + }, + 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 key = + typeof params.sortColumn === 'number' ? params.sortColumn : Number(params.sortColumn) + if (!Number.isInteger(key) || key < 0) { + throw new Error('sortColumn must be a non-negative integer column index') + } + + const body: Record = { + fields: [ + { + key, + ascending: params.sortAscending ?? true, + sortOn: 'Value', + }, + ], + matchCase: params.matchCase ?? false, + } + + if (!params.tableName?.trim()) { + body.hasHeaders = params.hasHeaders ?? false + body.orientation = 'Rows' + } + + return body + }, + }, + + transformResponse: async (_response: Response, params?: MicrosoftExcelSortRangeParams) => { + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) + + const target = params?.tableName?.trim() || params?.range || '' + + return { + success: true, + output: { + sorted: true, + target, + sortColumn: params?.sortColumn ?? 0, + ascending: params?.sortAscending ?? true, + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + sorted: { type: 'boolean', description: 'Whether the sort was applied' }, + target: { type: 'string', description: 'The range or table name that was sorted' }, + sortColumn: { type: 'number', description: 'The zero-based column index that was sorted on' }, + ascending: { type: 'boolean', description: 'Whether the sort was ascending' }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/types.ts b/apps/sim/tools/microsoft_excel/types.ts index 7e561e46df6..a3d3bc42d34 100644 --- a/apps/sim/tools/microsoft_excel/types.ts +++ b/apps/sim/tools/microsoft_excel/types.ts @@ -89,11 +89,123 @@ export interface MicrosoftExcelWorksheetToolParams { worksheetName: string } +export interface MicrosoftExcelClearRangeParams { + accessToken: string + spreadsheetId: string + driveId?: string + sheetName?: string + range: string + applyTo?: 'All' | 'Formats' | 'Contents' +} + +export interface MicrosoftExcelClearRangeResponse extends ToolResponse { + output: { + cleared: boolean + range: string + applyTo: string + metadata: MicrosoftExcelMetadata + } +} + +export interface MicrosoftExcelFormatRangeParams { + accessToken: string + spreadsheetId: string + driveId?: string + sheetName?: string + range: string + fillColor?: string + fontBold?: boolean + fontItalic?: boolean + fontColor?: string + fontSize?: number + fontName?: string +} + +export interface MicrosoftExcelFormatRangeResponse extends ToolResponse { + output: { + formatted: boolean + range: string + fill: { color: string | null } | null + font: { + bold: boolean | null + italic: boolean | null + color: string | null + name: string | null + size: number | null + } | null + metadata: MicrosoftExcelMetadata + } +} + +export interface MicrosoftExcelCreateTableParams { + accessToken: string + spreadsheetId: string + driveId?: string + address: string + hasHeaders?: boolean +} + +export interface MicrosoftExcelCreateTableResponse extends ToolResponse { + output: { + table: { + id: string + name: string + showHeaders: boolean + showTotals: boolean + style: string | null + } + metadata: MicrosoftExcelMetadata + } +} + +export interface MicrosoftExcelDeleteWorksheetParams { + accessToken: string + spreadsheetId: string + driveId?: string + worksheetName: string +} + +export interface MicrosoftExcelDeleteWorksheetResponse extends ToolResponse { + output: { + deleted: boolean + worksheetName: string + metadata: MicrosoftExcelMetadata + } +} + +export interface MicrosoftExcelSortRangeParams { + accessToken: string + spreadsheetId: string + driveId?: string + sheetName?: string + range?: string + tableName?: string + sortColumn: number + sortAscending?: boolean + hasHeaders?: boolean + matchCase?: boolean +} + +export interface MicrosoftExcelSortRangeResponse extends ToolResponse { + output: { + sorted: boolean + target: string + sortColumn: number + ascending: boolean + metadata: MicrosoftExcelMetadata + } +} + export type MicrosoftExcelResponse = | MicrosoftExcelReadResponse | MicrosoftExcelWriteResponse | MicrosoftExcelTableAddResponse | MicrosoftExcelWorksheetAddResponse + | MicrosoftExcelClearRangeResponse + | MicrosoftExcelFormatRangeResponse + | MicrosoftExcelCreateTableResponse + | MicrosoftExcelDeleteWorksheetResponse + | MicrosoftExcelSortRangeResponse // V2 Types - with separate sheetName param export interface MicrosoftExcelV2ToolParams { diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index cf3fab16cd2..27650907cf9 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -110,6 +110,17 @@ export async function extractGraphError(response: Response): Promise { return parseGraphErrorMessage(response.status, response.statusText, errorText) } +/** + * Escape a string for use inside an OData single-quoted literal (e.g. a + * `worksheets('...')` or `tables('...')` key). OData escapes an embedded single + * quote by doubling it, so a name like `O'Brien` becomes `O''Brien`; without this + * the apostrophe terminates the literal early and breaks the request URL. + * `encodeURIComponent` leaves apostrophes untouched, so the doubling must happen here. + */ +export function escapeODataString(value: string): string { + return value.replace(/'/g, "''") +} + /** Pattern for Microsoft Graph item/drive IDs: alphanumeric, hyphens, underscores, and ! (for SharePoint b! format) */ export const GRAPH_ID_PATTERN = /^[a-zA-Z0-9!_-]+$/ @@ -140,6 +151,56 @@ export function getItemBasePath(spreadsheetId: string, driveId?: string): string return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` } +/** + * Resolves a worksheet name and cell address from either an explicit sheet name + * plus address, or a combined "Sheet1!A1:B2" range string. + * + * - When `sheetName` is provided, `address` is treated as a bare cell address (e.g. "A1:B2"). + * - When `sheetName` is omitted, `address` must be a combined "Sheet1!A1:B2" range. + * + * Throws when a worksheet name cannot be determined or the combined format is invalid. + */ +export function resolveSheetAndAddress( + address: string | undefined, + sheetName?: string +): { sheetName: string; address: string } { + const trimmedAddress = address?.trim() + if (!trimmedAddress) { + throw new Error('A cell range is required (e.g., "A1:B2" or "Sheet1!A1:B2")') + } + + const trimmedSheet = sheetName?.trim() + if (trimmedSheet) { + return { sheetName: trimmedSheet, address: trimmedAddress } + } + + const match = trimmedAddress.match(/^([^!]+)!(.+)$/) + if (!match) { + throw new Error( + `Invalid range format: "${address}". Provide a sheet name, or use the combined format "Sheet1!A1:B2"` + ) + } + + return { sheetName: match[1], address: match[2] } +} + +/** + * Builds the Graph API URL for a worksheet range object, resolving the sheet name + * from either an explicit `sheetName` param or a combined "Sheet1!A1:B2" address. + * The returned URL has no trailing segment, so callers append `/clear`, `/sort/apply`, + * `/format/fill`, `/format/font`, or nothing for the range object itself. + */ +export function buildWorksheetRangeUrl( + basePath: string, + address: string | undefined, + sheetName?: string +): string { + const resolved = resolveSheetAndAddress(address, sheetName) + const encodedSheet = encodeURIComponent(escapeODataString(resolved.sheetName)) + const encodedAddress = encodeURIComponent(resolved.address) + return `${basePath}/workbook/worksheets('${encodedSheet}')/range(address='${encodedAddress}')` +} + export function trimTrailingEmptyRowsAndColumns(matrix: ExcelCellValue[][]): ExcelCellValue[][] { if (!Array.isArray(matrix) || matrix.length === 0) return [] diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index e9d2949788d..37583a9fed0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -78,6 +78,7 @@ import { } from '@/tools/ahrefs' import { airtableCreateRecordsTool, + airtableDeleteRecordsTool, airtableGetBaseSchemaTool, airtableGetRecordTool, airtableListBasesTool, @@ -85,6 +86,7 @@ import { airtableListTablesTool, airtableUpdateMultipleRecordsTool, airtableUpdateRecordTool, + airtableUpsertRecordsTool, } from '@/tools/airtable' import { airweaveSearchTool } from '@/tools/airweave' import { @@ -1245,7 +1247,17 @@ import { googleContactsSearchTool, googleContactsUpdateTool, } from '@/tools/google_contacts' -import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' +import { + googleDocsCreateTool, + googleDocsInsertImageTool, + googleDocsInsertPageBreakTool, + googleDocsInsertTableTool, + googleDocsInsertTextTool, + googleDocsReadTool, + googleDocsReplaceTextTool, + googleDocsUpdateTextStyleTool, + googleDocsWriteTool, +} from '@/tools/google_docs' import { googleDriveCopyTool, googleDriveCreateFolderTool, @@ -2157,8 +2169,13 @@ import { dataverseWhoAmITool, } from '@/tools/microsoft_dataverse' import { + microsoftExcelClearRangeTool, + microsoftExcelCreateTableTool, + microsoftExcelDeleteWorksheetTool, + microsoftExcelFormatRangeTool, microsoftExcelReadTool, microsoftExcelReadV2Tool, + microsoftExcelSortRangeTool, microsoftExcelTableAddTool, microsoftExcelWorksheetAddTool, microsoftExcelWriteTool, @@ -3949,7 +3966,14 @@ import { webflowListItemsTool, webflowUpdateItemTool, } from '@/tools/webflow' -import { whatsappSendMessageTool } from '@/tools/whatsapp' +import { + whatsappMarkReadTool, + whatsappSendInteractiveTool, + whatsappSendMediaTool, + whatsappSendMessageTool, + whatsappSendReactionTool, + whatsappSendTemplateTool, +} from '@/tools/whatsapp' import { wikipediaPageContentTool, wikipediaPageSummaryTool, @@ -5184,6 +5208,11 @@ export const tools: Record = { gmail_untrash_thread_v2: gmailUntrashThreadV2Tool, gmail_update_label_v2: gmailUpdateLabelV2Tool, whatsapp_send_message: whatsappSendMessageTool, + whatsapp_send_template: whatsappSendTemplateTool, + whatsapp_send_media: whatsappSendMediaTool, + whatsapp_send_interactive: whatsappSendInteractiveTool, + whatsapp_send_reaction: whatsappSendReactionTool, + whatsapp_mark_read: whatsappMarkReadTool, x_write: xWriteTool, x_read: xReadTool, x_search: xSearchTool, @@ -5934,6 +5963,12 @@ export const tools: Record = { google_docs_read: googleDocsReadTool, google_docs_write: googleDocsWriteTool, google_docs_create: googleDocsCreateTool, + google_docs_insert_text: googleDocsInsertTextTool, + google_docs_replace_text: googleDocsReplaceTextTool, + google_docs_insert_table: googleDocsInsertTableTool, + google_docs_insert_image: googleDocsInsertImageTool, + google_docs_insert_page_break: googleDocsInsertPageBreakTool, + google_docs_update_text_style: googleDocsUpdateTextStyleTool, google_books_volume_search: googleBooksVolumeSearchTool, google_books_volume_details: googleBooksVolumeDetailsTool, google_maps_air_quality: googleMapsAirQualityTool, @@ -6412,6 +6447,7 @@ export const tools: Record = { algolia_clear_records: algoliaClearRecordsTool, algolia_delete_by_filter: algoliaDeleteByFilterTool, airtable_create_records: airtableCreateRecordsTool, + airtable_delete_records: airtableDeleteRecordsTool, airtable_get_base_schema: airtableGetBaseSchemaTool, airtable_get_record: airtableGetRecordTool, airtable_list_bases: airtableListBasesTool, @@ -6419,6 +6455,7 @@ export const tools: Record = { airtable_list_tables: airtableListTablesTool, airtable_update_multiple_records: airtableUpdateMultipleRecordsTool, airtable_update_record: airtableUpdateRecordTool, + airtable_upsert_records: airtableUpsertRecordsTool, attio_assert_record: attioAssertRecordTool, attio_create_comment: attioCreateCommentTool, attio_create_list: attioCreateListTool, @@ -7021,6 +7058,11 @@ export const tools: Record = { microsoft_excel_worksheet_add: microsoftExcelWorksheetAddTool, microsoft_excel_read_v2: microsoftExcelReadV2Tool, microsoft_excel_write_v2: microsoftExcelWriteV2Tool, + microsoft_excel_clear_range: microsoftExcelClearRangeTool, + microsoft_excel_format_range: microsoftExcelFormatRangeTool, + microsoft_excel_create_table: microsoftExcelCreateTableTool, + microsoft_excel_delete_worksheet: microsoftExcelDeleteWorksheetTool, + microsoft_excel_sort_range: microsoftExcelSortRangeTool, microsoft_planner_create_task: microsoftPlannerCreateTaskTool, microsoft_planner_read_task: microsoftPlannerReadTaskTool, microsoft_planner_update_task: microsoftPlannerUpdateTaskTool, diff --git a/apps/sim/tools/whatsapp/index.ts b/apps/sim/tools/whatsapp/index.ts index c596cad77bf..50316221728 100644 --- a/apps/sim/tools/whatsapp/index.ts +++ b/apps/sim/tools/whatsapp/index.ts @@ -1,2 +1,7 @@ +export { markReadTool as whatsappMarkReadTool } from '@/tools/whatsapp/mark_read' +export { sendInteractiveTool as whatsappSendInteractiveTool } from '@/tools/whatsapp/send_interactive' +export { sendMediaTool as whatsappSendMediaTool } from '@/tools/whatsapp/send_media' export { sendMessageTool as whatsappSendMessageTool } from '@/tools/whatsapp/send_message' +export { sendReactionTool as whatsappSendReactionTool } from '@/tools/whatsapp/send_reaction' +export { sendTemplateTool as whatsappSendTemplateTool } from '@/tools/whatsapp/send_template' export * from '@/tools/whatsapp/types' diff --git a/apps/sim/tools/whatsapp/mark_read.ts b/apps/sim/tools/whatsapp/mark_read.ts new file mode 100644 index 00000000000..585580ec47f --- /dev/null +++ b/apps/sim/tools/whatsapp/mark_read.ts @@ -0,0 +1,76 @@ +import type { ToolConfig } from '@/tools/types' +import type { WhatsAppMarkReadParams, WhatsAppMarkReadResponse } from '@/tools/whatsapp/types' +import { buildAuthHeaders, buildMessagesUrl, isRecord } from '@/tools/whatsapp/utils' + +export const markReadTool: ToolConfig = { + id: 'whatsapp_mark_read', + name: 'WhatsApp Mark As Read', + description: 'Mark a received WhatsApp message as read so the sender sees blue checkmarks.', + version: '1.0.0', + + params: { + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID (wamid) of the incoming message to mark as read', + }, + phoneNumberId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business Phone Number ID (from Meta Business Suite)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business API Access Token (from Meta Developer Portal)', + }, + }, + + request: { + url: (params) => buildMessagesUrl(params.phoneNumberId), + method: 'POST', + headers: (params) => buildAuthHeaders(params.accessToken), + body: (params) => { + if (!params.messageId) { + throw new Error('Message ID is required but was not provided') + } + return { + messaging_product: 'whatsapp', + status: 'read', + message_id: params.messageId.trim(), + } + }, + }, + + transformResponse: async (response: Response) => { + const responseText = await response.text() + const parsed = responseText ? (JSON.parse(responseText) as unknown) : {} + const data = isRecord(parsed) ? parsed : {} + const error = isRecord(data.error) ? data.error : undefined + + if (!response.ok) { + const errorMessage = + (typeof error?.message === 'string' ? error.message : undefined) || + (typeof error?.error_user_msg === 'string' ? error.error_user_msg : undefined) || + `WhatsApp API error (${response.status})` + throw new Error(errorMessage) + } + + return { + success: true, + output: { + success: data.success !== false, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the message was successfully marked as read', + }, + }, +} diff --git a/apps/sim/tools/whatsapp/send_interactive.ts b/apps/sim/tools/whatsapp/send_interactive.ts new file mode 100644 index 00000000000..16d2e76aa8b --- /dev/null +++ b/apps/sim/tools/whatsapp/send_interactive.ts @@ -0,0 +1,139 @@ +import type { ToolConfig } from '@/tools/types' +import type { WhatsAppSendInteractiveParams, WhatsAppSendResponse } from '@/tools/whatsapp/types' +import { + buildAuthHeaders, + buildMessagesUrl, + transformWhatsAppSendResponse, + whatsappSendOutputs, +} from '@/tools/whatsapp/utils' + +function coerceArray(value: unknown): unknown[] | undefined { + if (value == null || value === '') return undefined + const parsed = typeof value === 'string' ? (JSON.parse(value) as unknown) : value + if (!Array.isArray(parsed)) { + throw new Error('Interactive buttons and sections must be JSON arrays') + } + return parsed.length > 0 ? parsed : undefined +} + +export const sendInteractiveTool: ToolConfig = + { + id: 'whatsapp_send_interactive', + name: 'WhatsApp Send Interactive', + description: 'Send an interactive WhatsApp message with reply buttons or a selectable list.', + version: '1.0.0', + + params: { + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient phone number with country code (e.g., +14155552671)', + }, + bodyText: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Main body text of the interactive message', + }, + headerText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional plain-text header shown above the body', + }, + footerText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional footer text shown below the body', + }, + buttons: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Reply buttons array (max 3), each item: { "type": "reply", "reply": { "id": "...", "title": "..." } }. Provide buttons or sections.', + }, + listButtonText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Label for the menu button that opens the list (required when sending a list)', + }, + sections: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'List sections array, each item: { "title": "...", "rows": [{ "id": "...", "title": "...", "description": "..." }] }. Provide sections or buttons.', + }, + phoneNumberId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business Phone Number ID (from Meta Business Suite)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business API Access Token (from Meta Developer Portal)', + }, + }, + + request: { + url: (params) => buildMessagesUrl(params.phoneNumberId), + method: 'POST', + headers: (params) => buildAuthHeaders(params.accessToken), + body: (params) => { + if (!params.phoneNumber) { + throw new Error('Phone number is required but was not provided') + } + if (!params.bodyText) { + throw new Error('Body text is required but was not provided') + } + + const buttons = coerceArray(params.buttons) + const sections = coerceArray(params.sections) + if (!buttons && !sections) { + throw new Error('Provide either buttons (reply buttons) or sections (list)') + } + if (buttons && sections) { + throw new Error('Provide either buttons or sections, not both') + } + + const interactive: Record = { + type: buttons ? 'button' : 'list', + body: { text: params.bodyText }, + } + if (params.headerText) { + interactive.header = { type: 'text', text: params.headerText } + } + if (params.footerText) { + interactive.footer = { text: params.footerText } + } + if (buttons) { + interactive.action = { buttons } + } else { + const listButton = params.listButtonText?.trim() + if (!listButton) { + throw new Error('listButtonText is required when sending a list') + } + interactive.action = { button: listButton, sections } + } + + return { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: params.phoneNumber.trim(), + type: 'interactive', + interactive, + } + }, + }, + + transformResponse: transformWhatsAppSendResponse, + + outputs: whatsappSendOutputs, + } diff --git a/apps/sim/tools/whatsapp/send_media.ts b/apps/sim/tools/whatsapp/send_media.ts new file mode 100644 index 00000000000..f6a19fab027 --- /dev/null +++ b/apps/sim/tools/whatsapp/send_media.ts @@ -0,0 +1,117 @@ +import type { ToolConfig } from '@/tools/types' +import type { + WhatsAppMediaType, + WhatsAppSendMediaParams, + WhatsAppSendResponse, +} from '@/tools/whatsapp/types' +import { + buildAuthHeaders, + buildMessagesUrl, + transformWhatsAppSendResponse, + whatsappSendOutputs, +} from '@/tools/whatsapp/utils' + +const MEDIA_TYPES: readonly WhatsAppMediaType[] = ['image', 'document', 'video', 'audio'] + +const CAPTION_TYPES: ReadonlySet = new Set(['image', 'video', 'document']) + +export const sendMediaTool: ToolConfig = { + id: 'whatsapp_send_media', + name: 'WhatsApp Send Media', + description: + 'Send an image, document, video, or audio message via a public link or an uploaded media ID.', + version: '1.0.0', + + params: { + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient phone number with country code (e.g., +14155552671)', + }, + mediaType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Type of media to send: image, document, video, or audio', + }, + mediaLink: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Public HTTPS URL of the media (provide this or mediaId)', + }, + mediaId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of media previously uploaded to WhatsApp (provide this or mediaLink)', + }, + caption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional caption for image, video, or document media', + }, + filename: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional file name shown to the recipient for document media', + }, + phoneNumberId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business Phone Number ID (from Meta Business Suite)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business API Access Token (from Meta Developer Portal)', + }, + }, + + request: { + url: (params) => buildMessagesUrl(params.phoneNumberId), + method: 'POST', + headers: (params) => buildAuthHeaders(params.accessToken), + body: (params) => { + if (!params.phoneNumber) { + throw new Error('Phone number is required but was not provided') + } + + const mediaType = params.mediaType?.trim() as WhatsAppMediaType + if (!MEDIA_TYPES.includes(mediaType)) { + throw new Error(`Media type must be one of: ${MEDIA_TYPES.join(', ')}`) + } + + const link = params.mediaLink?.trim() + const id = params.mediaId?.trim() + if (!link && !id) { + throw new Error('Either mediaLink or mediaId is required') + } + + const media: Record = id ? { id } : { link: link as string } + if (params.caption && CAPTION_TYPES.has(mediaType)) { + media.caption = params.caption + } + if (params.filename && mediaType === 'document') { + media.filename = params.filename + } + + return { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: params.phoneNumber.trim(), + type: mediaType, + [mediaType]: media, + } + }, + }, + + transformResponse: transformWhatsAppSendResponse, + + outputs: whatsappSendOutputs, +} diff --git a/apps/sim/tools/whatsapp/send_reaction.ts b/apps/sim/tools/whatsapp/send_reaction.ts new file mode 100644 index 00000000000..4664ab69d3c --- /dev/null +++ b/apps/sim/tools/whatsapp/send_reaction.ts @@ -0,0 +1,78 @@ +import type { ToolConfig } from '@/tools/types' +import type { WhatsAppSendReactionParams, WhatsAppSendResponse } from '@/tools/whatsapp/types' +import { + buildAuthHeaders, + buildMessagesUrl, + transformWhatsAppSendResponse, + whatsappSendOutputs, +} from '@/tools/whatsapp/utils' + +export const sendReactionTool: ToolConfig = { + id: 'whatsapp_send_reaction', + name: 'WhatsApp Send Reaction', + description: + 'React to a WhatsApp message with an emoji. Send an empty emoji to remove an existing reaction.', + version: '1.0.0', + + params: { + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient phone number with country code (e.g., +14155552671)', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID (wamid) of the message to react to', + }, + emoji: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Emoji to react with. Leave empty to remove an existing reaction.', + }, + phoneNumberId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business Phone Number ID (from Meta Business Suite)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business API Access Token (from Meta Developer Portal)', + }, + }, + + request: { + url: (params) => buildMessagesUrl(params.phoneNumberId), + method: 'POST', + headers: (params) => buildAuthHeaders(params.accessToken), + body: (params) => { + if (!params.phoneNumber) { + throw new Error('Phone number is required but was not provided') + } + if (!params.messageId) { + throw new Error('Message ID is required but was not provided') + } + + return { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: params.phoneNumber.trim(), + type: 'reaction', + reaction: { + message_id: params.messageId.trim(), + emoji: params.emoji ?? '', + }, + } + }, + }, + + transformResponse: transformWhatsAppSendResponse, + + outputs: whatsappSendOutputs, +} diff --git a/apps/sim/tools/whatsapp/send_template.ts b/apps/sim/tools/whatsapp/send_template.ts new file mode 100644 index 00000000000..7e2c5c28c91 --- /dev/null +++ b/apps/sim/tools/whatsapp/send_template.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' +import type { WhatsAppSendResponse, WhatsAppSendTemplateParams } from '@/tools/whatsapp/types' +import { + buildAuthHeaders, + buildMessagesUrl, + transformWhatsAppSendResponse, + whatsappSendOutputs, +} from '@/tools/whatsapp/utils' + +function coerceComponents(value: unknown): unknown[] | undefined { + if (value == null || value === '') return undefined + const parsed = typeof value === 'string' ? (JSON.parse(value) as unknown) : value + if (!Array.isArray(parsed)) { + throw new Error('Template components must be a JSON array') + } + return parsed +} + +export const sendTemplateTool: ToolConfig = { + id: 'whatsapp_send_template', + name: 'WhatsApp Send Template', + description: + 'Send a pre-approved WhatsApp template message with a language and optional variable components.', + version: '1.0.0', + + params: { + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient phone number with country code (e.g., +14155552671)', + }, + templateName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the approved message template', + }, + languageCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Template language/locale code (e.g., en_US)', + }, + components: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Template components array with parameters for header/body/button variables, per the WhatsApp template message schema', + }, + phoneNumberId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business Phone Number ID (from Meta Business Suite)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WhatsApp Business API Access Token (from Meta Developer Portal)', + }, + }, + + request: { + url: (params) => buildMessagesUrl(params.phoneNumberId), + method: 'POST', + headers: (params) => buildAuthHeaders(params.accessToken), + body: (params) => { + if (!params.phoneNumber) { + throw new Error('Phone number is required but was not provided') + } + if (!params.templateName) { + throw new Error('Template name is required but was not provided') + } + if (!params.languageCode) { + throw new Error('Template language code is required but was not provided') + } + + const components = coerceComponents(params.components) + const template: Record = { + name: params.templateName.trim(), + language: { code: params.languageCode.trim() }, + } + if (components && components.length > 0) { + template.components = components + } + + return { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: params.phoneNumber.trim(), + type: 'template', + template, + } + }, + }, + + transformResponse: transformWhatsAppSendResponse, + + outputs: whatsappSendOutputs, +} diff --git a/apps/sim/tools/whatsapp/types.ts b/apps/sim/tools/whatsapp/types.ts index 96dfc3f504f..36e34fe9dc5 100644 --- a/apps/sim/tools/whatsapp/types.ts +++ b/apps/sim/tools/whatsapp/types.ts @@ -1,5 +1,31 @@ import type { ToolResponse } from '@/tools/types' +interface WhatsAppMessageContact { + input: string + wa_id?: string | null +} + +interface WhatsAppSendOutput { + success: boolean + messageId?: string + messageStatus?: string + messagingProduct?: string + inputPhoneNumber?: string | null + whatsappUserId?: string | null + contacts?: WhatsAppMessageContact[] + error?: string +} + +/** Shared response for every outbound `/messages` send operation. */ +export interface WhatsAppSendResponse extends ToolResponse { + output: WhatsAppSendOutput +} + +/** Legacy alias kept for the text send_message tool and the block output type. */ +export interface WhatsAppResponse extends ToolResponse { + output: WhatsAppSendOutput +} + export interface WhatsAppSendMessageParams { phoneNumber: string message: string @@ -8,20 +34,57 @@ export interface WhatsAppSendMessageParams { previewUrl?: boolean } -interface WhatsAppMessageContact { - input: string - wa_id?: string | null +export interface WhatsAppSendTemplateParams { + phoneNumber: string + templateName: string + languageCode: string + components?: unknown + phoneNumberId: string + accessToken: string } -export interface WhatsAppResponse extends ToolResponse { +export type WhatsAppMediaType = 'image' | 'document' | 'video' | 'audio' + +export interface WhatsAppSendMediaParams { + phoneNumber: string + mediaType: WhatsAppMediaType + mediaLink?: string + mediaId?: string + caption?: string + filename?: string + phoneNumberId: string + accessToken: string +} + +export interface WhatsAppSendInteractiveParams { + phoneNumber: string + bodyText: string + headerText?: string + footerText?: string + buttons?: unknown + listButtonText?: string + sections?: unknown + phoneNumberId: string + accessToken: string +} + +export interface WhatsAppSendReactionParams { + phoneNumber: string + messageId: string + emoji?: string + phoneNumberId: string + accessToken: string +} + +export interface WhatsAppMarkReadParams { + messageId: string + phoneNumberId: string + accessToken: string +} + +export interface WhatsAppMarkReadResponse extends ToolResponse { output: { success: boolean - messageId?: string - messageStatus?: string - messagingProduct?: string - inputPhoneNumber?: string | null - whatsappUserId?: string | null - contacts?: WhatsAppMessageContact[] error?: string } } diff --git a/apps/sim/tools/whatsapp/utils.ts b/apps/sim/tools/whatsapp/utils.ts new file mode 100644 index 00000000000..1a5b6c509f0 --- /dev/null +++ b/apps/sim/tools/whatsapp/utils.ts @@ -0,0 +1,135 @@ +import type { WhatsAppSendResponse } from '@/tools/whatsapp/types' + +/** WhatsApp Cloud API Graph version used by every outbound tool. */ +export const WHATSAPP_GRAPH_VERSION = 'v25.0' + +/** Build the messages endpoint for a given business phone number ID. */ +export function buildMessagesUrl(phoneNumberId: string | undefined): string { + if (!phoneNumberId) { + throw new Error('WhatsApp Phone Number ID is required') + } + return `https://graph.facebook.com/${WHATSAPP_GRAPH_VERSION}/${phoneNumberId.trim()}/messages` +} + +/** Build the shared Bearer auth headers for the WhatsApp Cloud API. */ +export function buildAuthHeaders(accessToken: string | undefined): Record { + if (!accessToken) { + throw new Error('WhatsApp Access Token is required') + } + return { + Authorization: `Bearer ${accessToken.trim()}`, + 'Content-Type': 'application/json', + } +} + +export function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +async function parseWhatsAppResponse(response: Response): Promise> { + const responseText = await response.text() + const parsed = responseText ? (JSON.parse(responseText) as unknown) : {} + return isRecord(parsed) ? parsed : {} +} + +/** Extract a human-readable error message from a WhatsApp API error payload. */ +function extractErrorMessage(data: Record, status: number): string { + const error = isRecord(data.error) ? data.error : undefined + return ( + (typeof error?.message === 'string' ? error.message : undefined) || + (typeof error?.error_user_msg === 'string' ? error.error_user_msg : undefined) || + (isRecord(error?.error_data) && typeof error.error_data.details === 'string' + ? error.error_data.details + : undefined) || + `WhatsApp API error (${status})` + ) +} + +/** + * Transform the shared send response shape returned by every outbound message + * operation (template, media, interactive, reaction) on `/messages`. + */ +export async function transformWhatsAppSendResponse( + response: Response +): Promise { + const data = await parseWhatsAppResponse(response) + + if (!response.ok) { + throw new Error(extractErrorMessage(data, response.status)) + } + + const contacts = Array.isArray(data.contacts) + ? data.contacts.filter(isRecord).map((contact) => ({ + input: typeof contact.input === 'string' ? contact.input : '', + wa_id: typeof contact.wa_id === 'string' ? contact.wa_id : null, + })) + : [] + const firstMessage = + Array.isArray(data.messages) && isRecord(data.messages[0]) ? data.messages[0] : undefined + const messageId = typeof firstMessage?.id === 'string' ? firstMessage.id : undefined + const messageStatus = + typeof firstMessage?.message_status === 'string' ? firstMessage.message_status : undefined + + if (!messageId) { + throw new Error('WhatsApp API response did not include a message ID') + } + + return { + success: true, + output: { + success: true, + messageId, + messageStatus, + messagingProduct: + typeof data.messaging_product === 'string' ? data.messaging_product : undefined, + inputPhoneNumber: contacts[0]?.input ?? null, + whatsappUserId: contacts[0]?.wa_id ?? null, + contacts, + }, + } +} + +/** + * Shared output schema for every outbound send operation. Mirrors the + * `transformWhatsAppSendResponse` output so each tool stays consistent. + */ +export const whatsappSendOutputs = { + success: { type: 'boolean', description: 'WhatsApp message send success status' }, + messageId: { type: 'string', description: 'Unique WhatsApp message identifier' }, + messageStatus: { + type: 'string', + description: 'Initial delivery state returned by the API', + optional: true, + }, + messagingProduct: { + type: 'string', + description: 'Messaging product returned by the API', + optional: true, + }, + inputPhoneNumber: { + type: 'string', + description: 'Recipient phone number echoed back by WhatsApp', + optional: true, + }, + whatsappUserId: { + type: 'string', + description: 'WhatsApp user ID resolved for the recipient', + optional: true, + }, + contacts: { + type: 'array', + description: 'Recipient contact records returned by WhatsApp', + optional: true, + items: { + type: 'object', + properties: { + input: { type: 'string', description: 'Input phone number sent to the API' }, + wa_id: { + type: 'string', + description: 'WhatsApp user ID associated with the recipient', + optional: true, + }, + }, + }, + }, +} as const