{ "kind": "open-flow-workflow", "version": 1, "name": "Airtable SMS Broadcast", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_4468e6fea9a61aaa": { "name": "Send contact messages", "kind": "start", "inputDefs": [] }, "n_51c4ee72d90aaf6e": { "inputs": { "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_51c4ee72d90aaf6e", "output": "state" } ] }, "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_45db4f3e02824ca3", "output": "records" } ] }, "offset": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_45db4f3e02824ca3", "output": "offset" } ] } }, "name": "Collect Airtable contacts", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": true, "handle": "previousState" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": true, "handle": "records" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "Pagination cursor for the next page of records, or null when unavailable." }, "nullable": true, "handle": "offset" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": false, "handle": "records" }, { "jsonSchema": { "type": "string", "minLength": 1, "description": "Opaque pagination cursor returned by a previous Airtable response." }, "nullable": true, "handle": "offset" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" } ], "name": "Collect Airtable contacts", "moduleId": "n_52805d0c1249ea0e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_b04d05ebcba50252": { "inputs": {}, "name": "More Airtable contact pages", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_51c4ee72d90aaf6e", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_45db4f3e02824ca3": { "inputs": { "baseId": { "kind": "value", "value": "YOUR_AIRTABLE_BASE_ID" }, "tableIdOrName": { "kind": "value", "value": "YOUR_AIRTABLE_TABLE_ID" }, "offset": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_51c4ee72d90aaf6e", "output": "offset" } ] } }, "name": "Read one Airtable contact page", "kind": "task", "task": { "inputs": [ { "description": "Base ID in the format appXXXXXXXXXXXXXX.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Base ID in the format appXXXXXXXXXXXXXX." }, "nullable": false, "handle": "baseId" }, { "description": "Table ID or table name accepted by the Airtable path parameter.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Table ID or table name accepted by the Airtable path parameter." }, "nullable": false, "handle": "tableIdOrName" }, { "description": "View name or view ID used by Airtable to filter and sort results.", "jsonSchema": { "type": "string", "minLength": 1, "description": "View name or view ID used by Airtable to filter and sort results." }, "nullable": true, "handle": "view", "value": null }, { "description": "Field names or field IDs to include in the Airtable response.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "An array item." }, "minItems": 1, "description": "Field names or field IDs to include in the Airtable response." }, "nullable": true, "handle": "fields", "value": null }, { "description": "Sort rules applied by Airtable in order.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "field": { "type": "string", "minLength": 1, "description": "Field name or field ID used by Airtable for sorting." }, "direction": { "type": "string", "enum": [ "asc", "desc" ], "description": "Sort direction accepted by Airtable." } }, "required": [ "field" ], "additionalProperties": false, "description": "Sort rule accepted by the Airtable list records endpoint." }, "minItems": 1, "description": "Sort rules applied by Airtable in order." }, "nullable": true, "handle": "sort", "value": null }, { "description": "Formula string evaluated by Airtable to filter matching records.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Formula string evaluated by Airtable to filter matching records." }, "nullable": true, "handle": "filterByFormula", "value": null }, { "description": "Maximum total number of records to return before Airtable stops pagination.", "jsonSchema": { "type": "integer", "minimum": 1, "description": "Maximum total number of records to return before Airtable stops pagination." }, "nullable": true, "handle": "maxRecords", "value": null }, { "description": "Number of records to return per page.", "jsonSchema": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Number of records to return per page." }, "nullable": true, "handle": "pageSize", "value": null }, { "description": "Opaque pagination cursor returned by a previous Airtable response.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Opaque pagination cursor returned by a previous Airtable response." }, "nullable": true, "handle": "offset", "value": null }, { "description": "Cell format accepted by Airtable for read operations.", "jsonSchema": { "type": "string", "enum": [ "json", "string" ], "description": "Cell format accepted by Airtable for read operations." }, "nullable": true, "handle": "cellFormat", "value": null }, { "description": "Timezone string sent to Airtable when cellFormat is string.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Timezone string sent to Airtable when cellFormat is string." }, "nullable": true, "handle": "timeZone", "value": null }, { "description": "User locale sent to Airtable when cellFormat is string.", "jsonSchema": { "type": "string", "minLength": 1, "description": "User locale sent to Airtable when cellFormat is string." }, "nullable": true, "handle": "userLocale", "value": null }, { "description": "Whether Airtable should return field IDs instead of field names in record objects.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should return field IDs instead of field names in record objects." }, "nullable": true, "handle": "returnFieldsByFieldId", "value": null }, { "description": "Whether Airtable should return date dependency metadata for linked record fields.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should return date dependency metadata for linked record fields." }, "nullable": true, "handle": "includeDateDependencyMetadata", "value": null }, { "description": "Record metadata fields to include in Airtable list records responses.", "jsonSchema": { "type": "array", "items": { "const": "commentCount", "description": "An array item." }, "minItems": 1, "description": "Record metadata fields to include in Airtable list records responses." }, "nullable": true, "handle": "recordMetadata", "value": null } ], "outputs": [ { "description": "Records returned by Airtable.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": false, "handle": "records" }, { "description": "Pagination cursor for the next page of records, or null when unavailable.", "jsonSchema": { "type": [ "string", "null" ], "description": "Pagination cursor for the next page of records, or null when unavailable." }, "nullable": true, "handle": "offset" } ], "name": "Read one Airtable contact page", "executor": { "kind": "connector", "action": "airtable.list_records" } } }, "n_5ec91b910be33c44": { "inputs": { "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_51c4ee72d90aaf6e", "output": "records" } ] } }, "name": "Prepare and validate messages", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": false, "handle": "records" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "to": { "type": "string" }, "body": { "type": "string" } }, "required": [ "to", "body" ], "additionalProperties": false } }, "nullable": false, "handle": "messages" } ], "name": "Prepare and validate messages", "moduleId": "n_2a23d1cb4b1ebc4c", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_2774411df0634f9e": { "inputs": { "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "state" } ] }, "messages": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5ec91b910be33c44", "output": "messages" } ] }, "from": { "kind": "value", "value": "YOUR_TWILIO_NUMBER" }, "lastmessageSid": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "messageSid" } ] }, "lastaccountSid": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "accountSid" } ] }, "laststatus": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "status" } ] }, "lastto": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "to" } ] }, "lastfrom": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "from" } ] }, "lastbody": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_53c70b18e988773d", "output": "body" } ] } }, "name": "Collect SMS submissions", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object" }, "nullable": true, "handle": "previousState" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "to": { "type": "string" }, "body": { "type": "string" } }, "required": [ "to", "body" ], "additionalProperties": false } }, "nullable": false, "handle": "messages" }, { "jsonSchema": { "type": "string", "description": "The Twilio phone number sending the message." }, "nullable": false, "handle": "from" }, { "jsonSchema": { "type": "string", "description": "The Twilio message SID." }, "nullable": true, "handle": "lastmessageSid" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "The Twilio account SID that owns the message." }, "nullable": true, "handle": "lastaccountSid" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "The delivery status of the message." }, "nullable": true, "handle": "laststatus" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "The destination phone number." }, "nullable": true, "handle": "lastto" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "The sender phone number." }, "nullable": true, "handle": "lastfrom" }, { "jsonSchema": { "type": [ "string", "null" ], "description": "The text body of the message." }, "nullable": true, "handle": "lastbody" } ], "outputs": [ { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "messageSid": { "type": "string", "description": "The Twilio message SID." }, "accountSid": { "anyOf": [ { "type": [ "string", "null" ], "description": "The Twilio account SID that owns the message." }, { "type": "null" } ] }, "status": { "anyOf": [ { "type": [ "string", "null" ], "description": "The delivery status of the message." }, { "type": "null" } ] }, "to": { "anyOf": [ { "type": [ "string", "null" ], "description": "The destination phone number." }, { "type": "null" } ] }, "from": { "anyOf": [ { "type": [ "string", "null" ], "description": "The sender phone number." }, { "type": "null" } ] }, "body": { "anyOf": [ { "type": [ "string", "null" ], "description": "The text body of the message." }, { "type": "null" } ] } }, "required": [ "messageSid" ], "additionalProperties": false } }, "nullable": false, "handle": "sent" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "string", "description": "The destination phone number in E.164 format." }, "nullable": false, "handle": "to" }, { "jsonSchema": { "type": "string", "description": "The text body of the outbound message." }, "nullable": false, "handle": "body" }, { "jsonSchema": { "type": "string", "description": "The Twilio phone number sending the message." }, "nullable": false, "handle": "from" } ], "name": "Collect SMS submissions", "moduleId": "n_5269ba45db54b93c", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_851f1e8421d87e61": { "inputs": {}, "name": "More SMS messages", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_53c70b18e988773d": { "inputs": { "from": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "from" } ] }, "to": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "to" } ] }, "body": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "body" } ] } }, "name": "Send one SMS", "kind": "task", "task": { "inputs": [ { "description": "The destination phone number in E.164 format.", "jsonSchema": { "type": "string", "description": "The destination phone number in E.164 format." }, "nullable": false, "handle": "to" }, { "description": "The Twilio phone number sending the message.", "jsonSchema": { "type": "string", "description": "The Twilio phone number sending the message." }, "nullable": false, "handle": "from" }, { "description": "The text body of the outbound message.", "jsonSchema": { "type": "string", "description": "The text body of the outbound message." }, "nullable": false, "handle": "body" } ], "outputs": [ { "description": "The Twilio message SID.", "jsonSchema": { "type": "string", "description": "The Twilio message SID." }, "nullable": false, "handle": "messageSid" }, { "description": "The Twilio account SID that owns the message.", "jsonSchema": { "type": [ "string", "null" ], "description": "The Twilio account SID that owns the message." }, "nullable": true, "handle": "accountSid" }, { "description": "The delivery status of the message.", "jsonSchema": { "type": [ "string", "null" ], "description": "The delivery status of the message." }, "nullable": true, "handle": "status" }, { "description": "The destination phone number.", "jsonSchema": { "type": [ "string", "null" ], "description": "The destination phone number." }, "nullable": true, "handle": "to" }, { "description": "The sender phone number.", "jsonSchema": { "type": [ "string", "null" ], "description": "The sender phone number." }, "nullable": true, "handle": "from" }, { "description": "The text body of the message.", "jsonSchema": { "type": [ "string", "null" ], "description": "The text body of the message." }, "nullable": true, "handle": "body" } ], "name": "Send one SMS", "executor": { "kind": "connector", "action": "twilio.send_message" } } }, "n_dbf7d878ef1688b0": { "name": "Submitted messages", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "messageSid": { "type": "string", "description": "The Twilio message SID." }, "accountSid": { "anyOf": [ { "type": [ "string", "null" ], "description": "The Twilio account SID that owns the message." }, { "type": "null" } ] }, "status": { "anyOf": [ { "type": [ "string", "null" ], "description": "The delivery status of the message." }, { "type": "null" } ] }, "to": { "anyOf": [ { "type": [ "string", "null" ], "description": "The destination phone number." }, { "type": "null" } ] }, "from": { "anyOf": [ { "type": [ "string", "null" ], "description": "The sender phone number." }, { "type": "null" } ] }, "body": { "anyOf": [ { "type": [ "string", "null" ], "description": "The text body of the message." }, { "type": "null" } ] } }, "required": [ "messageSid" ], "additionalProperties": false } }, "nullable": false, "handle": "sent" }, { "jsonSchema": { "type": "integer", "minimum": 0 }, "nullable": false, "handle": "count" } ], "outputMappings": { "sent": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "sent" } ] }, "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2774411df0634f9e", "output": "count" } ] } } } }, "edges": [ { "source": "n_4468e6fea9a61aaa", "target": "n_51c4ee72d90aaf6e" }, { "source": "n_51c4ee72d90aaf6e", "target": "n_b04d05ebcba50252" }, { "source": "n_b04d05ebcba50252", "target": "n_45db4f3e02824ca3", "sourceHandle": "more" }, { "source": "n_45db4f3e02824ca3", "target": "n_51c4ee72d90aaf6e" }, { "source": "n_b04d05ebcba50252", "target": "n_5ec91b910be33c44", "sourceHandle": "otherwise" }, { "source": "n_5ec91b910be33c44", "target": "n_2774411df0634f9e" }, { "source": "n_2774411df0634f9e", "target": "n_851f1e8421d87e61" }, { "source": "n_851f1e8421d87e61", "target": "n_53c70b18e988773d", "sourceHandle": "more" }, { "source": "n_53c70b18e988773d", "target": "n_2774411df0634f9e" }, { "source": "n_851f1e8421d87e61", "target": "n_dbf7d878ef1688b0", "sourceHandle": "otherwise" } ] } }, "modules": { "n_52805d0c1249ea0e": { "name": "Collect Airtable contacts", "imports": [], "source": "export default function collectSmsContactPage({ previousState, records, offset }) {\n if (previousState === null) {\n if (records !== null || offset !== null) throw new Error('Unexpected Airtable page before collection starts');\n return { state: { records: [], seenOffsets: [] }, records: [], offset: null, more: true };\n }\n if (!previousState || !Array.isArray(previousState.records) || !Array.isArray(previousState.seenOffsets) || !Array.isArray(records)) throw new Error('Invalid Airtable contact collection state');\n if (offset !== null && (typeof offset !== 'string' || !offset || previousState.seenOffsets.includes(offset))) throw new Error('Airtable returned an invalid or repeated pagination cursor');\n const state = { records: [...previousState.records, ...records], seenOffsets: offset === null ? [...previousState.seenOffsets] : [...previousState.seenOffsets, offset] };\n return { state, records: state.records, offset, more: offset !== null };\n}\n" }, "n_2a23d1cb4b1ebc4c": { "name": "Prepare and validate messages", "imports": [], "source": "export default function prepareSms({ records }) {\n const messages = records.map((record) => {\n const to = String(record.fields?.Number ?? '').trim();\n if (!/^\\+[1-9]\\d{1,14}$/.test(to)) throw new Error('Each Airtable Number must be an E.164 phone number');\n const name = String(record.fields?.Name ?? '');\n return { to, body: `Hello, ${name}!\\nSending this SMS from n8n!` };\n });\n return { messages };\n}\n" }, "n_5269ba45db54b93c": { "name": "Collect SMS submissions", "imports": [], "source": "const receiptFields = [\"messageSid\",\"accountSid\",\"status\",\"to\",\"from\",\"body\"];\nexport default function collectSmsSubmission(input) {\n const { previousState, messages, from } = input;\n if (!Array.isArray(messages)) throw new Error('Expected the validated message collection');\n let state;\n if (previousState === null) {\n if (receiptFields.some((field) => input['last' + field] !== null)) throw new Error('Unexpected SMS result before sending starts');\n if (messages.length && !/^\\+[1-9]\\d{1,14}$/.test(from)) throw new Error('Configure the Twilio sender as an E.164 phone number');\n state = { index: 0, sent: [] };\n } else {\n if (!previousState || !Number.isSafeInteger(previousState.index) || previousState.index < 0 || previousState.index >= messages.length || !Array.isArray(previousState.sent) || previousState.sent.length !== previousState.index) throw new Error('Invalid SMS submission state');\n const result = Object.fromEntries(receiptFields.map((field) => [field, input['last' + field]]));\n if (typeof result.messageSid !== 'string' || !result.messageSid.trim()) throw new Error('Twilio did not return a submitted message SID');\n state = { index: previousState.index + 1, sent: [...previousState.sent, result] };\n }\n const message = state.index < messages.length ? messages[state.index] : null;\n return { state, sent: state.sent, count: state.index, more: message !== null, to: message?.to ?? '', body: message?.body ?? '', from };\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }