feat(integrations): extend Airtable, Google Docs, WhatsApp, and Excel tool depth (#5256)

* feat(airtable): add delete and upsert record tools

* feat(google-docs): add batchUpdate text, table, image, and style tools

* feat(whatsapp): add template, media, interactive, reaction, and mark-read tools

* feat(microsoft-excel): add clear, format, create-table, sort, and delete-worksheet tools

* fix(integrations): address review feedback

- google-docs: use camelCase fontSize field mask; normalize string booleans for bold/italic/underline and matchCase
- microsoft-excel: escape OData single quotes in worksheet/table keys; validate range for clear/format
- airtable: enforce batch limits (delete <=10 ids, upsert <=10 records and 1-3 merge fields) with clear errors

* fix(integrations): address round-2 review

- airtable: coerce upsert typecast as string-aware boolean (string "false" no longer truthy)
- microsoft-excel: format_range surfaces precise partial-state error when fill PATCH fails after font (no atomic font+fill endpoint in Graph)

* fix(whatsapp): treat 2xx mark-as-read as success unless body says success:false

* fix(integrations): build fix, doc accuracy, and comment cleanup

- google-docs: align manualDocumentId condition with the document selector so the documentId canonical group has matching conditions (fixes canonical-pair block test / build)
- microsoft-excel: describe fill/font color as hex code only (Graph does not reliably accept named colors)
- remove verbose explanatory inline comments from new tools (keep idiomatic section dividers)
- regenerate integration docs + integrations.json catalog from the block registry

* fix(integrations): harden delete-record id coercion and excel sort-column validation

- airtable: coerce recordIds entries via String() so numeric JSON values don't crash on .trim()
- microsoft-excel: drop the silent sortColumn default to 0 so invalid input surfaces the tool's clear validation error (both v1 and v2 blocks)

* fix(airtable): coerce upsert fieldsToMergeOn entries via String() to handle non-string values
This commit is contained in:
Waleed
2026-06-28 22:42:32 -07:00
committed by GitHub
parent c59631698f
commit d878f15815
49 changed files with 6566 additions and 126 deletions
+25
View File
@@ -5730,6 +5730,31 @@ export function McpIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function A2AIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 860 860' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='544' cy='307' r='27' fill='currentColor' />
<circle cx='154' cy='307' r='27' fill='currentColor' />
<circle cx='706' cy='307' r='27' fill='currentColor' />
<circle cx='316' cy='307' r='27' fill='currentColor' />
<path
d='M336.5 191H162C97.66 191 45.5 243.16 45.5 307.5C45.5 371.84 97.64 424 161.99 424C206.55 424 256.29 424 296.5 424C487.5 424 374 191.01 569 191C613.89 191 658.97 191 698.03 191C762.37 191 814.5 243.16 814.5 307.5C814.5 371.84 762.34 424 698 424H523.5'
stroke='currentColor'
strokeWidth='48'
strokeLinecap='round'
/>
<path
d='M256 510C270.36 510 282 521.64 282 536C282 550.36 270.36 562 256 562H148C133.64 562 122 550.36 122 536C122 521.64 133.64 510 148 510H256ZM712 510C726.36 510 738 521.64 738 536C738 550.36 726.36 562 712 562H360C345.64 562 334 550.36 334 536C334 521.64 345.64 510 360 510H712Z'
fill='currentColor'
/>
<path
d='M444 628C458.36 628 470 639.64 470 654C470 668.36 458.36 680 444 680H100C85.64 680 74 668.36 74 654C74 639.64 85.64 628 100 628H444ZM548 628C562.36 628 574 639.64 574 654C574 668.36 562.36 680 548 680C533.64 680 522 668.36 522 654C522 639.64 533.64 628 548 628ZM760 628C774.36 628 786 639.64 786 654C786 668.36 774.36 680 760 680H652C637.64 680 626 668.36 626 654C626 639.64 637.64 628 652 628H760Z'
fill='currentColor'
/>
</svg>
)
}
export function WordpressIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 25.93 25.93'>
@@ -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
@@ -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\) |
@@ -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 |
@@ -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\). |
@@ -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 |
@@ -216,6 +216,7 @@
"tinybird",
"trello",
"trigger_dev",
"twilio",
"twilio_sms",
"twilio_voice",
"typeform",
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1,95 @@
---
title: Twilio
description: Twilio triggers for automating workflows
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="twilio"
color="#6B7280"
/>
## 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 |
@@ -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 |
@@ -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
+93 -5
View File
@@ -11,7 +11,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
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<AirtableResponse> = {
{ 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' },
+241 -5
View File
@@ -8,10 +8,10 @@ import type { GoogleDocsResponse } from '@/tools/google_docs/types'
export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
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<GoogleDocsResponse> = {
{ 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<GoogleDocsResponse> = {
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<GoogleDocsResponse> = {
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<string, unknown> = {}
for (const field of numericFields) {
const value = (rest as Record<string, unknown>)[field]
const num = toNumber(value)
if (num !== undefined) coerced[field] = num
else delete (rest as Record<string, unknown>)[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' },
},
}
+690 -65
View File
@@ -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<Record<string, any>>({
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<MicrosoftExcelResponse> = {
type: 'microsoft_excel',
name: 'Microsoft Excel (Legacy)',
@@ -31,6 +77,11 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
{ 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<MicrosoftExcelResponse> = {
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.')
}
const effectiveDriveId = driveId ? String(driveId).trim() : undefined
const trimmedRange = range ? String(range).trim() : undefined
if (params.operation === 'worksheet_add' && !worksheetName) {
throw new Error('Worksheet name is required for worksheet operations.')
}
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 (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
}
}
if (params.operation === 'worksheet_add') {
return {
...baseParams,
worksheetName,
}
}
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<MicrosoftExcelV2Response> = {
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<MicrosoftExcelV2Response> = {
},
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,30 +1064,108 @@ 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.')
const base = {
spreadsheetId: effectiveSpreadsheetId,
driveId: effectiveDriveId,
oauthCredential,
}
return {
...rest,
spreadsheetId: effectiveSpreadsheetId,
sheetName: effectiveSheetName,
cellRange: cellRange ? (cellRange as string).trim() : undefined,
driveId: driveId ? String(driveId).trim() : undefined,
values: parsedValues,
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
+195 -6
View File
@@ -9,7 +9,8 @@ export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
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<WhatsAppResponse> = {
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<WhatsAppResponse> = {
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<WhatsAppResponse> = {
...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<WhatsAppResponse> = {
},
},
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<WhatsAppResponse> = {
}
export const WhatsAppBlockMeta = {
tags: ['messaging', 'automation'],
tags: ['messaging', 'automation', 'customer-support', 'marketing'],
url: 'https://www.whatsapp.com',
templates: [
{
+290 -26
View File
@@ -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",
+106
View File
@@ -0,0 +1,106 @@
import type { AirtableDeleteParams, AirtableDeleteResponse } from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
export const airtableDeleteRecordsTool: ToolConfig<AirtableDeleteParams, AirtableDeleteResponse> = {
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' },
},
},
},
}
+4
View File
@@ -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'
+42
View File
@@ -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<string, any> }>
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
+144
View File
@@ -0,0 +1,144 @@
import type { AirtableUpsertParams, AirtableUpsertResponse } from '@/tools/airtable/types'
import type { ToolConfig } from '@/tools/types'
export const airtableUpsertRecordsTool: ToolConfig<AirtableUpsertParams, AirtableUpsertResponse> = {
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<string, unknown> = {
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' },
},
},
},
}
+14
View File
@@ -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'
+132
View File
@@ -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<GoogleDocsToolParams, GoogleDocsInsertImageResponse> = {
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<string, unknown> = {
...buildInsertLocation(params.index),
uri: params.imageUrl,
}
const objectSize: Record<string, unknown> = {}
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' },
},
},
},
}
@@ -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' },
},
},
},
}
+120
View File
@@ -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<GoogleDocsToolParams, GoogleDocsInsertTableResponse> = {
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' },
},
},
},
}
+108
View File
@@ -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<GoogleDocsToolParams, GoogleDocsInsertTextResponse> = {
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' },
},
},
},
}
+117
View File
@@ -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<GoogleDocsToolParams, GoogleDocsReplaceTextResponse> = {
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' },
},
},
},
}
+64
View File
@@ -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
@@ -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<string, unknown> = {}
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' },
},
},
},
}
+75
View File
@@ -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<string, never> } {
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 = ''
@@ -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' },
},
},
},
}
@@ -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' },
},
},
},
}
@@ -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' },
},
},
},
}
@@ -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<string, unknown> | null {
const body: Record<string, unknown> = {}
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<string, unknown> | 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' },
},
},
},
}
+14
View File
@@ -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'
@@ -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<string, unknown> = {
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' },
},
},
},
}
+112
View File
@@ -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 {
+61
View File
@@ -110,6 +110,17 @@ export async function extractGraphError(response: Response): Promise<string> {
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!<base64> 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 []
+44 -2
View File
@@ -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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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,
+5
View File
@@ -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'
+76
View File
@@ -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<WhatsAppMarkReadParams, WhatsAppMarkReadResponse> = {
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',
},
},
}
+139
View File
@@ -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<WhatsAppSendInteractiveParams, WhatsAppSendResponse> =
{
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<string, unknown> = {
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,
}
+117
View File
@@ -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<WhatsAppMediaType> = new Set(['image', 'video', 'document'])
export const sendMediaTool: ToolConfig<WhatsAppSendMediaParams, WhatsAppSendResponse> = {
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<string, string> = 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,
}
+78
View File
@@ -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<WhatsAppSendReactionParams, WhatsAppSendResponse> = {
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,
}
+103
View File
@@ -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<WhatsAppSendTemplateParams, WhatsAppSendResponse> = {
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<string, unknown> = {
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,
}
+73 -10
View File
@@ -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
}
}
+135
View File
@@ -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<string, string> {
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<string, unknown> {
return typeof value === 'object' && value !== null
}
async function parseWhatsAppResponse(response: Response): Promise<Record<string, unknown>> {
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<string, unknown>, 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<WhatsAppSendResponse> {
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