Build pack: Client intake build pack
Everything from the video: the prompt that writes each welcome or follow-up, the importable n8n workflow with all three paths, and the setup checklist.
Watch the build: full video · 60-second version
- prompt
The client intake prompt
Goes in 'Prompt for Claude'. Swap [BUSINESS] and [OWNER]; keep the JSON line, the branch and the safety check read it.
[BUSINESS] got a new client form. {{ $json.name }} wants a {{ $json.project }} project. Budget: {{ $json.budget || 'blank' }}. Start: {{ $json.start || 'blank' }}. About it: "{{ $json.about || 'blank' }}" Still missing, per the checklist: {{ $json.missing || 'nothing' }}. Nothing missing = route welcome: welcome them, one line on what you heard, in [OWNER]'s voice. Something missing = route chase: thank them, ask only for the missing items, say why each one helps. Never quote a price. Anything [OWNER] must see first (a complaint, a rush, a question you can't answer) = route owner: write nothing to them, say why, draft [OWNER]'s reply. Answer as JSON: {"route": "welcome, chase or owner", "message": "under 60 words, signed [OWNER], [BUSINESS]", "summary": "the project in under 12 words, for the client file", "why": "owner only, under 12 words, or none", "draft": "[OWNER]'s reply, under 60 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": "Client intake (checks your checklist sheet: complete = filed + welcomed, missing = asks for exactly that, anything odd goes to the owner)", "nodes": [ { "name": "New client form", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.2, "position": [ 0, 0 ], "parameters": { "formTitle": "[BUSINESS]", "formDescription": "New client form.", "formFields": { "values": [ { "fieldLabel": "Name", "requiredField": true }, { "fieldLabel": "Email", "fieldType": "email", "requiredField": true }, { "fieldLabel": "Project", "fieldType": "dropdown", "requiredField": true, "fieldOptions": { "values": [ { "option": "Kitchen" }, { "option": "Bathroom" }, { "option": "Living room" }, { "option": "Whole home" } ] } }, { "fieldLabel": "Budget", "fieldType": "dropdown", "fieldOptions": { "values": [ { "option": "Not sure yet" }, { "option": "Under $10k" }, { "option": "$10k-$25k" }, { "option": "$25k-$60k" }, { "option": "$60k+" } ] } }, { "fieldLabel": "Start date" }, { "fieldLabel": "Photos link" }, { "fieldLabel": "Tell us about it", "fieldType": "textarea" } ] }, "options": { "appendAttribution": false, "path": "new-client-form", "respondWithOptions": { "values": { "formSubmittedText": "Thanks. Check your inbox." } } } } }, { "name": "Read the checklist", "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/Checklist!A2:B50", "authentication": "predefinedCredentialType", "nodeCredentialType": "googleDriveOAuth2Api", "options": {} } }, { "name": "Anything missing?", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 0, 448 ], "parameters": { "mode": "runOnceForAllItems", "language": "javaScript", "jsCode": "const f = $('New client form').first().json;\nconst rows = $('Read the checklist').first().json.values || [];\nconst val = k => String(f[k] ?? '').trim();\nconst project = val('Project');\nconst row = rows.find(r => String(r[0] ?? '').trim().toLowerCase() === project.toLowerCase());\nconst needs = row ? String(row[1] ?? '').split(',').map(x => x.trim()).filter(Boolean) : [];\nconst blank = k => { const v = val(k); return !v || /^not sure/i.test(v) || (k === 'Tell us about it' && v.split(/\\s+/).length < 5); };\nconst missing = needs.filter(blank);\nreturn [{ json: { name: val('Name'), email: val('Email'), project, budget: val('Budget'), start: val('Start date'),\n photos: val('Photos link'), about: val('Tell us about it'), known: !!row, needs: needs.join(', '), missing: missing.join(', ') } }];" } }, { "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] got a new client form. {{ $json.name }} wants a {{ $json.project }} project. Budget: {{ $json.budget || 'blank' }}. Start: {{ $json.start || 'blank' }}. About it: \"{{ $json.about || 'blank' }}\" Still missing, per the checklist: {{ $json.missing || 'nothing' }}. Nothing missing = route welcome: welcome them, one line on what you heard, in [OWNER]'s voice. Something missing = route chase: thank them, ask only for the missing items, say why each one helps. Never quote a price. Anything [OWNER] must see first (a complaint, a rush, a question you can't answer) = route owner: write nothing to them, say why, draft [OWNER]'s reply. Answer as JSON: {\"route\": \"welcome, chase or owner\", \"message\": \"under 60 words, signed [OWNER], [BUSINESS]\", \"summary\": \"the project in under 12 words, for the client file\", \"why\": \"owner only, under 12 words, or none\", \"draft\": \"[OWNER]'s reply, under 60 words, or none\"}" }, { "id": "p2", "name": "name", "type": "string", "value": "={{ $json.name }}" }, { "id": "p3", "name": "email", "type": "string", "value": "={{ $json.email }}" }, { "id": "p4", "name": "project", "type": "string", "value": "={{ $json.project }}" }, { "id": "p5", "name": "budget", "type": "string", "value": "={{ $json.budget }}" }, { "id": "p6", "name": "start", "type": "string", "value": "={{ $json.start }}" }, { "id": "p7", "name": "missing", "type": "string", "value": "={{ $json.missing }}" }, { "id": "p8", "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:E: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 form 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, 600);\nout[\"summary\"] = String(b[\"summary\"] ?? '').slice(0, 140);\nout[\"why\"] = String(b[\"why\"] ?? '').slice(0, 120);\nout[\"draft\"] = String(b[\"draft\"] ?? '').slice(0, 600);\nconst MOODS = [\"welcome\", \"chase\", \"owner\"];\nconst m = String(b.route ?? '').trim().toLowerCase();\nout.route = MOODS.includes(m) ? m : \"owner\";\nconst s = $('Anything missing?').first().json;\nconst want = s.missing ? 'chase' : 'welcome';\nif (!s.known) { out.route = 'owner'; out.why = 'that project type is not on the checklist'; }\nif (out.route !== 'owner' && out.route !== want) { out.route = 'owner'; out.why = 'the reply did not match the checklist, nothing was sent'; }\nconst prices = (out.message.match(/\\$\\s?\\d[\\d,.]*k?\\+?/gi) || []).map(x => x.replace(/\\s/g, '').toLowerCase());\nif (out.route !== 'owner' && prices.some(x => !s.budget.toLowerCase().includes(x))) { out.route = 'owner'; out.why = 'the reply named a price that is not on the form, nothing was sent'; }\nif (out.route !== 'owner' && out.message.trim().length < 20) { out.route = 'owner'; out.why = 'the reply came back empty'; }\nif (!out.summary || out.summary.toLowerCase() === 'none') out.summary = s.project + ' project';\nif (!out.why || out.why.toLowerCase() === 'none') out.why = 'their form needs you';\nif (!out.draft || out.draft.toLowerCase() === 'none') out.draft = 'no draft, read their form';\nreturn [{ json: { body: out } }];" } }, { "name": "Which path?", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ 0, 1568 ], "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_welcome", "leftValue": "={{ $json.body.route }}", "rightValue": "welcome", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Complete" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_chase", "leftValue": "={{ $json.body.route }}", "rightValue": "chase", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Missing" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "r_owner", "leftValue": "={{ $json.body.route }}", "rightValue": "owner", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "[OWNER]" } ] }, "options": {} } }, { "name": "File the client", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -380, 1792 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Clients!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.project, $('Prompt for Claude').item.json.budget, $('Prompt for Claude').item.json.start, $('Check the reply').item.json.body.summary ]] }) }}", "options": {} } }, { "name": "Send welcome", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ -380, 2016 ], "parameters": { "sendTo": "={{ $('Prompt for Claude').item.json.email }}", "emailType": "text", "subject": "Welcome to [BUSINESS]", "message": "={{ $('Check the reply').item.json.body.message + '\\n\\nYour file: ' + $('Prompt for Claude').item.json.project + ' · ' + $('Prompt for Claude').item.json.budget + ' · start ' + $('Prompt for Claude').item.json.start + '\\nNext: [OWNER] sends you two times for a walkthrough.' }}", "options": { "appendAttribution": false, "senderName": "[BUSINESS]" } } }, { "name": "Ask for the rest", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 0, 1792 ], "parameters": { "sendTo": "={{ $('Prompt for Claude').item.json.email }}", "emailType": "text", "subject": "One more thing before we start", "message": "={{ $('Check the reply').item.json.body.message + '\\n\\nStill needed: ' + $('Prompt for Claude').item.json.missing + '\\nJust reply to this email with them.' }}", "options": { "appendAttribution": false, "senderName": "[BUSINESS]" } } }, { "name": "Add to Waiting on", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 0, 2016 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/Waiting%20on!A:D: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.project, $('Prompt for Claude').item.json.missing ]] }) }}", "options": {} } }, { "name": "Ping [OWNER]", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 380, 1792 ], "parameters": { "sendTo": "YOUR_ALERT_EMAIL", "emailType": "text", "subject": "A new client needs you", "message": "={{ $('Prompt for Claude').item.json.name + ' sent the new client form: ' + $('Prompt for Claude').item.json.project + '.\\n\\nWhy it came to you: ' + $('Check the reply').item.json.body.why + '\\n\\nYour reply, drafted:\\n' + $('Check the reply').item.json.body.draft + '\\n\\nNothing went to ' + $('Prompt for Claude').item.json.name + '. It is on your list.' }}", "options": { "appendAttribution": false, "senderName": "Client Desk" } } }, { "name": "Add to [OWNER]'s list", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 380, 2016 ], "parameters": { "method": "POST", "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_GOOGLE_SHEET_ID/values/For%20[OWNER]!A:E: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.project, $('Check the reply').item.json.body.why, $('Check the reply').item.json.body.draft ]] }) }}", "options": {} } } ], "connections": { "New client form": { "main": [ [ { "node": "Read the checklist", "type": "main", "index": 0 } ] ] }, "Read the checklist": { "main": [ [ { "node": "Anything missing?", "type": "main", "index": 0 } ] ] }, "Anything missing?": { "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": "Which path?", "type": "main", "index": 0 } ] ] }, "Which path?": { "main": [ [ { "node": "File the client", "type": "main", "index": 0 } ], [ { "node": "Ask for the rest", "type": "main", "index": 0 } ], [ { "node": "Ping [OWNER]", "type": "main", "index": 0 } ] ] }, "File the client": { "main": [ [ { "node": "Send welcome", "type": "main", "index": 0 } ] ] }, "Ask for the rest": { "main": [ [ { "node": "Add to Waiting on", "type": "main", "index": 0 } ] ] }, "Ping [OWNER]": { "main": [ [ { "node": "Add to [OWNER]'s list", "type": "main", "index": 0 } ] ] } } } - text
Setup checklist
About an hour, most of it deciding what each kind of project needs.
1. Make a Google Sheet with tabs Checklist, Clients, Waiting on, For Owner and Queue (then change the tab name in the URL of 'Add to [OWNER]'s list' from 'For%20[OWNER]' to 'For%20Owner'). Checklist columns: Project, Needs before we start. One row per project type; the Needs cell lists form field names separated by commas, written exactly like the form labels (for example 'Budget, Start date, Photos link'). 2. Import the workflow into n8n and put the sheet ID into 'Read the checklist', 'Queue for Claude', 'File the client', 'Add to Waiting on' and 'Add to [OWNER]'s list'. 3. Make the form yours: the Project options must match the Checklist rows, and every item in a Needs cell must be a form field. A budget of 'Not sure yet' counts as missing, and 'Tell us about it' needs at least five words (both in 'Anything missing?'). 4. Connect Gmail. In 'Ping [OWNER]' replace YOUR_ALERT_EMAIL with the address that should get the forms that need you. In 'Check the reply' put your own test address in TEST_ADDRESSES. Edit the 'Next:' line in 'Send welcome' to your real next step. 5. 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. 6. What the code decides, not the AI: 'Anything missing?' reads the checklist and lists the gaps. 'Check the reply' sends the form to you if the AI's route doesn't match the checklist, if the project isn't on the checklist, or if the email names any dollar amount other than the budget the client picked. The 'Your file' and 'Still needed' lines are added from the form and the code, and subjects are fixed text. 7. Using your own form (Typeform, Jotform, Google Forms, your website): send it to an n8n Webhook trigger instead of the n8n form, and keep the field names the same as the checklist items. 8. Test with your own addresses first: one complete form, one with the budget left as 'Not sure yet', one for a project type that isn't on the checklist. Only the first two should email the client. Before going live: protect the form (turn on form authentication in n8n, or put it behind your site's own intake page and an internal-only trigger), 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 →