feat(sendblue): add Sendblue iMessage/SMS integration with tools and triggers (#4917)

* feat(sendblue): add Sendblue iMessage/SMS integration with tools and triggers

* fix(sendblue): address review — status-aware webhook dedup, shared routing map, uniform output casing, api-key authType

* fix(docs-gen): skip nested array `items` descriptor in tool input tables

* chore(sendblue): use SendblueSendStyle type, trim URL identifiers

* fix(sendblue): keep is_outbound routing map local to the webhook handler

Avoids a webhook-providers -> triggers cross-subgraph import (single source
of truth in the handler, its only runtime consumer).

* fix(sendblue): remove invalid `tags` from block config (belongs on BlockMeta)
This commit is contained in:
Waleed
2026-06-08 19:41:09 -07:00
committed by GitHub
parent 4f00baf6b4
commit a72e35e2f4
28 changed files with 1757 additions and 0 deletions
+15
View File
@@ -2047,6 +2047,21 @@ export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SendblueIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 72 72' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='currentColor'
d='M58.5425 5.19973C53.9872 5.19973 50.2944 8.89316 50.2944 13.4492C50.2944 18.0053 53.9872 21.6987 58.5425 21.6987C63.0978 21.6987 66.7906 18.0053 66.7907 13.4492C66.7907 8.89316 63.0978 5.19973 58.5425 5.19973Z'
/>
<path
fill='currentColor'
d='M0.0122017 36C0.0122012 41.6798 4.37038 46.3269 9.92916 46.8072C6.33936 51.07 6.54347 57.4463 10.5535 61.457C14.5635 65.4676 20.9387 65.6718 25.2008 62.0814C25.669 67.6411 30.3274 72 36.0062 72C41.685 72 46.3314 67.6411 46.8116 62.0814C51.0737 65.6718 57.4489 65.4676 61.4589 61.457C65.4689 57.4463 65.673 51.0701 62.0832 46.8072C67.642 46.3389 72.0002 41.6798 72.0002 36C72.0002 30.008 67.1378 25.1448 61.1468 25.1448C60.7266 25.1448 60.3183 25.1808 59.9101 25.2288C59.4659 25.2769 59.0097 25.2889 58.5415 25.2889C56.9927 25.2889 55.5159 24.9887 54.1713 24.4483C51.1458 23.2475 48.7446 20.8339 47.544 17.8199C47.0037 16.463 46.7035 14.986 46.7035 13.449C46.7035 12.9807 46.7155 12.5364 46.7636 12.0801C46.8116 11.6718 46.8476 11.2635 46.8476 10.8553C46.8476 4.86326 41.9852 1.65468e-05 35.9942 1.60229e-05C30.3154 1.55264e-05 25.669 4.35892 25.1888 9.91862C20.9267 6.32823 14.5515 6.53237 10.5415 10.543C6.53146 14.5537 6.32736 20.93 9.91715 25.1928C4.35837 25.6611 0.000195295 30.3202 0.000194799 36L0.0122017 36Z'
/>
</svg>
)
}
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
+2
View File
@@ -167,6 +167,7 @@ import {
SapS4HanaIcon,
SESIcon,
SecretsManagerIcon,
SendblueIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
@@ -392,6 +393,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sap_concur: SapConcurIcon,
sap_s4hana: SapS4HanaIcon,
secrets_manager: SecretsManagerIcon,
sendblue: SendblueIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
@@ -157,6 +157,7 @@
"sap_concur",
"sap_s4hana",
"secrets_manager",
"sendblue",
"sendgrid",
"sentry",
"serper",
@@ -0,0 +1,197 @@
---
title: Sendblue
description: Send and receive iMessage and SMS
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="sendblue"
color="#008BFF"
/>
{/* MANUAL-CONTENT-START:intro */}
## Manual Description
Sendblue connects your agents to iMessage and SMS through your own dedicated phone number. Use it to text one person or a group, attach images and other media, check whether a number can receive iMessage before you send, show a typing indicator, and look up the delivery status of any message.
Authentication uses a Sendblue **API Key ID** and **API Secret Key**, sent as the `sb-api-key-id` and `sb-api-secret-key` headers. You can find both in your [Sendblue dashboard](https://dashboard.sendblue.com). Every message is sent from one of your registered Sendblue lines, supplied as the **From Number** in E.164 format (for example `+15551234567`).
**Operations**
- **Send Message** — send an iMessage or SMS to a single recipient. Provide message text, a media URL, or both, and optionally apply an iMessage expressive style (celebration, fireworks, lasers, confetti, and more).
- **Send Group Message** — send to multiple recipients at once. Pass one recipient per line; reuse the returned `group_id` to keep replying in the same thread.
- **Evaluate Service** — check whether a number is reachable on iMessage or only SMS, so you can branch before sending.
- **Send Typing Indicator** — show a recipient that a reply is being composed (one-to-one chats only).
- **Get Message** — retrieve a single message and its current status by message handle.
**Triggers**
- **Message Received** — fires on every inbound message. Configure it as the **Receive** webhook in your Sendblue dashboard.
- **Message Status Updated** — fires when an outbound message changes state (`SENT`, `DELIVERED`, `ERROR`). Configure it as the **Outbound** webhook, or pass its URL per message as `status_callback`.
Each trigger generates its own webhook URL — paste the matching URL into the corresponding field in your Sendblue dashboard.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates.
## Tools
### `sendblue_send_message`
Send an iMessage or SMS to a single recipient via Sendblue.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `number` | string | Yes | Recipient phone number in E.164 format \(e.g., +19998887777\) |
| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) |
| `content` | string | No | Message text content. Either content or media_url must be provided. |
| `media_url` | string | No | URL of a media file to send. Either content or media_url must be provided. |
| `send_style` | string | No | iMessage expressive style \(e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam\). |
| `status_callback` | string | No | Webhook URL that Sendblue will POST message status updates to. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Message status: QUEUED, SENT, DELIVERED, or ERROR |
| `message_handle` | string | Unique identifier for tracking the message |
| `account_email` | string | Email of the account that sent the message |
| `content` | string | Message content |
| `is_outbound` | boolean | Whether this is an outbound message |
| `from_number` | string | Sending phone number |
| `number` | string | Recipient phone number |
| `media_url` | string | URL of attached media |
| `send_style` | string | iMessage expressive style applied |
| `seat_id` | string | UUID of the seat that sent the message |
| `sender_email` | string | Email of the seat \(user\) that sent the message |
| `error_code` | number | Numeric error code if the message failed |
| `error_message` | string | Error message if the message failed |
| `date_created` | string | When the message was created |
| `date_updated` | string | When the message was last updated |
### `sendblue_send_group_message`
Send an iMessage or SMS to a group of recipients via Sendblue.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `numbers` | array | Yes | Recipient phone numbers in E.164 format \(e.g., \["+19998887777", "+13334445555"\]\) |
| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) |
| `content` | string | No | Message text content. Either content or media_url must be provided. |
| `media_url` | string | No | URL of a media file to send. Either content or media_url must be provided. |
| `send_style` | string | No | iMessage expressive style \(e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam\). |
| `group_id` | string | No | Unique identifier of an existing group to send to. Omit to start a new group. |
| `status_callback` | string | No | Webhook URL that Sendblue will POST message status updates to. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Message status: QUEUED, SENT, DELIVERED, or ERROR |
| `message_handle` | string | Unique identifier for tracking the message |
| `group_id` | string | Identifier of the group the message was sent to |
| `participants` | array | Phone numbers participating in the group |
| `account_email` | string | Email of the account that sent the message |
| `content` | string | Message content |
| `is_outbound` | boolean | Whether this is an outbound message |
| `from_number` | string | Sending phone number |
| `number` | string | Recipient phone number |
| `media_url` | string | URL of attached media |
| `send_style` | string | iMessage expressive style applied |
| `seat_id` | string | UUID of the seat that sent the message |
| `sender_email` | string | Email of the seat \(user\) that sent the message |
| `error_code` | number | Numeric error code if the message failed |
| `error_message` | string | Error message if the message failed |
| `date_created` | string | When the message was created |
| `date_updated` | string | When the message was last updated |
### `sendblue_evaluate_service`
Check whether a phone number can receive iMessage or only SMS.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `number` | string | Yes | Phone number to evaluate, in E.164 format \(e.g., +19998887777\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `number` | string | The evaluated phone number in E.164 format |
| `service` | string | The service the number supports: iMessage or SMS |
### `sendblue_send_typing_indicator`
Display a typing indicator to a recipient (not supported in group chats).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `number` | string | Yes | Recipient's phone number in E.164 format \(e.g., +19998887777\) |
| `from_number` | string | No | Your Sendblue line number to send from, in E.164 format. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Delivery status of the typing indicator \(e.g., QUEUED\) |
| `status_code` | number | Numeric status code returned by Sendblue |
| `number` | string | The recipient phone number |
| `error_message` | string | Error details, null on success |
### `sendblue_get_message`
Retrieve a single message and its current status by message handle/ID.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `message_id` | string | Yes | The message handle/ID returned when the message was sent. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Current message status |
| `message_handle` | string | Unique message identifier |
| `account_email` | string | Email of the account |
| `content` | string | Message content |
| `is_outbound` | boolean | Whether the message is outbound |
| `from_number` | string | Sending phone number |
| `number` | string | Recipient phone number |
| `to_number` | string | Destination phone number |
| `media_url` | string | URL of attached media |
| `message_type` | string | Message category: message or group |
| `service` | string | Messaging service: iMessage, SMS, or RCS |
| `group_id` | string | Group identifier \(empty for non-group\) |
| `group_display_name` | string | Group chat name |
| `participants` | array | Participant phone numbers |
| `send_style` | string | Expressive style applied |
| `was_downgraded` | boolean | True if the recipient lacks iMessage support |
| `opted_out` | boolean | True if the recipient has opted out |
| `plan` | string | Account plan type |
| `sendblue_number` | string | Sendblue phone number used |
| `seat_id` | string | Seat UUID |
| `sender_email` | string | Email of the sending seat |
| `error_code` | number | Numeric error code if failed |
| `error_message` | string | Error message if failed |
| `error_reason` | string | Additional error context |
| `error_detail` | string | Detailed error information |
| `date_sent` | string | ISO 8601 creation timestamp |
| `date_updated` | string | ISO 8601 last-update timestamp |
@@ -38,6 +38,7 @@
"outlook",
"resend",
"salesforce",
"sendblue",
"servicenow",
"slack",
"stripe",
@@ -0,0 +1,93 @@
---
title: Sendblue
description: Available Sendblue triggers for automating workflows
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="sendblue"
color="#008BFF"
/>
Sendblue provides 2 triggers for automating workflows based on events.
## Triggers
### Sendblue Message Received
Trigger when an inbound iMessage or SMS is received in Sendblue
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `account_email` | string | Email of the Sendblue account |
| `content` | string | Message text content |
| `media_url` | string | CDN link to attached media, if any |
| `is_outbound` | boolean | True for outbound messages, false for inbound |
| `status` | string | Message status \(e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR\) |
| `error_code` | number | Error identifier, null if none |
| `error_message` | string | Descriptive error text, null if none |
| `error_reason` | string | Additional error context, null if none |
| `error_detail` | string | Detailed error information, null if none |
| `message_handle` | string | Sendblue message identifier \(use to deduplicate\) |
| `date_sent` | string | ISO 8601 creation timestamp |
| `date_updated` | string | ISO 8601 last-update timestamp |
| `from_number` | string | E.164 sender phone number |
| `number` | string | E.164 recipient/counterparty phone number |
| `to_number` | string | E.164 destination phone number |
| `was_downgraded` | boolean | True if the recipient lacks iMessage support |
| `plan` | string | Account plan type |
| `message_type` | string | Message category \(e.g., message, group\) |
| `group_id` | string | Group identifier, empty for non-group messages |
| `participants` | array | Participant phone numbers for group messages |
| `send_style` | string | Expressive style if applied |
| `opted_out` | boolean | True if the recipient has opted out |
| `sendblue_number` | string | Sendblue phone number used |
| `service` | string | Messaging service \(iMessage or SMS\) |
| `group_display_name` | string | Group chat name, null for non-group messages |
| `sender_email` | string | Email of the user who sent the message |
| `seat_id` | string | Seat UUID, null if absent |
| `raw` | string | Complete raw webhook payload from Sendblue as a JSON string |
---
### Sendblue Message Status Updated
Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `account_email` | string | Email of the Sendblue account |
| `content` | string | Message text content |
| `media_url` | string | CDN link to attached media, if any |
| `is_outbound` | boolean | True for outbound messages, false for inbound |
| `status` | string | Message status \(e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR\) |
| `error_code` | number | Error identifier, null if none |
| `error_message` | string | Descriptive error text, null if none |
| `error_reason` | string | Additional error context, null if none |
| `error_detail` | string | Detailed error information, null if none |
| `message_handle` | string | Sendblue message identifier \(use to deduplicate\) |
| `date_sent` | string | ISO 8601 creation timestamp |
| `date_updated` | string | ISO 8601 last-update timestamp |
| `from_number` | string | E.164 sender phone number |
| `number` | string | E.164 recipient/counterparty phone number |
| `to_number` | string | E.164 destination phone number |
| `was_downgraded` | boolean | True if the recipient lacks iMessage support |
| `plan` | string | Account plan type |
| `message_type` | string | Message category \(e.g., message, group\) |
| `group_id` | string | Group identifier, empty for non-group messages |
| `participants` | array | Participant phone numbers for group messages |
| `send_style` | string | Expressive style if applied |
| `opted_out` | boolean | True if the recipient has opted out |
| `sendblue_number` | string | Sendblue phone number used |
| `service` | string | Messaging service \(iMessage or SMS\) |
| `group_display_name` | string | Group chat name, null for non-group messages |
| `sender_email` | string | Email of the user who sent the message |
| `seat_id` | string | Seat UUID, null if absent |
| `raw` | string | Complete raw webhook payload from Sendblue as a JSON string |
+381
View File
@@ -0,0 +1,381 @@
import { SendblueIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getTrigger } from '@/triggers'
const SEND_STYLE_OPTIONS = [
{ label: 'None', id: '' },
{ label: 'Celebration', id: 'celebration' },
{ label: 'Shooting Star', id: 'shooting_star' },
{ label: 'Fireworks', id: 'fireworks' },
{ label: 'Lasers', id: 'lasers' },
{ label: 'Love', id: 'love' },
{ label: 'Confetti', id: 'confetti' },
{ label: 'Balloons', id: 'balloons' },
{ label: 'Spotlight', id: 'spotlight' },
{ label: 'Echo', id: 'echo' },
{ label: 'Invisible', id: 'invisible' },
{ label: 'Gentle', id: 'gentle' },
{ label: 'Loud', id: 'loud' },
{ label: 'Slam', id: 'slam' },
] as const
export const SendblueBlock: BlockConfig = {
type: 'sendblue',
name: 'Sendblue',
description: 'Send and receive iMessage and SMS',
longDescription:
'Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates.',
docsLink: 'https://docs.sim.ai/tools/sendblue',
category: 'tools',
integrationType: IntegrationType.Communication,
bgColor: '#008BFF',
icon: SendblueIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Send Message', id: 'sendblue_send_message' },
{ label: 'Send Group Message', id: 'sendblue_send_group_message' },
{ label: 'Evaluate Service', id: 'sendblue_evaluate_service' },
{ label: 'Send Typing Indicator', id: 'sendblue_send_typing_indicator' },
{ label: 'Get Message', id: 'sendblue_get_message' },
],
value: () => 'sendblue_send_message',
},
{
id: 'apiKeyId',
title: 'API Key ID',
type: 'short-input',
placeholder: 'Your Sendblue API Key ID (sb-api-key-id)',
password: true,
required: true,
},
{
id: 'apiSecretKey',
title: 'API Secret Key',
type: 'short-input',
placeholder: 'Your Sendblue API Secret Key (sb-api-secret-key)',
password: true,
required: true,
},
{
id: 'from_number',
title: 'From Number',
type: 'short-input',
placeholder: 'e.g. +18887776666',
condition: {
field: 'operation',
value: [
'sendblue_send_message',
'sendblue_send_group_message',
'sendblue_send_typing_indicator',
],
},
required: {
field: 'operation',
value: ['sendblue_send_message', 'sendblue_send_group_message'],
},
},
{
id: 'number',
title: 'Recipient Number',
type: 'short-input',
placeholder: 'e.g. +19998887777',
condition: {
field: 'operation',
value: [
'sendblue_send_message',
'sendblue_evaluate_service',
'sendblue_send_typing_indicator',
],
},
required: {
field: 'operation',
value: [
'sendblue_send_message',
'sendblue_evaluate_service',
'sendblue_send_typing_indicator',
],
},
},
{
id: 'numbers',
title: 'Recipient Numbers',
type: 'long-input',
placeholder: 'One phone number per line, e.g.\n+19998887777\n+13334445555',
condition: { field: 'operation', value: 'sendblue_send_group_message' },
required: { field: 'operation', value: 'sendblue_send_group_message' },
},
{
id: 'content',
title: 'Message',
type: 'long-input',
placeholder: 'Message text (required unless a media URL is provided)',
condition: {
field: 'operation',
value: ['sendblue_send_message', 'sendblue_send_group_message'],
},
},
{
id: 'media_url',
title: 'Media URL',
type: 'short-input',
placeholder: 'https://example.com/image.jpg',
mode: 'advanced',
condition: {
field: 'operation',
value: ['sendblue_send_message', 'sendblue_send_group_message'],
},
},
{
id: 'send_style',
title: 'Send Style',
type: 'dropdown',
options: [...SEND_STYLE_OPTIONS],
value: () => '',
mode: 'advanced',
condition: {
field: 'operation',
value: ['sendblue_send_message', 'sendblue_send_group_message'],
},
},
{
id: 'group_id',
title: 'Group ID',
type: 'short-input',
placeholder: 'Existing group ID (leave blank to start a new group)',
mode: 'advanced',
condition: { field: 'operation', value: 'sendblue_send_group_message' },
},
{
id: 'status_callback',
title: 'Status Callback URL',
type: 'short-input',
placeholder: 'https://your-app.com/sendblue-status',
mode: 'advanced',
condition: {
field: 'operation',
value: ['sendblue_send_message', 'sendblue_send_group_message'],
},
},
{
id: 'message_id',
title: 'Message Handle / ID',
type: 'short-input',
placeholder: 'The message handle returned when sending',
condition: { field: 'operation', value: 'sendblue_get_message' },
required: { field: 'operation', value: 'sendblue_get_message' },
},
...getTrigger('sendblue_message_received').subBlocks,
...getTrigger('sendblue_message_status_updated').subBlocks,
],
tools: {
access: [
'sendblue_send_message',
'sendblue_send_group_message',
'sendblue_evaluate_service',
'sendblue_send_typing_indicator',
'sendblue_get_message',
],
config: {
tool: (params) => params.operation || 'sendblue_send_message',
params: (params) => {
const base: Record<string, any> = {
apiKeyId: params.apiKeyId,
apiSecretKey: params.apiSecretKey,
}
switch (params.operation) {
case 'sendblue_send_message':
return {
...base,
number: params.number,
from_number: params.from_number,
content: params.content || undefined,
media_url: params.media_url || undefined,
send_style: params.send_style || undefined,
status_callback: params.status_callback || undefined,
}
case 'sendblue_send_group_message':
return {
...base,
numbers:
typeof params.numbers === 'string'
? params.numbers
.split('\n')
.map((n: string) => n.trim())
.filter(Boolean)
: params.numbers,
from_number: params.from_number,
content: params.content || undefined,
media_url: params.media_url || undefined,
send_style: params.send_style || undefined,
group_id: params.group_id || undefined,
status_callback: params.status_callback || undefined,
}
case 'sendblue_evaluate_service':
return { ...base, number: params.number }
case 'sendblue_send_typing_indicator':
return {
...base,
number: params.number,
from_number: params.from_number || undefined,
}
case 'sendblue_get_message':
return { ...base, message_id: params.message_id }
default:
return base
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
apiKeyId: { type: 'string', description: 'Sendblue API Key ID' },
apiSecretKey: { type: 'string', description: 'Sendblue API Secret Key' },
number: { type: 'string', description: 'Recipient phone number (E.164)' },
numbers: { type: 'string', description: 'Recipient phone numbers, one per line (E.164)' },
from_number: { type: 'string', description: 'Sender Sendblue phone number (E.164)' },
content: { type: 'string', description: 'Message text content' },
media_url: { type: 'string', description: 'URL of media to send' },
send_style: { type: 'string', description: 'iMessage expressive style' },
group_id: { type: 'string', description: 'Existing group ID' },
status_callback: { type: 'string', description: 'Status callback webhook URL' },
message_id: { type: 'string', description: 'Message handle/ID to retrieve' },
},
outputs: {
status: { type: 'string', description: 'Message or request status' },
message_handle: { type: 'string', description: 'Unique message identifier' },
service: { type: 'string', description: 'Service the number supports (iMessage or SMS)' },
number: { type: 'string', description: 'Recipient phone number' },
from_number: { type: 'string', description: 'Sending phone number' },
content: { type: 'string', description: 'Message content' },
media_url: { type: 'string', description: 'URL of attached media' },
is_outbound: { type: 'boolean', description: 'Whether the message is outbound' },
group_id: { type: 'string', description: 'Group identifier' },
participants: { type: 'array', description: 'Group participant phone numbers' },
send_style: { type: 'string', description: 'Expressive style applied' },
account_email: { type: 'string', description: 'Account email' },
sender_email: { type: 'string', description: 'Sending seat email' },
seat_id: { type: 'string', description: 'Seat UUID' },
status_code: { type: 'number', description: 'Numeric status code (typing indicator)' },
error_code: { type: 'number', description: 'Numeric error code if failed' },
error_message: { type: 'string', description: 'Error message if failed' },
date_created: { type: 'string', description: 'Creation timestamp' },
date_updated: { type: 'string', description: 'Last-update timestamp' },
},
triggers: {
enabled: true,
available: ['sendblue_message_received', 'sendblue_message_status_updated'],
},
}
export const SendblueBlockMeta = {
tags: ['messaging', 'automation', 'webhooks'],
templates: [
{
icon: SendblueIcon,
title: 'Sendblue lead speed-to-text',
prompt:
'Build a workflow that fires when a new lead submits a form, drafts a friendly intro, and sends it as an iMessage via Sendblue within seconds so reps reach hot leads while they are still interested.',
modules: ['agent', 'workflows'],
category: 'sales',
tags: ['messaging', 'sales', 'automation'],
alsoIntegrations: ['typeform'],
},
{
icon: SendblueIcon,
title: 'Sendblue appointment reminders',
prompt:
"Create a scheduled workflow that reads tomorrow's appointments from a table and sends each customer a personalized Sendblue iMessage reminder with the time and a reschedule link.",
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['messaging', 'automation', 'scheduling'],
},
{
icon: SendblueIcon,
title: 'Sendblue inbound reply autoresponder',
prompt:
'Build a workflow triggered when a Sendblue message is received that classifies the inbound text, drafts a context-aware reply with an agent, and sends it back to the same number as an iMessage.',
modules: ['agent', 'workflows'],
category: 'support',
tags: ['messaging', 'automation', 'support'],
},
{
icon: SendblueIcon,
title: 'Sendblue iMessage vs SMS routing',
prompt:
'Create a workflow that evaluates whether a recipient number supports iMessage with Sendblue, then sends a rich iMessage when supported or a plain SMS fallback otherwise.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['messaging', 'automation'],
},
{
icon: SendblueIcon,
title: 'Sendblue order-status notifier',
prompt:
'Create a workflow triggered when an order ships that looks up the customer phone number and sends a Sendblue iMessage with the tracking number and estimated delivery date.',
modules: ['agent', 'workflows'],
category: 'operations',
tags: ['messaging', 'automation'],
alsoIntegrations: ['shopify'],
},
{
icon: SendblueIcon,
title: 'Sendblue delivery-failure alerts',
prompt:
'Build a workflow triggered by a Sendblue message status update that, when the status is ERROR, posts the failing number and error message to a Slack channel so the team can follow up.',
modules: ['agent', 'workflows'],
category: 'operations',
tags: ['messaging', 'automation', 'incident-management'],
alsoIntegrations: ['slack'],
},
{
icon: SendblueIcon,
title: 'Sendblue group broadcast',
prompt:
'Create a workflow that reads a list of VIP customers from a table and sends them a single Sendblue group iMessage announcing a new product launch with an image attachment.',
modules: ['tables', 'agent', 'workflows'],
category: 'marketing',
tags: ['messaging', 'marketing', 'automation'],
},
{
icon: SendblueIcon,
title: 'Sendblue conversational support agent',
prompt:
'Build a workflow triggered on inbound Sendblue messages that shows a typing indicator, looks up the customer in a table, answers their question with an agent, and replies over iMessage.',
modules: ['tables', 'agent', 'workflows'],
category: 'support',
tags: ['messaging', 'support', 'automation'],
},
],
skills: [
{
name: 'send-imessage-notification',
description: 'Send an iMessage or SMS notification to a single recipient via Sendblue.',
content:
'# Send an iMessage or SMS Notification\n\nDeliver a message to one recipient through your Sendblue number.\n\n## Steps\n1. Choose the Send Message operation.\n2. Enter the Recipient Number and your From Number in E.164 format (for example +19998887777).\n3. Write the Message text. To send media instead of or alongside text, add a Media URL.\n4. Provide your Sendblue API Key ID and API Secret Key.\n\n## Output\nReturn the message status (QUEUED, SENT, DELIVERED, ERROR) and the message handle so the send can be tracked.',
},
{
name: 'route-imessage-or-sms',
description: 'Check whether a number supports iMessage before sending with Sendblue.',
content:
'# Route iMessage vs SMS\n\nDecide how to reach a recipient based on their service.\n\n## Steps\n1. Use the Evaluate Service operation with the Recipient Number to learn whether the number supports iMessage or only SMS.\n2. Branch on the returned service value.\n3. Send a rich iMessage when supported, or a plain SMS fallback otherwise, using the Send Message operation.\n\n## Output\nReturn the evaluated number and its supported service so downstream steps can branch correctly.',
},
{
name: 'reply-to-inbound-message',
description: 'Trigger on an inbound Sendblue message and reply automatically.',
content:
'# Reply to an Inbound Message\n\nRespond to customers as soon as they text in.\n\n## Steps\n1. Add the Sendblue Message Received trigger and point your Sendblue Receive Webhook at the generated URL.\n2. Read the inbound content, from_number, and service from the trigger output.\n3. Draft a reply with an agent and send it back with the Send Message operation, using the from_number as the recipient.\n\n## Output\nReturn the reply message handle and status so the conversation can be tracked.',
},
],
} as const satisfies BlockMeta
+3
View File
@@ -242,6 +242,7 @@ import { SapS4HanaBlock, SapS4HanaBlockMeta } from '@/blocks/blocks/sap_s4hana'
import { ScheduleBlock } from '@/blocks/blocks/schedule'
import { SearchBlock } from '@/blocks/blocks/search'
import { SecretsManagerBlock, SecretsManagerBlockMeta } from '@/blocks/blocks/secrets_manager'
import { SendblueBlock, SendblueBlockMeta } from '@/blocks/blocks/sendblue'
import { SendGridBlock, SendGridBlockMeta } from '@/blocks/blocks/sendgrid'
import { SentryBlock, SentryBlockMeta } from '@/blocks/blocks/sentry'
import { SerperBlock, SerperBlockMeta } from '@/blocks/blocks/serper'
@@ -525,6 +526,7 @@ const BLOCK_REGISTRY: Record<string, BlockConfig> = {
schedule: ScheduleBlock,
search: SearchBlock,
secrets_manager: SecretsManagerBlock,
sendblue: SendblueBlock,
sendgrid: SendGridBlock,
sentry: SentryBlock,
serper: SerperBlock,
@@ -773,6 +775,7 @@ const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
sap_concur: SapConcurBlockMeta,
sap_s4hana: SapS4HanaBlockMeta,
secrets_manager: SecretsManagerBlockMeta,
sendblue: SendblueBlockMeta,
sendgrid: SendGridBlockMeta,
sentry: SentryBlockMeta,
serper: SerperBlockMeta,
+15
View File
@@ -2047,6 +2047,21 @@ export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SendblueIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 72 72' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='currentColor'
d='M58.5425 5.19973C53.9872 5.19973 50.2944 8.89316 50.2944 13.4492C50.2944 18.0053 53.9872 21.6987 58.5425 21.6987C63.0978 21.6987 66.7906 18.0053 66.7907 13.4492C66.7907 8.89316 63.0978 5.19973 58.5425 5.19973Z'
/>
<path
fill='currentColor'
d='M0.0122017 36C0.0122012 41.6798 4.37038 46.3269 9.92916 46.8072C6.33936 51.07 6.54347 57.4463 10.5535 61.457C14.5635 65.4676 20.9387 65.6718 25.2008 62.0814C25.669 67.6411 30.3274 72 36.0062 72C41.685 72 46.3314 67.6411 46.8116 62.0814C51.0737 65.6718 57.4489 65.4676 61.4589 61.457C65.4689 57.4463 65.673 51.0701 62.0832 46.8072C67.642 46.3389 72.0002 41.6798 72.0002 36C72.0002 30.008 67.1378 25.1448 61.1468 25.1448C60.7266 25.1448 60.3183 25.1808 59.9101 25.2288C59.4659 25.2769 59.0097 25.2889 58.5415 25.2889C56.9927 25.2889 55.5159 24.9887 54.1713 24.4483C51.1458 23.2475 48.7446 20.8339 47.544 17.8199C47.0037 16.463 46.7035 14.986 46.7035 13.449C46.7035 12.9807 46.7155 12.5364 46.7636 12.0801C46.8116 11.6718 46.8476 11.2635 46.8476 10.8553C46.8476 4.86326 41.9852 1.65468e-05 35.9942 1.60229e-05C30.3154 1.55264e-05 25.669 4.35892 25.1888 9.91862C20.9267 6.32823 14.5515 6.53237 10.5415 10.543C6.53146 14.5537 6.32736 20.93 9.91715 25.1928C4.35837 25.6611 0.000195295 30.3202 0.000194799 36L0.0122017 36Z'
/>
</svg>
)
}
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -166,6 +166,7 @@ import {
SapS4HanaIcon,
SESIcon,
SecretsManagerIcon,
SendblueIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
@@ -370,6 +371,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sap_concur: SapConcurIcon,
sap_s4hana: SapS4HanaIcon,
secrets_manager: SecretsManagerIcon,
sendblue: SendblueIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
@@ -12699,6 +12699,56 @@
"integrationType": "hr",
"tags": ["automation"]
},
{
"type": "sendblue",
"slug": "sendblue",
"name": "Sendblue",
"description": "Send and receive iMessage and SMS",
"longDescription": "Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates.",
"bgColor": "#008BFF",
"iconName": "SendblueIcon",
"docsUrl": "https://docs.sim.ai/tools/sendblue",
"operations": [
{
"name": "Send Message",
"description": "Send an iMessage or SMS to a single recipient via Sendblue."
},
{
"name": "Send Group Message",
"description": "Send an iMessage or SMS to a group of recipients via Sendblue."
},
{
"name": "Evaluate Service",
"description": "Check whether a phone number can receive iMessage or only SMS."
},
{
"name": "Send Typing Indicator",
"description": "Display a typing indicator to a recipient (not supported in group chats)."
},
{
"name": "Get Message",
"description": "Retrieve a single message and its current status by message handle/ID."
}
],
"operationCount": 5,
"triggers": [
{
"id": "sendblue_message_received",
"name": "Sendblue Message Received",
"description": "Trigger when an inbound iMessage or SMS is received in Sendblue"
},
{
"id": "sendblue_message_status_updated",
"name": "Sendblue Message Status Updated",
"description": "Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue"
}
],
"triggerCount": 2,
"authType": "api-key",
"category": "tools",
"integrationType": "communication",
"tags": ["messaging", "automation", "webhooks"]
},
{
"type": "sendgrid",
"slug": "sendgrid",
@@ -32,6 +32,7 @@ import { outlookHandler } from '@/lib/webhooks/providers/outlook'
import { resendHandler } from '@/lib/webhooks/providers/resend'
import { rssHandler } from '@/lib/webhooks/providers/rss'
import { salesforceHandler } from '@/lib/webhooks/providers/salesforce'
import { sendblueHandler } from '@/lib/webhooks/providers/sendblue'
import { servicenowHandler } from '@/lib/webhooks/providers/servicenow'
import { slackHandler } from '@/lib/webhooks/providers/slack'
import { stripeHandler } from '@/lib/webhooks/providers/stripe'
@@ -82,6 +83,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
outlook: outlookHandler,
rss: rssHandler,
salesforce: salesforceHandler,
sendblue: sendblueHandler,
servicenow: servicenowHandler,
slack: slackHandler,
stripe: stripeHandler,
@@ -0,0 +1,95 @@
import { createLogger } from '@sim/logger'
import { getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type {
EventMatchContext,
FormatInputContext,
FormatInputResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
const logger = createLogger('WebhookProvider:Sendblue')
/**
* Maps Sendblue trigger IDs to the expected value of the webhook payload's
* `is_outbound` flag, used to route inbound vs. outbound status events.
* The handler is the only runtime consumer, so the map lives here (single
* source of truth) rather than crossing from the triggers graph into the
* webhook-providers graph.
*/
const SENDBLUE_TRIGGER_IS_OUTBOUND: Record<string, boolean> = {
sendblue_message_received: false,
sendblue_message_status_updated: true,
}
export const sendblueHandler: WebhookProviderHandler = {
matchEvent({ body, webhook, requestId }: EventMatchContext): boolean {
const providerConfig = getProviderConfig(webhook)
const triggerId = providerConfig.triggerId as string | undefined
if (!triggerId || !(triggerId in SENDBLUE_TRIGGER_IS_OUTBOUND)) return true
if (!isRecord(body)) {
logger.warn(`[${requestId}] Sendblue webhook payload was not an object`)
return false
}
const expected = SENDBLUE_TRIGGER_IS_OUTBOUND[triggerId]
const isOutbound = body.is_outbound === true
if (isOutbound !== expected) {
logger.info(`[${requestId}] Sendblue event did not match trigger`, { triggerId, isOutbound })
return false
}
return true
},
extractIdempotencyId(body: unknown): string | null {
if (!isRecord(body)) return null
const handle = body.message_handle
if (typeof handle !== 'string' || handle.length === 0) return null
// A single outbound message emits multiple status callbacks (e.g. SENT then
// DELIVERED) that share one message_handle, so the status is part of the key
// to keep distinct transitions from being deduped as retries.
const status = typeof body.status === 'string' && body.status.length > 0 ? body.status : null
return status ? `${handle}:${status}` : handle
},
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
const b = isRecord(body) ? body : {}
return {
input: {
account_email: b.accountEmail ?? b.account_email ?? null,
content: b.content ?? null,
media_url: b.media_url ?? null,
is_outbound: b.is_outbound ?? null,
status: b.status ?? null,
error_code: b.error_code ?? null,
error_message: b.error_message ?? null,
error_reason: b.error_reason ?? null,
error_detail: b.error_detail ?? null,
message_handle: b.message_handle ?? null,
date_sent: b.date_sent ?? null,
date_updated: b.date_updated ?? null,
from_number: b.from_number ?? null,
number: b.number ?? null,
to_number: b.to_number ?? null,
was_downgraded: b.was_downgraded ?? null,
plan: b.plan ?? null,
message_type: b.message_type ?? null,
group_id: b.group_id ?? null,
participants: b.participants ?? [],
send_style: b.send_style ?? null,
opted_out: b.opted_out ?? null,
sendblue_number: b.sendblue_number ?? null,
service: b.service ?? null,
group_display_name: b.group_display_name ?? null,
sender_email: b.sender_email ?? null,
seat_id: b.seat_id ?? null,
raw: JSON.stringify(b),
},
}
},
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
+12
View File
@@ -2637,6 +2637,13 @@ import {
secretsManagerListSecretsTool,
secretsManagerUpdateSecretTool,
} from '@/tools/secrets_manager'
import {
sendblueEvaluateServiceTool,
sendblueGetMessageTool,
sendblueSendGroupMessageTool,
sendblueSendMessageTool,
sendblueSendTypingIndicatorTool,
} from '@/tools/sendblue'
import {
sendGridAddContactsToListTool,
sendGridAddContactTool,
@@ -3659,6 +3666,11 @@ export const tools: Record<string, ToolConfig> = {
resend_update_contact: resendUpdateContactTool,
resend_delete_contact: resendDeleteContactTool,
resend_list_domains: resendListDomainsTool,
sendblue_send_message: sendblueSendMessageTool,
sendblue_send_group_message: sendblueSendGroupMessageTool,
sendblue_evaluate_service: sendblueEvaluateServiceTool,
sendblue_send_typing_indicator: sendblueSendTypingIndicatorTool,
sendblue_get_message: sendblueGetMessageTool,
sendgrid_send_mail: sendGridSendMailTool,
sendgrid_add_contact: sendGridAddContactTool,
sendgrid_get_contact: sendGridGetContactTool,
@@ -0,0 +1,56 @@
import type {
SendblueEvaluateServiceParams,
SendblueEvaluateServiceResponse,
} from '@/tools/sendblue/types'
import {
SENDBLUE_API_BASE_URL,
sendblueBaseParamFields,
sendblueHeaders,
} from '@/tools/sendblue/utils'
import type { ToolConfig } from '@/tools/types'
export const sendblueEvaluateServiceTool: ToolConfig<
SendblueEvaluateServiceParams,
SendblueEvaluateServiceResponse
> = {
id: 'sendblue_evaluate_service',
name: 'Sendblue Evaluate Service',
description: 'Check whether a phone number can receive iMessage or only SMS.',
version: '1.0.0',
params: {
...sendblueBaseParamFields,
number: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Phone number to evaluate, in E.164 format (e.g., +19998887777)',
},
},
request: {
url: (params) => {
const url = new URL(`${SENDBLUE_API_BASE_URL}/api/evaluate-service`)
url.searchParams.set('number', params.number.trim())
return url.toString()
},
method: 'GET',
headers: (params) => sendblueHeaders(params),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
number: data.number ?? null,
service: data.service ?? null,
},
}
},
outputs: {
number: { type: 'string', description: 'The evaluated phone number in E.164 format' },
service: { type: 'string', description: 'The service the number supports: iMessage or SMS' },
},
}
+130
View File
@@ -0,0 +1,130 @@
import type { SendblueGetMessageParams, SendblueGetMessageResponse } from '@/tools/sendblue/types'
import {
SENDBLUE_API_BASE_URL,
sendblueBaseParamFields,
sendblueHeaders,
} from '@/tools/sendblue/utils'
import type { ToolConfig } from '@/tools/types'
export const sendblueGetMessageTool: ToolConfig<
SendblueGetMessageParams,
SendblueGetMessageResponse
> = {
id: 'sendblue_get_message',
name: 'Sendblue Get Message',
description: 'Retrieve a single message and its current status by message handle/ID.',
version: '1.0.0',
params: {
...sendblueBaseParamFields,
message_id: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The message handle/ID returned when the message was sent.',
},
},
request: {
url: (params) =>
`${SENDBLUE_API_BASE_URL}/api/v2/messages/${encodeURIComponent(params.message_id.trim())}`,
method: 'GET',
headers: (params) => sendblueHeaders(params),
},
transformResponse: async (response) => {
const body = await response.json()
const data = body?.data ?? body ?? {}
return {
success: true,
output: {
status: data.status ?? null,
message_handle: data.message_handle ?? null,
account_email: data.accountEmail ?? data.account_email ?? null,
content: data.content ?? null,
is_outbound: data.is_outbound ?? null,
from_number: data.from_number ?? null,
number: data.number ?? null,
to_number: data.to_number ?? null,
media_url: data.media_url ?? null,
message_type: data.message_type ?? null,
service: data.service ?? null,
group_id: data.group_id ?? null,
group_display_name: data.group_display_name ?? null,
participants: data.participants ?? [],
send_style: data.send_style ?? null,
was_downgraded: data.was_downgraded ?? null,
opted_out: data.opted_out ?? null,
plan: data.plan ?? null,
sendblue_number: data.sendblue_number ?? null,
seat_id: data.seat_id ?? null,
sender_email: data.sender_email ?? null,
error_code: data.error_code ?? null,
error_message: data.error_message ?? null,
error_reason: data.error_reason ?? null,
error_detail: data.error_detail ?? null,
date_sent: data.date_sent ?? null,
date_updated: data.date_updated ?? null,
},
}
},
outputs: {
status: { type: 'string', description: 'Current message status' },
message_handle: { type: 'string', description: 'Unique message identifier' },
account_email: { type: 'string', description: 'Email of the account', optional: true },
content: { type: 'string', description: 'Message content', optional: true },
is_outbound: {
type: 'boolean',
description: 'Whether the message is outbound',
optional: true,
},
from_number: { type: 'string', description: 'Sending phone number', optional: true },
number: { type: 'string', description: 'Recipient phone number', optional: true },
to_number: { type: 'string', description: 'Destination phone number', optional: true },
media_url: { type: 'string', description: 'URL of attached media', optional: true },
message_type: {
type: 'string',
description: 'Message category: message or group',
optional: true,
},
service: {
type: 'string',
description: 'Messaging service: iMessage, SMS, or RCS',
optional: true,
},
group_id: {
type: 'string',
description: 'Group identifier (empty for non-group)',
optional: true,
},
group_display_name: { type: 'string', description: 'Group chat name', optional: true },
participants: {
type: 'array',
description: 'Participant phone numbers',
items: { type: 'string' },
optional: true,
},
send_style: { type: 'string', description: 'Expressive style applied', optional: true },
was_downgraded: {
type: 'boolean',
description: 'True if the recipient lacks iMessage support',
optional: true,
},
opted_out: {
type: 'boolean',
description: 'True if the recipient has opted out',
optional: true,
},
plan: { type: 'string', description: 'Account plan type', optional: true },
sendblue_number: { type: 'string', description: 'Sendblue phone number used', optional: true },
seat_id: { type: 'string', description: 'Seat UUID', optional: true },
sender_email: { type: 'string', description: 'Email of the sending seat', optional: true },
error_code: { type: 'number', description: 'Numeric error code if failed', optional: true },
error_message: { type: 'string', description: 'Error message if failed', optional: true },
error_reason: { type: 'string', description: 'Additional error context', optional: true },
error_detail: { type: 'string', description: 'Detailed error information', optional: true },
date_sent: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
date_updated: { type: 'string', description: 'ISO 8601 last-update timestamp', optional: true },
},
}
+6
View File
@@ -0,0 +1,6 @@
export { sendblueEvaluateServiceTool } from '@/tools/sendblue/evaluate_service'
export { sendblueGetMessageTool } from '@/tools/sendblue/get_message'
export { sendblueSendGroupMessageTool } from '@/tools/sendblue/send_group_message'
export { sendblueSendMessageTool } from '@/tools/sendblue/send_message'
export { sendblueSendTypingIndicatorTool } from '@/tools/sendblue/send_typing_indicator'
export * from '@/tools/sendblue/types'
@@ -0,0 +1,165 @@
import { filterUndefined } from '@sim/utils/object'
import type {
SendblueSendGroupMessageParams,
SendblueSendGroupMessageResponse,
} from '@/tools/sendblue/types'
import {
SENDBLUE_API_BASE_URL,
sendblueBaseParamFields,
sendblueHeaders,
} from '@/tools/sendblue/utils'
import type { ToolConfig } from '@/tools/types'
export const sendblueSendGroupMessageTool: ToolConfig<
SendblueSendGroupMessageParams,
SendblueSendGroupMessageResponse
> = {
id: 'sendblue_send_group_message',
name: 'Sendblue Send Group Message',
description: 'Send an iMessage or SMS to a group of recipients via Sendblue.',
version: '1.0.0',
params: {
...sendblueBaseParamFields,
numbers: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description:
'Recipient phone numbers in E.164 format (e.g., ["+19998887777", "+13334445555"])',
items: { type: 'string', description: 'Phone number in E.164 format' },
},
from_number: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'One of your registered Sendblue phone numbers to send from, in E.164 format (e.g., +18887776666)',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Message text content. Either content or media_url must be provided.',
},
media_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL of a media file to send. Either content or media_url must be provided.',
},
send_style: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'iMessage expressive style (e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam).',
},
group_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Unique identifier of an existing group to send to. Omit to start a new group.',
},
status_callback: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Webhook URL that Sendblue will POST message status updates to.',
},
},
request: {
url: `${SENDBLUE_API_BASE_URL}/api/send-group-message`,
method: 'POST',
headers: (params) => sendblueHeaders(params),
body: (params) =>
filterUndefined({
numbers: params.numbers,
from_number: params.from_number,
content: params.content,
media_url: params.media_url,
send_style: params.send_style,
group_id: params.group_id,
status_callback: params.status_callback,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
status: data.status ?? null,
message_handle: data.message_handle ?? null,
account_email: data.account_email ?? data.accountEmail ?? null,
content: data.content ?? null,
is_outbound: data.is_outbound ?? null,
from_number: data.from_number ?? null,
number: data.number ?? null,
media_url: data.media_url ?? null,
send_style: data.send_style ?? null,
seat_id: data.seat_id ?? null,
sender_email: data.sender_email ?? null,
error_code: data.error_code ?? null,
error_message: data.error_message ?? null,
date_created: data.date_created ?? null,
date_updated: data.date_updated ?? null,
group_id: data.group_id ?? null,
participants: data.participants ?? [],
},
}
},
outputs: {
status: { type: 'string', description: 'Message status: QUEUED, SENT, DELIVERED, or ERROR' },
message_handle: { type: 'string', description: 'Unique identifier for tracking the message' },
group_id: {
type: 'string',
description: 'Identifier of the group the message was sent to',
optional: true,
},
participants: {
type: 'array',
description: 'Phone numbers participating in the group',
items: { type: 'string' },
},
account_email: { type: 'string', description: 'Email of the account that sent the message' },
content: { type: 'string', description: 'Message content', optional: true },
is_outbound: { type: 'boolean', description: 'Whether this is an outbound message' },
from_number: { type: 'string', description: 'Sending phone number' },
number: { type: 'string', description: 'Recipient phone number', optional: true },
media_url: { type: 'string', description: 'URL of attached media', optional: true },
send_style: {
type: 'string',
description: 'iMessage expressive style applied',
optional: true,
},
seat_id: {
type: 'string',
description: 'UUID of the seat that sent the message',
optional: true,
},
sender_email: {
type: 'string',
description: 'Email of the seat (user) that sent the message',
optional: true,
},
error_code: {
type: 'number',
description: 'Numeric error code if the message failed',
optional: true,
},
error_message: {
type: 'string',
description: 'Error message if the message failed',
optional: true,
},
date_created: { type: 'string', description: 'When the message was created', optional: true },
date_updated: {
type: 'string',
description: 'When the message was last updated',
optional: true,
},
},
}
+141
View File
@@ -0,0 +1,141 @@
import { filterUndefined } from '@sim/utils/object'
import type { SendblueSendMessageParams, SendblueSendMessageResponse } from '@/tools/sendblue/types'
import {
SENDBLUE_API_BASE_URL,
sendblueBaseParamFields,
sendblueHeaders,
} from '@/tools/sendblue/utils'
import type { ToolConfig } from '@/tools/types'
export const sendblueSendMessageTool: ToolConfig<
SendblueSendMessageParams,
SendblueSendMessageResponse
> = {
id: 'sendblue_send_message',
name: 'Sendblue Send Message',
description: 'Send an iMessage or SMS to a single recipient via Sendblue.',
version: '1.0.0',
params: {
...sendblueBaseParamFields,
number: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Recipient phone number in E.164 format (e.g., +19998887777)',
},
from_number: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'One of your registered Sendblue phone numbers to send from, in E.164 format (e.g., +18887776666)',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Message text content. Either content or media_url must be provided.',
},
media_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL of a media file to send. Either content or media_url must be provided.',
},
send_style: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'iMessage expressive style (e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam).',
},
status_callback: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Webhook URL that Sendblue will POST message status updates to.',
},
},
request: {
url: `${SENDBLUE_API_BASE_URL}/api/send-message`,
method: 'POST',
headers: (params) => sendblueHeaders(params),
body: (params) =>
filterUndefined({
number: params.number,
from_number: params.from_number,
content: params.content,
media_url: params.media_url,
send_style: params.send_style,
status_callback: params.status_callback,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
status: data.status ?? null,
message_handle: data.message_handle ?? null,
account_email: data.account_email ?? data.accountEmail ?? null,
content: data.content ?? null,
is_outbound: data.is_outbound ?? null,
from_number: data.from_number ?? null,
number: data.number ?? null,
media_url: data.media_url ?? null,
send_style: data.send_style ?? null,
seat_id: data.seat_id ?? null,
sender_email: data.sender_email ?? null,
error_code: data.error_code ?? null,
error_message: data.error_message ?? null,
date_created: data.date_created ?? null,
date_updated: data.date_updated ?? null,
},
}
},
outputs: {
status: { type: 'string', description: 'Message status: QUEUED, SENT, DELIVERED, or ERROR' },
message_handle: { type: 'string', description: 'Unique identifier for tracking the message' },
account_email: { type: 'string', description: 'Email of the account that sent the message' },
content: { type: 'string', description: 'Message content', optional: true },
is_outbound: { type: 'boolean', description: 'Whether this is an outbound message' },
from_number: { type: 'string', description: 'Sending phone number' },
number: { type: 'string', description: 'Recipient phone number' },
media_url: { type: 'string', description: 'URL of attached media', optional: true },
send_style: {
type: 'string',
description: 'iMessage expressive style applied',
optional: true,
},
seat_id: {
type: 'string',
description: 'UUID of the seat that sent the message',
optional: true,
},
sender_email: {
type: 'string',
description: 'Email of the seat (user) that sent the message',
optional: true,
},
error_code: {
type: 'number',
description: 'Numeric error code if the message failed',
optional: true,
},
error_message: {
type: 'string',
description: 'Error message if the message failed',
optional: true,
},
date_created: { type: 'string', description: 'When the message was created', optional: true },
date_updated: {
type: 'string',
description: 'When the message was last updated',
optional: true,
},
},
}
@@ -0,0 +1,75 @@
import { filterUndefined } from '@sim/utils/object'
import type {
SendblueTypingIndicatorParams,
SendblueTypingIndicatorResponse,
} from '@/tools/sendblue/types'
import {
SENDBLUE_API_BASE_URL,
sendblueBaseParamFields,
sendblueHeaders,
} from '@/tools/sendblue/utils'
import type { ToolConfig } from '@/tools/types'
export const sendblueSendTypingIndicatorTool: ToolConfig<
SendblueTypingIndicatorParams,
SendblueTypingIndicatorResponse
> = {
id: 'sendblue_send_typing_indicator',
name: 'Sendblue Send Typing Indicator',
description: 'Display a typing indicator to a recipient (not supported in group chats).',
version: '1.0.0',
params: {
...sendblueBaseParamFields,
number: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: "Recipient's phone number in E.164 format (e.g., +19998887777)",
},
from_number: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Your Sendblue line number to send from, in E.164 format.',
},
},
request: {
url: `${SENDBLUE_API_BASE_URL}/api/send-typing-indicator`,
method: 'POST',
headers: (params) => sendblueHeaders(params),
body: (params) =>
filterUndefined({
number: params.number,
from_number: params.from_number,
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
status: data.status ?? null,
status_code: data.status_code ?? null,
number: data.number ?? null,
error_message: data.error_message ?? null,
},
}
},
outputs: {
status: {
type: 'string',
description: 'Delivery status of the typing indicator (e.g., QUEUED)',
},
status_code: { type: 'number', description: 'Numeric status code returned by Sendblue' },
number: { type: 'string', description: 'The recipient phone number' },
error_message: {
type: 'string',
description: 'Error details, null on success',
optional: true,
},
},
}
+136
View File
@@ -0,0 +1,136 @@
import type { ToolResponse } from '@/tools/types'
/**
* iMessage expressive styles supported by Sendblue.
*/
export type SendblueSendStyle =
| 'celebration'
| 'shooting_star'
| 'fireworks'
| 'lasers'
| 'love'
| 'confetti'
| 'balloons'
| 'spotlight'
| 'echo'
| 'invisible'
| 'gentle'
| 'loud'
| 'slam'
export interface SendblueBaseParams {
apiKeyId: string
apiSecretKey: string
}
export interface SendblueSendMessageParams extends SendblueBaseParams {
number: string
from_number: string
content?: string
media_url?: string
send_style?: SendblueSendStyle
status_callback?: string
}
export interface SendblueSendGroupMessageParams extends SendblueBaseParams {
numbers: string[]
from_number: string
content?: string
media_url?: string
send_style?: SendblueSendStyle
group_id?: string
status_callback?: string
}
export interface SendblueEvaluateServiceParams extends SendblueBaseParams {
number: string
}
export interface SendblueTypingIndicatorParams extends SendblueBaseParams {
number: string
from_number?: string
}
export interface SendblueGetMessageParams extends SendblueBaseParams {
message_id: string
}
/**
* Shared shape of a Sendblue message resource returned by the send endpoints.
*/
export interface SendblueMessageOutput {
status: string | null
message_handle: string | null
account_email: string | null
content: string | null
is_outbound: boolean | null
from_number: string | null
number: string | null
media_url: string | null
send_style: string | null
seat_id: string | null
sender_email: string | null
error_code: number | null
error_message: string | null
date_created: string | null
date_updated: string | null
}
export interface SendblueSendMessageResponse extends ToolResponse {
output: SendblueMessageOutput
}
export interface SendblueSendGroupMessageResponse extends ToolResponse {
output: SendblueMessageOutput & {
group_id: string | null
participants: string[]
}
}
export interface SendblueEvaluateServiceResponse extends ToolResponse {
output: {
number: string | null
service: string | null
}
}
export interface SendblueTypingIndicatorResponse extends ToolResponse {
output: {
status: string | null
status_code: number | null
number: string | null
error_message: string | null
}
}
export interface SendblueGetMessageResponse extends ToolResponse {
output: {
status: string | null
message_handle: string | null
account_email: string | null
content: string | null
is_outbound: boolean | null
from_number: string | null
number: string | null
to_number: string | null
media_url: string | null
message_type: string | null
service: string | null
group_id: string | null
group_display_name: string | null
participants: string[]
send_style: string | null
was_downgraded: boolean | null
opted_out: boolean | null
plan: string | null
sendblue_number: string | null
seat_id: string | null
sender_email: string | null
error_code: number | null
error_message: string | null
error_reason: string | null
error_detail: string | null
date_sent: string | null
date_updated: string | null
}
}
+38
View File
@@ -0,0 +1,38 @@
import type { SendblueBaseParams } from '@/tools/sendblue/types'
import type { ToolConfig } from '@/tools/types'
/**
* Base URL for the Sendblue API as documented at https://docs.sendblue.com/api-v2/.
*/
export const SENDBLUE_API_BASE_URL = 'https://api.sendblue.com'
/**
* Builds the authentication headers required by every Sendblue endpoint.
* Sendblue authenticates with an API Key ID and API Secret Key sent as
* the `sb-api-key-id` and `sb-api-secret-key` headers.
*/
export function sendblueHeaders(params: SendblueBaseParams): Record<string, string> {
return {
'sb-api-key-id': params.apiKeyId.trim(),
'sb-api-secret-key': params.apiSecretKey.trim(),
'Content-Type': 'application/json',
}
}
/**
* Shared credential param definitions reused across all Sendblue tools.
*/
export const sendblueBaseParamFields = {
apiKeyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Sendblue API Key ID (sb-api-key-id)',
},
apiSecretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Sendblue API Secret Key (sb-api-secret-key)',
},
} satisfies ToolConfig['params']
+6
View File
@@ -290,6 +290,10 @@ import {
salesforceRecordUpdatedTrigger,
salesforceWebhookTrigger,
} from '@/triggers/salesforce'
import {
sendblueMessageReceivedTrigger,
sendblueMessageStatusUpdatedTrigger,
} from '@/triggers/sendblue'
import {
servicenowChangeRequestCreatedTrigger,
servicenowChangeRequestUpdatedTrigger,
@@ -553,6 +557,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
salesforce_opportunity_stage_changed: salesforceOpportunityStageChangedTrigger,
salesforce_case_status_changed: salesforceCaseStatusChangedTrigger,
salesforce_webhook: salesforceWebhookTrigger,
sendblue_message_received: sendblueMessageReceivedTrigger,
sendblue_message_status_updated: sendblueMessageStatusUpdatedTrigger,
servicenow_incident_created: servicenowIncidentCreatedTrigger,
servicenow_incident_updated: servicenowIncidentUpdatedTrigger,
servicenow_change_request_created: servicenowChangeRequestCreatedTrigger,
+2
View File
@@ -0,0 +1,2 @@
export { sendblueMessageReceivedTrigger } from '@/triggers/sendblue/message_received'
export { sendblueMessageStatusUpdatedTrigger } from '@/triggers/sendblue/message_status_updated'
@@ -0,0 +1,25 @@
import { SendblueIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSendblueOutputs,
sendblueSetupInstructions,
sendblueTriggerOptions,
} from '@/triggers/sendblue/utils'
import type { TriggerConfig } from '@/triggers/types'
export const sendblueMessageReceivedTrigger: TriggerConfig = {
id: 'sendblue_message_received',
name: 'Sendblue Message Received',
provider: 'sendblue',
description: 'Trigger when an inbound iMessage or SMS is received in Sendblue',
version: '1.0.0',
icon: SendblueIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'sendblue_message_received',
triggerOptions: sendblueTriggerOptions,
includeDropdown: true,
setupInstructions: sendblueSetupInstructions('Message Received'),
}),
outputs: buildSendblueOutputs(),
webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } },
}
@@ -0,0 +1,25 @@
import { SendblueIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSendblueOutputs,
sendblueSetupInstructions,
sendblueTriggerOptions,
} from '@/triggers/sendblue/utils'
import type { TriggerConfig } from '@/triggers/types'
export const sendblueMessageStatusUpdatedTrigger: TriggerConfig = {
id: 'sendblue_message_status_updated',
name: 'Sendblue Message Status Updated',
provider: 'sendblue',
description:
'Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue',
version: '1.0.0',
icon: SendblueIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'sendblue_message_status_updated',
triggerOptions: sendblueTriggerOptions,
setupInstructions: sendblueSetupInstructions('Message Status Updated'),
}),
outputs: buildSendblueOutputs(),
webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } },
}
+76
View File
@@ -0,0 +1,76 @@
import type { TriggerOutput } from '@/triggers/types'
export const sendblueTriggerOptions = [
{ label: 'Message Received', id: 'sendblue_message_received' },
{ label: 'Message Status Updated', id: 'sendblue_message_status_updated' },
]
export function sendblueSetupInstructions(eventType: string): string {
const instructions = [
'Copy the <strong>Webhook URL</strong> above.',
'Open your <a href="https://dashboard.sendblue.com" target="_blank" rel="noopener noreferrer">Sendblue dashboard</a> and go to <strong>Settings &gt; Webhooks</strong>.',
eventType === 'Message Received'
? 'Paste the Webhook URL into the <strong>Receive Webhook</strong> field to receive inbound messages.'
: 'Paste the Webhook URL into the <strong>Send / Status Webhook</strong> field to receive outbound message status updates. You can also pass it per-message as the <code>status_callback</code> parameter.',
'Save your webhook settings in Sendblue.',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Outputs shared by both Sendblue message webhooks. Inbound and outbound
* status callbacks use the same payload schema.
*/
export function buildSendblueOutputs(): Record<string, TriggerOutput> {
return {
account_email: { type: 'string', description: 'Email of the Sendblue account' },
content: { type: 'string', description: 'Message text content' },
media_url: { type: 'string', description: 'CDN link to attached media, if any' },
is_outbound: { type: 'boolean', description: 'True for outbound messages, false for inbound' },
status: {
type: 'string',
description: 'Message status (e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR)',
},
error_code: { type: 'number', description: 'Error identifier, null if none' },
error_message: { type: 'string', description: 'Descriptive error text, null if none' },
error_reason: { type: 'string', description: 'Additional error context, null if none' },
error_detail: { type: 'string', description: 'Detailed error information, null if none' },
message_handle: {
type: 'string',
description: 'Sendblue message identifier (use to deduplicate)',
},
date_sent: { type: 'string', description: 'ISO 8601 creation timestamp' },
date_updated: { type: 'string', description: 'ISO 8601 last-update timestamp' },
from_number: { type: 'string', description: 'E.164 sender phone number' },
number: { type: 'string', description: 'E.164 recipient/counterparty phone number' },
to_number: { type: 'string', description: 'E.164 destination phone number' },
was_downgraded: {
type: 'boolean',
description: 'True if the recipient lacks iMessage support',
},
plan: { type: 'string', description: 'Account plan type' },
message_type: { type: 'string', description: 'Message category (e.g., message, group)' },
group_id: { type: 'string', description: 'Group identifier, empty for non-group messages' },
participants: { type: 'array', description: 'Participant phone numbers for group messages' },
send_style: { type: 'string', description: 'Expressive style if applied' },
opted_out: { type: 'boolean', description: 'True if the recipient has opted out' },
sendblue_number: { type: 'string', description: 'Sendblue phone number used' },
service: { type: 'string', description: 'Messaging service (iMessage or SMS)' },
group_display_name: {
type: 'string',
description: 'Group chat name, null for non-group messages',
},
sender_email: { type: 'string', description: 'Email of the user who sent the message' },
seat_id: { type: 'string', description: 'Seat UUID, null if absent' },
raw: {
type: 'string',
description: 'Complete raw webhook payload from Sendblue as a JSON string',
},
}
}
+7
View File
@@ -535,6 +535,10 @@ async function buildToolDescriptionMap(): Promise<ToolMaps> {
* 'api-key' if it uses a plain API key field, or 'none' otherwise.
*/
function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' {
// Prefer the authoritative `authMode` declaration when present.
if (/authMode\s*:\s*AuthMode\.OAuth\b/.test(blockContent)) return 'oauth'
if (/authMode\s*:\s*AuthMode\.(?:ApiKey|BotToken)\b/.test(blockContent)) return 'api-key'
// Fall back to credential subBlock heuristics for blocks without authMode.
if (/type\s*:\s*['"]oauth-input['"]/.test(blockContent)) return 'oauth'
if (/\bid\s*:\s*['"](?:apiKey|api_key|accessToken)['"]/.test(blockContent)) return 'api-key'
return 'none'
@@ -1775,6 +1779,9 @@ function extractToolInfo(
if (endPos !== -1) {
const paramBlock = paramsContent.substring(startPos + 1, endPos - 1).trim()
paramPositions.push({ name: paramName, start: startPos, content: paramBlock })
// Resume scanning after this param's block so nested descriptors
// (e.g. an array param's `items: {...}`) are not parsed as params.
paramBlocksRegex.lastIndex = endPos
}
}