The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Twilio Inbound \u2014 STOP + Reply Handler (Template)",
"nodes": [
{
"id": "i1",
"name": "Twilio Inbound SMS",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
200,
400
],
"parameters": {
"httpMethod": "POST",
"path": "twilio/inbound-sms",
"responseMode": "lastNode",
"multipleMethods": false,
"options": {
"rawBody": false
}
},
"notes": "Configure this URL in Twilio Console \u2192 Phone Number \u2192 'A MESSAGE COMES IN'. Twilio POSTs form-encoded with From, Body, To, MessageSid."
},
{
"id": "i2",
"name": "Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
420,
400
],
"parameters": {
"mode": "manual",
"includeOtherFields": true,
"assignments": {
"assignments": [
{
"id": "1",
"name": "tenantId",
"type": "string",
"value": "={{ $env.ST_TENANT_ID }}"
},
{
"id": "2",
"name": "appKey",
"type": "string",
"value": "={{ $env.ST_APP_KEY }}"
},
{
"id": "3",
"name": "clientId",
"type": "string",
"value": "={{ $env.ST_CLIENT_ID }}"
},
{
"id": "4",
"name": "clientSecret",
"type": "string",
"value": "={{ $env.ST_CLIENT_SECRET }}"
},
{
"id": "5",
"name": "stBaseUrl",
"type": "string",
"value": "https://api-integration.servicetitan.io"
},
{
"id": "6",
"name": "stAuthUrl",
"type": "string",
"value": "https://auth-integration.servicetitan.io/connect/token"
},
{
"id": "7",
"name": "resendApiKey",
"type": "string",
"value": "={{ $env.RESEND_API_KEY }}"
},
{
"id": "8",
"name": "resendFromEmail",
"type": "string",
"value": "={{ $env.RESEND_FROM_EMAIL }}"
},
{
"id": "9",
"name": "ownerNotifyEmail",
"type": "string",
"value": "={{ $env.OWNER_NOTIFY_EMAIL }}"
},
{
"id": "10",
"name": "clientName",
"type": "string",
"value": "={{ $env.CLIENT_BUSINESS_NAME }}"
},
{
"id": "11",
"name": "fromPhone",
"type": "string",
"value": "={{ ($json.body && $json.body.From) || $json.From || '' }}"
},
{
"id": "12",
"name": "messageBody",
"type": "string",
"value": "={{ ($json.body && $json.body.Body) || $json.Body || '' }}"
},
{
"id": "13",
"name": "isOptOut",
"type": "boolean",
"value": "={{ /^\\s*(stop|stopall|unsubscribe|cancel|quit|end|opt[ -]?out)\\s*$/i.test((($json.body && $json.body.Body) || $json.Body || '')) }}"
}
]
},
"options": {}
},
"notes": "Parses Twilio form payload and classifies as opt-out vs regular reply."
},
{
"id": "i3",
"name": "ServiceTitan OAuth Token",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
640,
400
],
"parameters": {
"method": "POST",
"url": "={{ $json.stAuthUrl }}",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "grant_type",
"value": "client_credentials"
},
{
"name": "client_id",
"value": "={{ $json.clientId }}"
},
{
"name": "client_secret",
"value": "={{ $json.clientSecret }}"
}
]
},
"options": {}
}
},
{
"id": "i4",
"name": "Carry Context",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
860,
400
],
"parameters": {
"mode": "manual",
"includeOtherFields": false,
"assignments": {
"assignments": [
{
"id": "1",
"name": "accessToken",
"type": "string",
"value": "={{ $json.access_token }}"
},
{
"id": "2",
"name": "tenantId",
"type": "string",
"value": "={{ $('Config').item.json.tenantId }}"
},
{
"id": "3",
"name": "appKey",
"type": "string",
"value": "={{ $('Config').item.json.appKey }}"
},
{
"id": "4",
"name": "stBaseUrl",
"type": "string",
"value": "={{ $('Config').item.json.stBaseUrl }}"
},
{
"id": "5",
"name": "fromPhone",
"type": "string",
"value": "={{ $('Config').item.json.fromPhone }}"
},
{
"id": "6",
"name": "messageBody",
"type": "string",
"value": "={{ $('Config').item.json.messageBody }}"
},
{
"id": "7",
"name": "isOptOut",
"type": "boolean",
"value": "={{ $('Config').item.json.isOptOut }}"
},
{
"id": "8",
"name": "ownerNotifyEmail",
"type": "string",
"value": "={{ $('Config').item.json.ownerNotifyEmail }}"
},
{
"id": "9",
"name": "clientName",
"type": "string",
"value": "={{ $('Config').item.json.clientName }}"
},
{
"id": "10",
"name": "resendApiKey",
"type": "string",
"value": "={{ $('Config').item.json.resendApiKey }}"
},
{
"id": "11",
"name": "resendFromEmail",
"type": "string",
"value": "={{ $('Config').item.json.resendFromEmail }}"
}
]
},
"options": {}
}
},
{
"id": "i5",
"name": "Find Customer by Phone",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1080,
400
],
"parameters": {
"method": "GET",
"url": "={{ $json.stBaseUrl }}/crm/v2/tenant/{{ $json.tenantId }}/customers?phone={{ encodeURIComponent($json.fromPhone) }}&pageSize=1",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.accessToken }}"
},
{
"name": "ST-App-Key",
"value": "={{ $json.appKey }}"
}
]
},
"sendBody": false,
"options": {}
},
"continueOnFail": true,
"notes": "Best-effort lookup. If no match, downstream nodes still complete."
},
{
"id": "i6",
"name": "Opt-out?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1300,
400
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "c1",
"leftValue": "={{ $('Carry Context').item.json.isOptOut }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
},
"options": {}
}
},
{
"id": "i7",
"name": "PATCH doNotMail=true",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1540,
280
],
"parameters": {
"method": "PATCH",
"url": "={{ $('Carry Context').item.json.stBaseUrl }}/crm/v2/tenant/{{ $('Carry Context').item.json.tenantId }}/customers/{{ ($json.data && $json.data[0] && $json.data[0].id) || '' }}",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Carry Context').item.json.accessToken }}"
},
{
"name": "ST-App-Key",
"value": "={{ $('Carry Context').item.json.appKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ doNotMail: true }) }}",
"options": {}
},
"continueOnFail": true
},
{
"id": "i8",
"name": "Note: Opted Out",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1760,
280
],
"parameters": {
"method": "POST",
"url": "={{ $('Carry Context').item.json.stBaseUrl }}/crm/v2/tenant/{{ $('Carry Context').item.json.tenantId }}/customers/{{ ($('Find Customer by Phone').item.json.data && $('Find Customer by Phone').item.json.data[0] && $('Find Customer by Phone').item.json.data[0].id) || '' }}/notes",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Carry Context').item.json.accessToken }}"
},
{
"name": "ST-App-Key",
"value": "={{ $('Carry Context').item.json.appKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ text: 'ModernFlow Automation: customer opted out via SMS reply (' + $('Carry Context').item.json.messageBody + '). doNotMail set to true.', pinToTop: true }) }}",
"options": {}
},
"continueOnFail": true
},
{
"id": "i9",
"name": "Note: Reply Received",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1540,
520
],
"parameters": {
"method": "POST",
"url": "={{ $('Carry Context').item.json.stBaseUrl }}/crm/v2/tenant/{{ $('Carry Context').item.json.tenantId }}/customers/{{ ($json.data && $json.data[0] && $json.data[0].id) || '' }}/notes",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Carry Context').item.json.accessToken }}"
},
{
"name": "ST-App-Key",
"value": "={{ $('Carry Context').item.json.appKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ text: 'Customer replied to follow-up SMS: \"' + $('Carry Context').item.json.messageBody + '\" (from ' + $('Carry Context').item.json.fromPhone + ')', pinToTop: true }) }}",
"options": {}
},
"continueOnFail": true
},
{
"id": "i10",
"name": "Alert Owner (Resend)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1760,
520
],
"parameters": {
"method": "POST",
"url": "https://api.resend.com/emails",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Carry Context').item.json.resendApiKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ from: $('Carry Context').item.json.resendFromEmail, to: [$('Carry Context').item.json.ownerNotifyEmail], subject: 'Customer reply to ' + $('Carry Context').item.json.clientName + ' follow-up', html: '<p>A customer replied to a follow-up SMS:</p><blockquote>' + $('Carry Context').item.json.messageBody + '</blockquote><p>From: ' + $('Carry Context').item.json.fromPhone + '</p>' }) }}",
"options": {}
},
"continueOnFail": true,
"notes": "Emails the business owner so they can respond personally."
}
],
"connections": {
"Twilio Inbound SMS": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Config": {
"main": [
[
{
"node": "ServiceTitan OAuth Token",
"type": "main",
"index": 0
}
]
]
},
"ServiceTitan OAuth Token": {
"main": [
[
{
"node": "Carry Context",
"type": "main",
"index": 0
}
]
]
},
"Carry Context": {
"main": [
[
{
"node": "Find Customer by Phone",
"type": "main",
"index": 0
}
]
]
},
"Find Customer by Phone": {
"main": [
[
{
"node": "Opt-out?",
"type": "main",
"index": 0
}
]
]
},
"Opt-out?": {
"main": [
[
{
"node": "PATCH doNotMail=true",
"type": "main",
"index": 0
}
],
[
{
"node": "Note: Reply Received",
"type": "main",
"index": 0
}
]
]
},
"PATCH doNotMail=true": {
"main": [
[
{
"node": "Note: Opted Out",
"type": "main",
"index": 0
}
]
]
},
"Note: Reply Received": {
"main": [
[
{
"node": "Alert Owner (Resend)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveManualExecutions": true,
"timezone": "America/New_York"
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Twilio Inbound — STOP + Reply Handler (Template). Uses httpRequest. Webhook trigger; 10 nodes.
Source: https://github.com/itsdariusdavis-a11y/modernflow-ai/blob/c78ec6f21948f38bc8445a6df53ebb205cd3bf36/automations/servicetitan-followup/inbound-workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c