first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,75 @@
import type {
IExecuteFunctions,
IHookFunctions,
IDataObject,
IHttpRequestMethods,
IRequestOptions,
IHttpRequestOptions,
ILoadOptionsFunctions,
} from 'n8n-workflow';
/**
* Make an API request to Twilio
*
*/
export async function twilioApiRequest(
this: IHookFunctions | IExecuteFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject,
query?: IDataObject,
): Promise<any> {
const credentials = await this.getCredentials<{
accountSid: string;
authType: 'authToken' | 'apiKey';
authToken: string;
apiKeySid: string;
apiKeySecret: string;
}>('twilioApi');
if (query === undefined) {
query = {};
}
const options: IRequestOptions = {
method,
form: body,
qs: query,
uri: `https://api.twilio.com/2010-04-01/Accounts/${credentials.accountSid}${endpoint}`,
json: true,
};
return await this.helpers.requestWithAuthentication.call(this, 'twilioApi', options);
}
export async function twilioTriggerApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: FormData | IDataObject = {},
): Promise<any> {
const options: IHttpRequestOptions = {
method,
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
url: `https://events.twilio.com/v1/${endpoint}`,
json: true,
};
return await this.helpers.requestWithAuthentication.call(this, 'twilioApi', options);
}
const XML_CHAR_MAP: { [key: string]: string } = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
};
export function escapeXml(str: string) {
return str.replace(/[<>&"']/g, (ch: string) => {
return XML_CHAR_MAP[ch];
});
}
@@ -0,0 +1,61 @@
{
"node": "n8n-nodes-base.twilio",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/twilio/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twilio/"
}
],
"generic": [
{
"label": "Love at first sight: Ricardos n8n journey",
"icon": "❤️",
"url": "https://n8n.io/blog/love-at-first-sight-ricardos-n8n-journey/"
},
{
"label": "Database Monitoring and Alerting with n8n",
"icon": "📡",
"url": "https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/"
},
{
"label": "Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n",
"icon": "🧾",
"url": "https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/"
},
{
"label": "Tracking Time Spent in Meetings With Google Calendar, Twilio, and n8n",
"icon": "🗓",
"url": "https://n8n.io/blog/tracking-time-spent-in-meetings-with-google-calendar-twilio-and-n8n/"
},
{
"label": "Creating Error Workflows in n8n",
"icon": "🌪",
"url": "https://n8n.io/blog/creating-error-workflows-in-n8n/"
},
{
"label": "Sending Automated Congratulations with Google Sheets, Twilio, and n8n ",
"icon": "🙌",
"url": "https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/"
},
{
"label": "Learn to Build Powerful API Endpoints Using Webhooks",
"icon": "🧰",
"url": "https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/"
},
{
"label": "Sending SMS the Low-Code Way with Airtable, Twilio Programmable SMS, and n8n",
"icon": "📱",
"url": "https://n8n.io/blog/sending-sms-the-low-code-way-with-airtable-twilio-programmable-sms-and-n8n/"
}
]
},
"alias": ["SMS", "Phone", "Voice"]
}
@@ -0,0 +1,305 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { escapeXml, twilioApiRequest } from './GenericFunctions';
export class Twilio implements INodeType {
description: INodeTypeDescription = {
displayName: 'Twilio',
name: 'twilio',
icon: 'file:twilio.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Send SMS and WhatsApp messages or make phone calls',
defaults: {
name: 'Twilio',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'twilioApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Call',
value: 'call',
},
{
name: 'SMS',
value: 'sms',
},
],
default: 'sms',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['sms'],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send SMS/MMS/WhatsApp message',
action: 'Send an SMS/MMS/WhatsApp message',
},
],
default: 'send',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['call'],
},
},
options: [
{
name: 'Make',
value: 'make',
action: 'Make a call',
},
],
default: 'make',
},
// ----------------------------------
// sms / call
// ----------------------------------
// ----------------------------------
// sms:send / call:make
// ----------------------------------
{
displayName: 'From',
name: 'from',
type: 'string',
default: '',
placeholder: '+14155238886',
required: true,
displayOptions: {
show: {
operation: ['send', 'make'],
resource: ['sms', 'call'],
},
},
description: 'The number from which to send the message',
},
{
displayName: 'To',
name: 'to',
type: 'string',
default: '',
placeholder: '+14155238886',
required: true,
displayOptions: {
show: {
operation: ['send', 'make'],
resource: ['sms', 'call'],
},
},
description: 'The number to which to send the message',
},
{
displayName: 'To Whatsapp',
name: 'toWhatsapp',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: ['send'],
resource: ['sms'],
},
},
description: 'Whether the message should be sent to WhatsApp',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ['send'],
resource: ['sms'],
},
},
description: 'The message to send',
},
{
displayName: 'Use TwiML',
name: 'twiml',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: ['make'],
resource: ['call'],
},
},
description:
'Whether to use the <a href="https://www.twilio.com/docs/voice/twiml">Twilio Markup Language</a> in the message',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ['make'],
resource: ['call'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Status Callback',
name: 'statusCallback',
type: 'string',
default: '',
description:
'Status Callbacks allow you to receive events related to the REST resources managed by Twilio: Rooms, Recordings and Compositions',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let operation: string;
let resource: string;
// For Post
let body: IDataObject;
// For Query string
let qs: IDataObject;
let requestMethod: IHttpRequestMethods;
let endpoint: string;
for (let i = 0; i < items.length; i++) {
try {
requestMethod = 'GET';
endpoint = '';
body = {};
qs = {};
resource = this.getNodeParameter('resource', i);
operation = this.getNodeParameter('operation', i);
if (resource === 'sms') {
if (operation === 'send') {
// ----------------------------------
// sms:send
// ----------------------------------
requestMethod = 'POST';
endpoint = '/Messages.json';
body.From = this.getNodeParameter('from', i) as string;
body.To = this.getNodeParameter('to', i) as string;
body.Body = this.getNodeParameter('message', i) as string;
body.StatusCallback = this.getNodeParameter('options.statusCallback', i, '') as string;
const toWhatsapp = this.getNodeParameter('toWhatsapp', i) as boolean;
if (toWhatsapp) {
body.From = `whatsapp:${body.From}`;
body.To = `whatsapp:${body.To}`;
}
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not known!`,
{ itemIndex: i },
);
}
} else if (resource === 'call') {
if (operation === 'make') {
// ----------------------------------
// call:make
// ----------------------------------
requestMethod = 'POST';
endpoint = '/Calls.json';
const message = this.getNodeParameter('message', i) as string;
const useTwiml = this.getNodeParameter('twiml', i) as boolean;
body.From = this.getNodeParameter('from', i) as string;
body.To = this.getNodeParameter('to', i) as string;
if (useTwiml) {
body.Twiml = message;
} else {
body.Twiml = `<Response><Say>${escapeXml(message)}</Say></Response>`;
}
body.StatusCallback = this.getNodeParameter('options.statusCallback', i, '') as string;
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not known!`,
{ itemIndex: i },
);
}
} else {
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known!`, {
itemIndex: i,
});
}
const responseData = await twilioApiRequest.call(this, requestMethod, endpoint, body, qs);
returnData.push(responseData as IDataObject);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
@@ -0,0 +1,19 @@
{
"node": "n8n-nodes-base.twilioTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/twilio/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.twiliotrigger/"
}
]
},
"alias": ["SMS", "Phone", "Voice"]
}
@@ -0,0 +1,197 @@
import {
type IHookFunctions,
type IWebhookFunctions,
type INodeType,
type INodeTypeDescription,
type IWebhookResponseData,
NodeConnectionTypes,
} from 'n8n-workflow';
import { twilioTriggerApiRequest } from './GenericFunctions';
export class TwilioTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Twilio Trigger',
name: 'twilioTrigger',
icon: 'file:twilio.svg',
group: ['trigger'],
version: [1],
defaultVersion: 1,
subtitle: '=Updates: {{$parameter["updates"].join(", ")}}',
description: 'Starts the workflow on a Twilio update',
defaults: {
name: 'Twilio Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'twilioApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Trigger On',
name: 'updates',
type: 'multiOptions',
options: [
{
name: 'New SMS',
value: 'com.twilio.messaging.inbound-message.received',
description: 'When an SMS message is received',
},
{
name: 'New Call',
value: 'com.twilio.voice.insights.call-summary.complete',
description: 'When a call is received',
},
],
required: true,
default: [],
},
{
displayName: "The 'New Call' event may take up to thirty minutes to be triggered",
name: 'callTriggerNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
updates: ['com.twilio.voice.insights.call-summary.complete'],
},
},
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const { sinks } = (await twilioTriggerApiRequest.call(this, 'GET', 'Sinks')) || {};
const sink = sinks.find(
(entry: { sink_configuration: { destination: string | undefined } }) =>
entry.sink_configuration.destination === webhookUrl,
);
if (sink) {
const { subscriptions } =
(await twilioTriggerApiRequest.call(this, 'GET', 'Subscriptions')) || {};
const subscription = subscriptions.find(
(entry: { sink_sid: any }) => entry.sink_sid === sink.sid,
);
if (subscription) {
const { types } =
(await twilioTriggerApiRequest.call(
this,
'GET',
`Subscriptions/${subscription.sid}/SubscribedEvents`,
)) || {};
const typesFound = types.map((type: { type: any }) => type.type);
const allowedUpdates = this.getNodeParameter('updates') as string[];
if (typesFound.sort().join(',') === allowedUpdates.sort().join(',')) {
return true;
} else {
return false;
}
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const workflowData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const allowedUpdates = this.getNodeParameter('updates') as string[];
const bodySink = {
Description: 'Sink created by n8n Twilio Trigger Node.',
SinkConfiguration: `{ "destination": "${webhookUrl}", "method": "POST" }`,
SinkType: 'webhook',
};
const sink = await twilioTriggerApiRequest.call(this, 'POST', 'Sinks', bodySink);
workflowData.sinkId = sink.sid;
const body = {
Description: 'Subscription created by n8n Twilio Trigger Node.',
Types: `{ "type": "${allowedUpdates[0]}" }`,
SinkSid: sink.sid,
};
const subscription = await twilioTriggerApiRequest.call(
this,
'POST',
'Subscriptions',
body,
);
workflowData.subscriptionId = subscription.sid;
// if there is more than one event type add the others on the existing subscription
if (allowedUpdates.length > 1) {
for (let index = 1; index < allowedUpdates.length; index++) {
await twilioTriggerApiRequest.call(
this,
'POST',
`Subscriptions/${workflowData.subscriptionId}/SubscribedEvents`,
{
Type: allowedUpdates[index],
},
);
}
}
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const workflowData = this.getWorkflowStaticData('node');
const sinkId = workflowData.sinkId;
const subscriptionId = workflowData.subscriptionId;
try {
if (sinkId) {
await twilioTriggerApiRequest.call(this, 'DELETE', `Sinks/${sinkId}`, {});
workflowData.sinkId = '';
}
if (subscriptionId) {
await twilioTriggerApiRequest.call(
this,
'DELETE',
`Subscriptions/${subscriptionId}`,
{},
);
workflowData.subscriptionId = '';
}
} catch (error) {
return false;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
return {
workflowData: [this.helpers.returnJsonArray(bodyData)],
};
}
}
@@ -0,0 +1,113 @@
{
"type": "object",
"properties": {
"account_sid": {
"type": "string"
},
"annotation": {
"type": "null"
},
"answered_by": {
"type": "null"
},
"api_version": {
"type": "string"
},
"caller_name": {
"type": "null"
},
"date_created": {
"type": "null"
},
"date_updated": {
"type": "null"
},
"direction": {
"type": "string"
},
"duration": {
"type": "null"
},
"end_time": {
"type": "null"
},
"forwarded_from": {
"type": "null"
},
"from": {
"type": "string"
},
"from_formatted": {
"type": "string"
},
"group_sid": {
"type": "null"
},
"parent_call_sid": {
"type": "null"
},
"price": {
"type": "null"
},
"price_unit": {
"type": "string"
},
"queue_time": {
"type": "string"
},
"sid": {
"type": "string"
},
"start_time": {
"type": "null"
},
"status": {
"type": "string"
},
"subresource_uris": {
"type": "object",
"properties": {
"events": {
"type": "string"
},
"notifications": {
"type": "string"
},
"payments": {
"type": "string"
},
"recordings": {
"type": "string"
},
"siprec": {
"type": "string"
},
"streams": {
"type": "string"
},
"transcriptions": {
"type": "string"
},
"user_defined_message_subscriptions": {
"type": "string"
},
"user_defined_messages": {
"type": "string"
}
}
},
"to": {
"type": "string"
},
"to_formatted": {
"type": "string"
},
"trunk_sid": {
"type": "null"
},
"uri": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,68 @@
{
"type": "object",
"properties": {
"account_sid": {
"type": "string"
},
"api_version": {
"type": "string"
},
"body": {
"type": "string"
},
"date_created": {
"type": "string"
},
"date_sent": {
"type": "null"
},
"date_updated": {
"type": "string"
},
"direction": {
"type": "string"
},
"error_code": {
"type": "null"
},
"error_message": {
"type": "null"
},
"from": {
"type": "string"
},
"messaging_service_sid": {
"type": "string"
},
"num_media": {
"type": "string"
},
"num_segments": {
"type": "string"
},
"price": {
"type": "null"
},
"sid": {
"type": "string"
},
"status": {
"type": "string"
},
"subresource_uris": {
"type": "object",
"properties": {
"media": {
"type": "string"
}
}
},
"to": {
"type": "string"
},
"uri": {
"type": "string"
}
},
"version": 5
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 65 65"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><g fill="#e31e26" fill-rule="nonzero" stroke="none"><path d="M31.953 0C14.337 0 0 14.337 0 31.953s14.337 31.953 31.953 31.953C49.663 64 64 49.663 64 31.953S49.663 0 31.953 0m0 55.567c-12.931 0-23.52-10.589-23.52-23.614 0-12.931 10.589-23.52 23.52-23.52 13.025 0 23.614 10.589 23.614 23.52 0 13.025-10.589 23.614-23.614 23.614"/><use xlink:href="#b"/><use xlink:href="#b" y="15.93"/><path d="M17.335 39.918a6.64 6.64 0 0 1 6.653-6.653 6.653 6.653 0 1 1 0 13.306 6.64 6.64 0 0 1-6.653-6.653m0-15.93a6.64 6.64 0 0 1 6.653-6.653 6.64 6.64 0 0 1 6.653 6.653 6.64 6.64 0 0 1-6.653 6.653 6.64 6.64 0 0 1-6.653-6.653"/></g></symbol><defs><path id="b" d="M33.265 23.988a6.64 6.64 0 1 1 13.306 0 6.64 6.64 0 1 1-13.306 0"/></defs></svg>

After

Width:  |  Height:  |  Size: 992 B