{
  "name": "Win-back text (Barbershops & salons)",
  "nodes": [
    {
      "id": "c00ee37c-ce07-58c6-b37b-f85e12a3aa51",
      "name": "Every morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 10
            }
          ]
        }
      }
    },
    {
      "id": "ed9f0174-0223-56cc-bf9e-b1f8ee0dd8c2",
      "name": "Incoming text",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "win-back-sms",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "0d5a99a2-d050-5e64-8101-1279a583bd17"
    },
    {
      "id": "f3ff6683-3404-5a1d-9abd-93237dbe1d3d",
      "name": "Config (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        140
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "eb105c88-06a9-5523-97f2-f852e77b4562",
              "name": "business_name",
              "value": "YOUR BUSINESS NAME",
              "type": "string"
            },
            {
              "id": "d6e10b79-9a97-5b56-8212-9ea1af873cf2",
              "name": "booking_link",
              "value": "https://YOUR-BOOKING-LINK",
              "type": "string"
            },
            {
              "id": "da6fd523-5711-515d-b502-139a2de3e808",
              "name": "hours",
              "value": "Mon-Fri 8am-6pm",
              "type": "string"
            },
            {
              "id": "cec39efe-f48d-577a-a751-732a53fa17bf",
              "name": "owner_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "60b2a7c2-e411-5806-a614-d74cc4440083",
              "name": "owner_cell",
              "value": "+15555550100",
              "type": "string"
            },
            {
              "id": "c4b524a5-396a-5237-a6e6-10c152f76287",
              "name": "twilio_number",
              "value": "+15555550199",
              "type": "string"
            },
            {
              "id": "14d65fff-b0d1-5df7-afbe-0be9cdb45ed8",
              "name": "twilio_auth_token",
              "value": "PASTE_YOUR_TWILIO_AUTH_TOKEN",
              "type": "string"
            },
            {
              "id": "e80848e0-ebd2-5458-af06-9e32270d9421",
              "name": "twilio_sms_webhook_url",
              "value": "https://YOUR-N8N-URL/webhook/win-back-sms",
              "type": "string"
            },
            {
              "id": "2f68f41f-ac70-5959-b62f-66bb2b28f72f",
              "name": "clients_sheet_id",
              "value": "YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "0b18e10c-4c57-5add-89b0-7ef084b776f5",
              "name": "winback_days",
              "value": "35",
              "type": "string"
            },
            {
              "id": "f9c1af6c-46b4-5e9e-9057-272f6e0e3053",
              "name": "daily_cap",
              "value": "25",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "4f83f149-89ca-5a7a-a462-c7f1f48bd647",
      "name": "Scheduled run or text?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "bf74e186-2080-52da-86a7-a550d437f105",
              "leftValue": "={{ ($json.body || {}).MessageSid }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "e7e35ce4-4bc7-55d3-a533-b3eebf284c6a",
      "name": "Read your list",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        0
      ],
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config (edit me)').first().json.clients_sheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Clients",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account (placeholder)"
        }
      }
    },
    {
      "id": "dbcfc9cb-3762-5de8-a546-ce13466d8836",
      "name": "Due for a nudge?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "db734e1f-473c-5495-8f50-35814f213b45",
              "leftValue": "={{ !!$json.phone && !String($json.opted_out || '').trim() && DateTime.fromISO(String($json.last_visit || '')).isValid && $now.diff(DateTime.fromISO(String($json.last_visit)), 'days').days >= Number($('Config (edit me)').first().json.winback_days) && (!String($json.last_texted || '').trim() || DateTime.fromISO(String($json.last_texted)) < DateTime.fromISO(String($json.last_visit))) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "b85f95ba-3168-5aa5-be29-e73bdddf1a99",
      "name": "Daily cap",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "maxItems": "={{ Number($('Config (edit me)').first().json.daily_cap) || 25 }}"
      }
    },
    {
      "id": "36db68ee-2124-571b-ad65-a036dfc78cc1",
      "name": "Send the win-back text",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $json.phone }}",
        "message": "=Hey {{ String($json.name || '').split(' ')[0] || 'there' }}, it's {{ $('Config (edit me)').first().json.business_name }}. It's been about 5 weeks since your last cut. Want to grab a spot? {{ $('Config (edit me)').first().json.booking_link }} Walk-ins welcome too. Reply STOP to opt out.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "4601598a-632f-5686-80c3-d92f668a0fee",
      "name": "Mark as texted",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config (edit me)').first().json.clients_sheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Clients",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "phone": "={{ $('Daily cap').item.json.phone }}",
            "last_texted": "={{ $now.toFormat('yyyy-MM-dd') }}"
          },
          "matchingColumns": [
            "phone"
          ],
          "schema": [
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "last_texted",
              "displayName": "last_texted",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account (placeholder)"
        }
      }
    },
    {
      "id": "09f99900-cd4e-5fe0-b81b-4f3109302739",
      "name": "Email you a summary",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Win-back texts sent today: {{ $('Daily cap').all().length }}",
        "emailType": "text",
        "message": "=Texted {{ $('Daily cap').all().length }} people from your list today. Replies come to you by email; anyone who replies STOP is opted out by Twilio automatically (also put an x in their opted_out cell).",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      },
      "onError": "continueRegularOutput",
      "executeOnce": true
    },
    {
      "id": "a8bd3c99-b90f-540b-ab59-aee730ed8e44",
      "name": "Check Twilio signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        460
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Is this request really from Twilio? Checks the X-Twilio-Signature header.\n// Twilio signs: the exact webhook URL + every POST field, sorted by name, as name+value,\n// using HMAC-SHA1 keyed with your auth token, base64-encoded.\n// n8n Cloud allows require('crypto'). Self-hosted: set NODE_FUNCTION_ALLOW_BUILTIN=crypto\n// (on the task runner too, if you use one).\nconst crypto = require('crypto');\nconst cfg = $('Config (edit me)').first().json;\nconst req = $json;\nconst params = req.body || {};\nconst url = String(cfg.twilio_sms_webhook_url || '').trim();\nconst data = url + Object.keys(params).sort().map(k => k + params[k]).join('');\nconst expected = crypto.createHmac('sha1', String(cfg.twilio_auth_token || '')).update(data, 'utf8').digest('base64');\nconst received = String((req.headers || {})['x-twilio-signature'] || '');\nconst a = Buffer.from(expected);\nconst b = Buffer.from(received);\nconst ok = url !== '' && received !== '' && a.length === b.length && crypto.timingSafeEqual(a, b);\nreturn { json: { ...req, signature_ok: ok } };"
      }
    },
    {
      "id": "6077256b-0cf5-503f-915d-7f63a84b6440",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "306f8f79-565a-5053-9bf1-1d21c85e1d48",
              "leftValue": "={{ $json.signature_ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "1ab151ed-6c8b-5166-b80f-78153b10e4aa",
      "name": "Reject: not from Twilio (403)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        640
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "Forbidden",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          },
          "responseCode": 403
        }
      }
    },
    {
      "id": "3644e405-fa6e-5aa8-b80e-0189c6fb4148",
      "name": "Acknowledge Twilio (text)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        460
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "<Response></Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      }
    },
    {
      "id": "1a42c447-77f0-50ee-ad65-3f278a8a39dc",
      "name": "Says URGENT?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "d869348a-e545-5bf4-91ad-16f20547edf0",
              "leftValue": "={{ $('Incoming text').first().json.body.Body }}",
              "rightValue": "urgent",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "9beeeca6-65b5-57da-88d7-25d7af1c7f17",
      "name": "Text the owner (URGENT)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1540,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Config (edit me)').first().json.owner_cell }}",
        "message": "=URGENT text to {{ $('Config (edit me)').first().json.business_name }} from {{ $('Incoming text').first().json.body.From }}: \"{{ $('Incoming text').first().json.body.Body }}\" Call or text them back now.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "79b3732e-4d61-5d12-8fd4-6e35243c7fd7",
      "name": "Text the customer (URGENT ack)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1760,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Incoming text').first().json.body.From }}",
        "message": "=Got it, we just sent your message straight to the shop's phone. Thanks for the heads-up! - {{ $('Config (edit me)').first().json.business_name }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "6f93c095-d385-54f2-956e-e15a8df1be07",
      "name": "Email the owner (URGENT, optional)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        280
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=URGENT: text from {{ $('Incoming text').first().json.body.From }}",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nYour cell was texted and they were told you'll get back to them right away.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "4655dea7-1fd0-585d-829e-627a8f536f0e",
      "name": "Forward the reply to you",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1540,
        640
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Reply from {{ $('Incoming text').first().json.body.From }} (replied to a win-back text)",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nReply to them from your phone or the Twilio console.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      }
    },
    {
      "id": "75b5dba4-b987-57ff-a3f3-234d65bce579",
      "name": "Setup steps",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -200
      ],
      "parameters": {
        "content": "## Setup (about 1-2 hours)\n1. Make a Google Sheet with a tab named `Clients` and columns `name | phone | last_visit | last_texted | opted_out`. Phones in +1 format, dates as YYYY-MM-DD (format the columns as plain text). Start with an export of your client list from your booking tool if it has one, then keep last_visit current (paste a fresh export weekly, or update it at checkout).\n2. Fill in **Config (edit me)**: sheet ID, how many days counts as overdue (**winback_days**), the daily cap, Twilio number, **auth token** and webhook URL.\n3. Connect **Google Sheets**, **Twilio** and **Gmail** credentials.\n4. In Twilio > Phone Numbers > your number > Messaging: 'A message comes in' > Webhook, HTTP POST, `https://YOUR-N8N-URL/webhook/win-back-sms`. Paste the exact same URL into **twilio_sms_webhook_url** in Config. A number has one 'message comes in' webhook, so if you run several of these workflows on one number, point it at only one of them.\n5. Only include people who agreed to get texts from you. Activate: every morning at 10 it texts up to the daily cap, once per visit.\n\n**Security:** the Check Twilio signature node rejects (403) anything not signed with your Twilio auth token. Self-hosted n8n: set `NODE_FUNCTION_ALLOW_BUILTIN=crypto`. Don't share this workflow once your token is in it.",
        "height": 640,
        "width": 540
      }
    }
  ],
  "connections": {
    "Every morning": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Incoming text": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (edit me)": {
      "main": [
        [
          {
            "node": "Scheduled run or text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scheduled run or text?": {
      "main": [
        [
          {
            "node": "Read your list",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Twilio signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read your list": {
      "main": [
        [
          {
            "node": "Due for a nudge?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Due for a nudge?": {
      "main": [
        [
          {
            "node": "Daily cap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily cap": {
      "main": [
        [
          {
            "node": "Send the win-back text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the win-back text": {
      "main": [
        [
          {
            "node": "Mark as texted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as texted": {
      "main": [
        [
          {
            "node": "Email you a summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Twilio signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Acknowledge Twilio (text)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject: not from Twilio (403)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Twilio (text)": {
      "main": [
        [
          {
            "node": "Says URGENT?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Says URGENT?": {
      "main": [
        [
          {
            "node": "Text the owner (URGENT)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Forward the reply to you",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the owner (URGENT)": {
      "main": [
        [
          {
            "node": "Text the customer (URGENT ack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the customer (URGENT ack)": {
      "main": [
        [
          {
            "node": "Email the owner (URGENT, optional)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false,
    "generatedBy": "Speed-to-Lead Playbooks"
  },
  "tags": []
}