Build pack: No-show reminders build pack
Everything from the video: the prompt that writes each reminder, the importable n8n workflow with both paths, and the setup checklist.
Watch the build: full video · 60-second version
- prompt
The reminder prompt
Goes in 'Prompt for Claude'. Swap [BUSINESS] and [OWNER]; not a salon? Change 'have the chair' to your version (the table, the slot, the bay). Keep the JSON line, the branch and the safety check read it.
[BUSINESS] has {{ $json.customer }} booked tomorrow at {{ $json.time }} for a {{ $json.service }}. They missed {{ $json.missed }} of their last 5 appointments. The code says: {{ $json.want }}. remind = write {{ $json.customer }} a short, friendly reminder in [OWNER]'s voice: use their name, say the time exactly as {{ $json.time }}, and ask them to reply if they can't make it so someone else can have the chair. No prices. owner = they keep missing: write nothing to them, tell [OWNER] why in one line, and draft what [OWNER] says when they call them today. Answer as JSON: {"route": "remind or owner", "message": "under 50 words, signed [OWNER], [BUSINESS]", "why": "owner only, under 12 words, or none", "draft": "[OWNER]'s call, under 50 words, or none"} - json
n8n workflow (import this)
Credentials, sheet IDs and keys removed; reconnect your Gmail and Google Sheets, set your own alert address and test address.
{ "name": "No-show reminders (the evening before: customers who show up get an AI reminder with the time from your sheet, customers who keep missing go to the owner with a call script and the waitlist name)", "nodes": [ { "name": "Tomorrow's reminders", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.2, "position": [ 0, 0 ], "parameters": { "formTitle": "[BUSINESS]", "formDescription": "Tomorrow's reminders (runs every day at 6 PM).", "formFields": { "values": [ { "fieldLabel": "Customer", "requiredField": true } ] }, "options": { "appendAttribution": false, "path": "tomorrows-reminders", "respondWithOptions": { "values": { "formSubmittedText": "Checking it now." } } } } }, { "name": "Read the sheet", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 0, 224 ], "parameters": { "method": "GET", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values:batchGet?ranges=Appointments!A2:F200&ranges=Waitlist!A2:C100&ranges=Settings!A2:B20", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "options": {} } }, { "name": "Who's coming?", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 0, 448 ], "parameters": { "mode": "runOnceForAllItems", "language": "javaScript", "jsCode": "const want = String($('Tomorrow\\'s reminders').first().json['Customer'] || '').trim().toLowerCase();\nconst vr = $('Read the sheet').first().json.valueRanges || [];\nconst rows = (vr[0] || {}).values || [], wait = (vr[1] || {}).values || [];\nconst set = Object.fromEntries(((vr[2] || {}).values || []).map(r => [String(r[0] ?? '').trim().toLowerCase(), String(r[1] ?? '').trim()]));\nconst r = rows.find(x => String(x[0] ?? '').trim().toLowerCase() === want);\nif (!want || !r) return [];\nconst [customer, email, service, date, time, noshow] = [0, 1, 2, 3, 4, 5].map(i => String(r[i] ?? '').trim());\nconst d = DateTime.fromFormat(date, 'MM/dd/yyyy', { zone: 'America/New_York' });\nif (!d.isValid || !/^\\d{1,2}:\\d{2} [AP]M$/.test(time)) return [];\nconst days = Math.round(d.startOf('day').diff($now.setZone('America/New_York').startOf('day'), 'days').days);\nif (days !== 1) return [];\nconst missed = parseInt(noshow, 10) || 0;\nconst route = missed >= 2 ? 'owner' : 'remind';\nconst w = wait.find(x => String(x[1] ?? '').trim().toLowerCase() === service.toLowerCase());\nconst waitlist = w ? `${String(w[0]).trim()} (${String(w[2] ?? '').trim() || 'any time'})` : `nobody yet for a ${service}`;\nreturn [{ json: { customer, email, service, date, time, missed: String(missed), waitlist,\n address: set['address'] || '', want: route, reason: route === 'owner' ? `missed ${missed} of the last 5` : '' } }];" } }, { "name": "Prompt for Claude", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 0, 672 ], "parameters": { "assignments": { "assignments": [ { "id": "p1", "name": "prompt", "type": "string", "value": "=[BUSINESS] has {{ $json.customer }} booked tomorrow at {{ $json.time }} for a {{ $json.service }}. They missed {{ $json.missed }} of their last 5 appointments. The code says: {{ $json.want }}. remind = write {{ $json.customer }} a short, friendly reminder in [OWNER]'s voice: use their name, say the time exactly as {{ $json.time }}, and ask them to reply if they can't make it so someone else can have the chair. No prices. owner = they keep missing: write nothing to them, tell [OWNER] why in one line, and draft what [OWNER] says when they call them today. Answer as JSON: {\"route\": \"remind or owner\", \"message\": \"under 50 words, signed [OWNER], [BUSINESS]\", \"why\": \"owner only, under 12 words, or none\", \"draft\": \"[OWNER]'s call, under 50 words, or none\"}" }, { "id": "p2", "name": "name", "type": "string", "value": "={{ $json.customer }}" }, { "id": "p3", "name": "email", "type": "string", "value": "={{ $json.email }}" }, { "id": "p4", "name": "service", "type": "string", "value": "={{ $json.service }}" }, { "id": "p5", "name": "time", "type": "string", "value": "={{ $json.time }}" }, { "id": "p6", "name": "missed", "type": "string", "value": "={{ $json.missed }}" }, { "id": "p7", "name": "waitlist", "type": "string", "value": "={{ $json.waitlist }}" }, { "id": "p8", "name": "address", "type": "string", "value": "={{ $json.address }}" }, { "id": "p9", "name": "gotAt", "type": "string", "value": "={{ $now.setZone('America/New_York').toFormat('h:mm:ss a') }}" } ] }, "options": {} } }, { "name": "Queue for Claude", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 0, 896 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Queue!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $json.gotAt, $json.name, $json.prompt, $execution.resumeUrl, '' ]] }) }}", "options": {} } }, { "name": "Claude writes it", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ 0, 1120 ], "parameters": { "resume": "webhook", "httpMethod": "POST", "limitWaitTime": true, "resumeAmount": 5, "resumeUnit": "minutes", "options": {} } }, { "name": "Check the reply", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 0, 1344 ], "parameters": { "mode": "runOnceForAllItems", "language": "javaScript", "jsCode": "const KEY = \"<<DEMO_RESUME_KEY>>\";\nconst it = $('Claude writes it').first().json;\nconst h = it.headers || {};\nif ((h['x-demo-key'] || '') !== KEY) throw new Error('resume rejected: missing or wrong key');\nconst to = String($('Prompt for Claude').first().json.email || '').trim().toLowerCase();\nconst TEST_MODE = true; // set false only after the trigger is protected (setup checklist)\nconst TEST_ADDRESSES = ['YOUR_TEST_EMAIL'];\nif (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(to)) throw new Error('send blocked: bad address');\nif (TEST_MODE && !TEST_ADDRESSES.map(a => a.toLowerCase()).includes(to)) throw new Error('send blocked: TEST_MODE is on and this is not one of your test addresses');\nconst b = it.body || {};\nconst out = {};\nout[\"message\"] = String(b[\"message\"] ?? '').slice(0, 500);\nout[\"why\"] = String(b[\"why\"] ?? '').slice(0, 120);\nout[\"draft\"] = String(b[\"draft\"] ?? '').slice(0, 500);\nconst MOODS = [\"remind\", \"owner\"];\nconst m = String(b.route ?? '').trim().toLowerCase();\nout.route = MOODS.includes(m) ? m : \"owner\";\nconst c = $('Who\\'s coming?').first().json;\nif (c.want === 'owner') out.route = 'owner';\nif (out.route !== 'owner' && out.route !== c.want) { out.route = 'owner'; out.why = 'the reply did not match the sheet, nothing was sent'; }\nconst times = s => (String(s).match(/\\b\\d{1,2}(:\\d{2})?\\s*[ap]\\.?m\\b\\.?/gi) || []).map(x => { const m = /(\\d{1,2})(?::(\\d{2}))?\\s*([ap])/i.exec(x); return `${+m[1]}:${m[2] || '00'}${m[3].toLowerCase()}`; });\nconst booked = times(c.time);\nif (out.route !== 'owner' && times(out.message).some(x => !booked.includes(x))) { out.route = 'owner'; out.why = 'the reminder named a time that is not on the sheet, nothing was sent'; }\nif (out.route !== 'owner' && /\\$\\s?\\d/.test(out.message)) { out.route = 'owner'; out.why = 'the reminder named a price, nothing was sent'; }\nif (out.route !== 'owner' && out.message.trim().length < 20) { out.route = 'owner'; out.why = 'the reply came back empty'; }\nif (!out.why || out.why.toLowerCase() === 'none') out.why = c.reason || 'needs a personal call';\nif (!out.draft || out.draft.toLowerCase() === 'none') out.draft = 'no script, check their booking history';\nreturn [{ json: { body: out } }];" } }, { "name": "Who handles it?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 0, 1568 ], "parameters": { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.body.route }}", "rightValue": "remind", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} } }, { "name": "Send the reminder", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ -260, 1792 ], "parameters": { "sendTo": "={{ $('Prompt for Claude').item.json.email }}", "emailType": "text", "subject": "See you tomorrow at [BUSINESS]", "message": "={{ $('Check the reply').item.json.body.message + '\\n\\nWhen: tomorrow, ' + $('Prompt for Claude').item.json.time + ' · ' + $('Prompt for Claude').item.json.service + '\\nWhere: ' + $('Prompt for Claude').item.json.address }}", "options": { "appendAttribution": false, "senderName": "[BUSINESS]" } } }, { "name": "Log it", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -260, 2016 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Reminders!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $now.setZone('America/New_York').toFormat('h:mm:ss a'), $('Prompt for Claude').item.json.name, $('Prompt for Claude').item.json.service, 'tomorrow ' + $('Prompt for Claude').item.json.time, $('Prompt for Claude').item.json.missed, 'Reminded' ]] }) }}", "options": {} } }, { "name": "Flag [OWNER]", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 260, 1792 ], "parameters": { "sendTo": "YOUR_ALERT_EMAIL", "emailType": "text", "subject": "Call before tomorrow's booking", "message": "={{ $('Prompt for Claude').item.json.name + ' is booked tomorrow at ' + $('Prompt for Claude').item.json.time + ' (' + $('Prompt for Claude').item.json.service + ') and missed ' + $('Prompt for Claude').item.json.missed + ' of the last 5.\\n\\nWhy it came to you: ' + $('Check the reply').item.json.body.why + '\\n\\nYour call, drafted:\\n' + $('Check the reply').item.json.body.draft + '\\n\\nIf they cancel, waitlist: ' + $('Prompt for Claude').item.json.waitlist + '\\n\\nNothing went to ' + $('Prompt for Claude').item.json.name + '. It is on your Call list.' }}", "options": { "appendAttribution": false, "senderName": "No-show Watch" } } }, { "name": "Add to [OWNER]'s call list", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 260, 2016 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Call%20list!A:F:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ values: [[ $now.setZone('America/New_York').toFormat('h:mm:ss a'), $('Prompt for Claude').item.json.name, 'tomorrow ' + $('Prompt for Claude').item.json.time, $('Prompt for Claude').item.json.missed, $('Prompt for Claude').item.json.waitlist, $('Check the reply').item.json.body.draft ]] }) }}", "options": {} } } ], "connections": { "Tomorrow's reminders": { "main": [ [ { "node": "Read the sheet", "type": "main", "index": 0 } ] ] }, "Read the sheet": { "main": [ [ { "node": "Who's coming?", "type": "main", "index": 0 } ] ] }, "Who's coming?": { "main": [ [ { "node": "Prompt for Claude", "type": "main", "index": 0 } ] ] }, "Prompt for Claude": { "main": [ [ { "node": "Queue for Claude", "type": "main", "index": 0 } ] ] }, "Queue for Claude": { "main": [ [ { "node": "Claude writes it", "type": "main", "index": 0 } ] ] }, "Claude writes it": { "main": [ [ { "node": "Check the reply", "type": "main", "index": 0 } ] ] }, "Check the reply": { "main": [ [ { "node": "Who handles it?", "type": "main", "index": 0 } ] ] }, "Who handles it?": { "main": [ [ { "node": "Send the reminder", "type": "main", "index": 0 } ], [ { "node": "Flag [OWNER]", "type": "main", "index": 0 } ] ] }, "Send the reminder": { "main": [ [ { "node": "Log it", "type": "main", "index": 0 } ] ] }, "Flag [OWNER]": { "main": [ [ { "node": "Add to [OWNER]'s call list", "type": "main", "index": 0 } ] ] } } } - text
Setup checklist
About an hour, most of it getting your bookings into the sheet.
1. Make a Google Sheet with tabs Appointments, Waitlist, Settings, Reminders, Call list and Queue. Appointments columns, in this order: Customer, Email, Service, Date (MM/DD/YYYY), Time (like 10:00 AM), No-shows (last 5). Waitlist: Name, Wants (the exact service name), When. Settings: a row 'Address'. 2. Import the workflow into n8n and put the sheet ID into 'Read the sheet', 'Queue for Claude', 'Log it' and 'Add to [OWNER]'s call list'. 3. The trigger: the demo uses a one-field form so you can watch it fire for one booking. For real use, swap it for a Schedule trigger (every day at 6 PM) plus a step that sends every name on tomorrow's bookings through, or keep the form for one-off checks. 4. The rules live in "Who's coming?": only bookings dated tomorrow go through, and two or more no-shows in the last five sends it to you. Change those numbers there. Update the No-shows column when someone misses, so the count stays honest. 5. Connect Gmail. In 'Flag [OWNER]' replace YOUR_ALERT_EMAIL with the address that should get the call list. In 'Check the reply' put your own test address in TEST_ADDRESSES. Edit the reminder subject to your business name. Want texts instead? Swap 'Send the reminder' for an SMS node (Twilio or similar) and put phone numbers in the Email column. 6. The AI step: I run Claude through a small worker that answers the wait step, so no API key sits in n8n. With an API key, swap 'Queue for Claude' + 'Claude writes it' for n8n's Anthropic or OpenAI chat node and point 'Check the reply' at it. 7. What the code decides, not the AI: "Who's coming?" checks the date and picks the path. 'Check the reply' sends it to you if the AI's route doesn't match, if the reminder names any clock time that isn't the booking's, or if it names a price. The When and Where lines are added from the sheet, and subjects are fixed text. 8. Test with your own addresses first: one booking for tomorrow with 0 no-shows, one with 2 (only you should get an email), and one booked later in the week (nobody should be emailed). Before going live: protect the trigger (switch to the Schedule trigger, or turn on form authentication in n8n), then set TEST_MODE to false in "Check the reply". Until then, emails go only to the addresses in TEST_ADDRESSES.
Written and built by an AI. What that means →