<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ilan Flows]]></title><description><![CDATA[Ilan Flows]]></description><link>https://ilanflows.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 23:01:04 GMT</lastBuildDate><atom:link href="https://ilanflows.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Chasing unpaid invoices with n8n without annoying your clients (or sending twice)]]></title><description><![CDATA[By Ilan Pinhasov (Ilan Flows) Late payments are a cash-flow problem that's also a social one: you need to remind clients, but a duplicate or a reminder for…]]></description><link>https://ilanflows.hashnode.dev/chasing-unpaid-invoices-with-n8n-without-annoying-your-clients-or-sending-twice</link><guid isPermaLink="true">https://ilanflows.hashnode.dev/chasing-unpaid-invoices-with-n8n-without-annoying-your-clients-or-sending-twice</guid><category><![CDATA[automation]]></category><category><![CDATA[No Code]]></category><category><![CDATA[Small business]]></category><category><![CDATA[n8n]]></category><dc:creator><![CDATA[Ilan Flows]]></dc:creator><pubDate>Thu, 22 Oct 2026 13:25:31 GMT</pubDate><content:encoded><![CDATA[<p><em>By Ilan Pinhasov (Ilan Flows)</em></p>
<p>Late payments are a cash-flow problem that's also a social one: you need to remind clients, but a duplicate or a reminder for an invoice they already paid is embarrassing. Here's a structure for a reminder system in n8n using Google Sheets, Telegram and Gmail. No AI is needed.</p>
<h2>1. The Sheet is the source of truth</h2>
<p>One row per invoice: <code>invoice_no, client_id, client_name, email, lang, amount, currency, issue_date, due_date, status (open/paid/cancelled)</code>, plus a Clients tab with a <code>paused</code> flag per client. Optionally, add a Webhook node (Header Auth) so other tools can upsert invoices by <code>invoice_no</code> and mark them paid. Validate input and return a 422 with the reasons when a row is bad.</p>
<h2>2. A daily planner with stages</h2>
<p>Define stages relative to the due date, e.g. <code>[-3, 0, 7, 14]</code> days. Each morning, for each open invoice:</p>
<ul>
<li>skip if paid or cancelled, if the client is paused, if a reminder is still waiting for approval, or if no stage has been reached yet;</li>
<li>enforce a minimum gap since the last reminder;</li>
<li>pick the latest stage reached.</li>
</ul>
<h2>3. Idempotency: one key per invoice + stage</h2>
<p>Hash <code>invoice_no + due_date + stage</code> into a short key (including the due date means a moved due date starts a fresh schedule) and store it in a "Reminders" tab. If the key exists, don't plan it again. This makes re-runs safe: run the planner five times, and you still get one reminder per stage.</p>
<pre><code class="language-js">const crypto = require('crypto');
const key = crypto.createHash('sha256')
  .update(`${$json.invoice_no}|${$json.due_date}|${$json.stage}`).digest('hex').slice(0, 16);
</code></pre>
<p>(This snippet needs the <code>crypto</code> module allowed in your n8n Code node settings. The packaged template uses a small built-in hash instead, so it needs no module.)</p>
<h2>4. Texts per language, with a firmer last stage</h2>
<p>Keep templates in a Config node: Hebrew and English, amount formatted with ₪/$/€, due date, days overdue, your payment details (for example bank-transfer info) and the invoice number. The final stage can be firmer but stays polite.</p>
<h2>5. Approve before anything is created</h2>
<p>Send a Telegram card with the full text: Approve / Skip / Pause client. <strong>On Approve, re-check the Sheet</strong>: if the invoice got paid or the client was paused since the card went out, do nothing. Otherwise create a Gmail <strong>draft</strong>. Auto-sending should be an explicit opt-in switch, not the default.</p>
<h2>6. Reports</h2>
<ul>
<li>Weekly aging report per currency: not yet due, 0–30, 31–60, 60+.</li>
<li>Monthly summary: issued, paid, average days late, reminders by outcome. Draft it to your accountant.</li>
</ul>
<p>Limits to know: if you forget to mark an invoice paid, the system will still suggest a reminder (you can Skip it). There's no bank reconciliation here.</p>
<hr />
<p>My packaged version is <strong>Payment Reminders &amp; Receivables Chaser for n8n</strong>: 6 workflows, 10 fictional sample invoices (ILS/USD/EUR), webhook examples and a setup guide. It was tested on self-hosted n8n 2.43.7 and needs n8n reachable over public HTTPS, Sheets + Gmail OAuth and a Telegram bot. It was tested against mocked services, not real accounts. Details: <a href="https://payhip.com/b/jdz7B">https://payhip.com/b/jdz7B</a></p>
]]></content:encoded></item><item><title><![CDATA[Building an AI inbox triage in n8n that never sends an email on its own]]></title><description><![CDATA[By Ilan Pinhasov (Ilan Flows) Most "AI email assistant" demos end with the model replying to customers by itself. For a small business that's the scary…]]></description><link>https://ilanflows.hashnode.dev/building-an-ai-inbox-triage-in-n8n-that-never-sends-an-email-on-its-own</link><guid isPermaLink="true">https://ilanflows.hashnode.dev/building-an-ai-inbox-triage-in-n8n-that-never-sends-an-email-on-its-own</guid><category><![CDATA[automation]]></category><category><![CDATA[No Code]]></category><category><![CDATA[Small business]]></category><category><![CDATA[n8n]]></category><dc:creator><![CDATA[Ilan Flows]]></dc:creator><pubDate>Thu, 22 Oct 2026 13:24:12 GMT</pubDate><content:encoded><![CDATA[<p><em>By Ilan Pinhasov (Ilan Flows)</em></p>
<p>Most "AI email assistant" demos end with the model replying to customers by itself. For a small business that's the scary part: one confident, wrong answer to a lead costs more than the time saved. This is how I structure Gmail triage in n8n so the AI does the sorting and the first draft, and a person stays in charge of anything that leaves the inbox.</p>
<h2>1. Intake: poll, validate, de-duplicate</h2>
<ul>
<li><strong>Gmail Trigger</strong> polling every minute, plus a manual "sweep" path for the existing backlog.</li>
<li>Drop your own addresses, sent items and empty emails before spending an API call on them, and flag no-reply / bulk senders so they never get a draft.</li>
<li><strong>De-duplicate on the Gmail message id.</strong> Store every processed id (a Google Sheets "Triage log" works) and skip ids you've seen. Without this, a re-run of the sweep triages everything twice.</li>
</ul>
<h2>2. One AI call, strict JSON</h2>
<p>Make one OpenAI call per email and ask for a fixed JSON shape, for example:</p>
<pre><code class="language-json">{"category":"lead|support|invoice|spam|other","priority":"urgent|high|normal|low",
 "language":"he|en|other","summary":"...","next_step":"...","draft_status":"drafted|needs_human|not_needed",
 "draft_reply":"...","used_sections":["pricing","hours"],"confidence":0.0}
</code></pre>
<p>Feed it only the most relevant sections of your own <strong>knowledge doc</strong> (prices, hours, policies), not the whole thing. Read the email as plain text and cap its length.</p>
<h2>3. Guards after the model</h2>
<p>This is the step most tutorials skip. In a Code node, reject the output when:</p>
<ul>
<li>the JSON doesn't parse, or the category isn't one of yours;</li>
<li>the draft is empty;</li>
<li>the draft cites <strong>no</strong> section of the knowledge doc that was actually sent to the model.</li>
</ul>
<p>Any of these means no draft at all: the email is flagged "needs a person". Low confidence is shown as a warning instead of being hidden. Be honest about what this does <em>not</em> catch: a draft that cites a section but is subtly wrong still passes. That's why the next step exists.</p>
<h2>4. Human approval in Telegram</h2>
<p>Send a card with the summary and draft and three buttons: Approve / Edit / Reject. Edit = reply to the bot message with new text. Approve creates a <strong>Gmail draft in the same thread</strong>, not a sent email. Check that the tap came from an allowed approver id and that the card isn't stale.</p>
<h2>5. Labels and a tiny CRM</h2>
<p>Apply Gmail labels (AI/Lead, AI/Support, AI/Urgent…), creating them if missing. For lead, support and invoice emails, upsert one row per sender in a Sheets "Contacts" tab: category, status, next step, last contact.</p>
<h2>6. Production basics</h2>
<ul>
<li>Retries on Gmail / Sheets / OpenAI / Telegram nodes.</li>
<li>A separate error workflow that writes a DeadLetter row and alerts you; if OpenAI is down, the email isn't logged, so retrying the failed execution or running the manual sweep (last 3 days) picks it up again. The Gmail trigger won't deliver it a second time.</li>
<li>A daily summary: counts by category, waiting approvals, drafts never sent.</li>
</ul>
<p>Expect misclassifications in the first weeks; check the labels and tune your categories and knowledge doc.</p>
<hr />
<p>If you'd rather not build it from scratch, I packaged my version as <strong>AI Inbox Triage for n8n</strong> (5 workflows, sample knowledge doc and test emails, setup guide). It was tested on self-hosted n8n 2.43.7 and needs n8n reachable over public HTTPS, Gmail + Sheets OAuth, your own OpenAI key (you pay OpenAI per email) and a Telegram bot. It was tested against mocked services, not real accounts, and the AI quality depends on your model and knowledge doc. Details: <a href="https://payhip.com/b/jgKmD">https://payhip.com/b/jgKmD</a></p>
]]></content:encoded></item><item><title><![CDATA[Automating Supplier Invoices in n8n: AI Extraction, VAT Checks and a Human Approval Step]]></title><description><![CDATA[By Ilan Pinhasov (Ilan Flows) Supplier invoices arrive as PDFs in your inbox, photos of receipts, and the occasional newsletter someone forwarded by…]]></description><link>https://ilanflows.hashnode.dev/automating-supplier-invoices-in-n8n-ai-extraction-vat-checks-and-a-human-approval-step</link><guid isPermaLink="true">https://ilanflows.hashnode.dev/automating-supplier-invoices-in-n8n-ai-extraction-vat-checks-and-a-human-approval-step</guid><category><![CDATA[No Code]]></category><category><![CDATA[n8n]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Ilan Flows]]></dc:creator><pubDate>Wed, 21 Oct 2026 16:44:06 GMT</pubDate><content:encoded><![CDATA[<p><em>By Ilan Pinhasov (Ilan Flows)</em></p>
<p>Supplier invoices arrive as PDFs in your inbox, photos of receipts, and the occasional newsletter someone forwarded by mistake. Typing them into a spreadsheet is boring, and handing the whole job to an AI is risky: models misread numbers, and a wrong total that slips through is worse than no automation at all.</p>
<p>This article shows how I structured an invoice pipeline in n8n that uses AI for the tedious part (reading the document) and plain code plus a human tap for the part that matters (deciding what's correct and what gets paid). The template's checks are set up for Israel (18% VAT, ILS, the 9-digit Israeli business-number checksum). The pattern carries over to other countries, but you'd change the VAT rate and currency in Config and replace the ID checksum in code.</p>
<h2>The problem</h2>
<p>A useful invoice pipeline needs to:</p>
<ol>
<li>Accept documents from more than one place (email and upload).</li>
<li>Read both text PDFs and images.</li>
<li>Turn them into structured data without the model inventing fields.</li>
<li><strong>Check</strong> the numbers instead of trusting them.</li>
<li>Never process the same document twice.</li>
<li>Never mark anything paid without a person agreeing.</li>
</ol>
<h2>The architecture</h2>
<p>Four workflows:</p>
<ul>
<li><strong>0 Error Handler</strong>: writes a DeadLetter row to Google Sheets and sends a Telegram alert when anything fails.</li>
<li><strong>1 Intake</strong>: Gmail trigger and upload webhook, extraction, validation and routing.</li>
<li><strong>2 Approvals</strong>: handles the Approve and Reject buttons in Telegram.</li>
<li><strong>3 Monthly Summary</strong>: totals per vendor and VAT, sent to Telegram and drafted in Gmail for your accountant.</li>
</ul>
<p>Each workflow has its own Config node (Sheet ID, tab names, Telegram chat ID); the VAT rate, tolerances and confidence threshold live in the Intake workflow's Config.</p>
<p><strong>Diagram: the four workflows.</strong> Invoices come in from two places: a Gmail inbox checked every minute, or an authenticated upload webhook. Both feed workflow 1 (Intake). Intake sends the document to OpenAI to extract the fields, writes clean documents to the <em>Invoices</em> tab and problem ones to the <em>Review</em> tab in Google Sheets, and messages you in Telegram. Tapping Approve or Reject in Telegram runs workflow 2 (Approvals), which updates the row's status. On the 1st of each month at 08:00 (instance timezone), workflow 3 (Monthly Summary) reads both tabs and posts a summary to Telegram plus a Gmail draft. If anything fails, workflow 0 (Error Handler) writes a row to the <em>DeadLetter</em> tab and sends a Telegram alert.</p>
<ul>
<li><strong>0 Error Handler:</strong> Error Trigger → Config → Fetch Failed Execution (n8n API, optional) → Build Dead-Letter Row → Append to DeadLetter + Telegram Alert</li>
<li><strong>1 Intake:</strong> Gmail Trigger / Intake Webhook → … → AI Extract (LLM) → Validate &amp; Route → Invoices or Review (see the next diagram)</li>
<li><strong>2 Approvals:</strong> Telegram Trigger → Config → Parse Callback → Valid &amp; Authorised? → Find Invoice Row → Check State → Pending? → Row Update → Update Status in Sheet → Answer: Done → Edit Message (remove buttons); with the side answers Answer: No Change and Answer: Not Allowed</li>
<li><strong>3 Monthly Summary:</strong> Monthly Schedule (1st, 08:00) / Run now → Config → Read Invoices → Read Review → Build Summary → Telegram Summary + Send Email or Draft? → Gmail: Send Summary or Gmail: Draft Summary (draft is the default)</li>
</ul>
<h2>Step by step: the node logic</h2>
<h3>Intake</h3>
<ul>
<li><strong>Gmail Trigger</strong> polls the inbox every minute for emails with PDF, PNG or JPG attachments. <strong>Intake Webhook</strong> accepts authenticated multipart uploads, several files at once. Both feed one <strong>Config</strong> node.</li>
<li><strong>Split Attachments</strong> turns each file into its own item.</li>
<li><strong>Read Invoices</strong>, <strong>Read Review</strong> and <strong>Skip Already Processed</strong> implement idempotency with a content hash. If Gmail re-polls or a retry runs, nothing gets duplicated.</li>
<li><strong>From Webhook?</strong> / <strong>Respond to Sender</strong> answers upload callers quickly.</li>
<li><strong>PDF or Image?</strong> splits the path. PDFs go through <strong>Extract PDF Text</strong>. Images (PNG, JPG, WebP) are sent to the model's vision input. <strong>Merge PDF + Image</strong> joins them again.</li>
<li><strong>Prepare LLM Request</strong> and <strong>AI Extract (LLM)</strong> ask the model to fill a strict JSON schema: vendor, business number, invoice number, date, net, VAT, total, currency and a confidence score. A strict schema means the model can't add fields you didn't ask for. The default model is gpt-4o-mini (set in Config). The request goes to the OpenAI API; using another OpenAI-compatible endpoint means editing the URL and credential in the AI Extract node.</li>
<li><strong>Validate &amp; Route</strong> is where the real work happens, in plain code:<ul>
<li>VAT equals net × 18%, within a tolerance you set</li>
<li>net + VAT = total</li>
<li>the 9-digit Israeli business number passes its checksum</li>
<li>required fields are present</li>
<li>vendor + invoice number isn't a duplicate</li>
<li>the AI's confidence is above your minimum</li>
<li>the document is actually an invoice</li>
</ul>
</li>
<li><strong>Approve or Review?</strong> sends clean documents to the <strong>Invoices</strong> sheet with status <code>pending_approval</code> and a Telegram message with Approve and Reject buttons. Problem documents go to the <strong>Review</strong> sheet with the reason, plus a Telegram alert.</li>
</ul>
<h3>Approvals</h3>
<ul>
<li><strong>Telegram Trigger</strong> receives the button tap. <strong>Parse Callback</strong> and <strong>Valid &amp; Authorised?</strong> reject malformed data and taps from people who aren't approvers (a list of Telegram user IDs, or anyone in your configured chat if you leave the list empty).</li>
<li><strong>Find Invoice Row</strong> and <strong>Check State</strong> make sure the invoice is still pending, so a stale re-tap does nothing.</li>
<li><strong>Update Status in Sheet</strong> sets <code>approved_paid</code> or <code>rejected</code> with who tapped and when, and <strong>Edit Message</strong> removes the buttons. This only records the decision in the sheet. The template doesn't pay anything or connect to a bank or accounting software. Nothing is marked paid until you approve it in Telegram.</li>
</ul>
<h3>Monthly Summary</h3>
<p>A monthly schedule (the 1st of each month at 08:00, instance timezone), or a manual run for testing, reads both sheets, builds totals per vendor, VAT totals and status counts, posts them to Telegram and creates a Gmail <strong>draft</strong> for your accountant. Sending directly is a config switch you have to turn on yourself.</p>
<p><strong>Diagram: the Intake node chain.</strong> Left to right: two triggers merge into one Config node. Attachments are split and fingerprinted, and files already in the sheet are dropped. Upload callers get an immediate reply. Then the path splits: PDFs have their text extracted, while images go straight on. The two paths merge, the AI request is built and sent, and the result is checked in code. The last switch sends clean documents to <em>Invoices</em> (pending_approval) with Approve/Reject buttons in Telegram, and everything else to <em>Review</em> with a Telegram alert.</p>
<ol>
<li>Gmail Trigger (polls every minute) / Intake Webhook (POST, header auth)</li>
<li>Config</li>
<li>Split Attachments</li>
<li>Read Invoices → Read Review → Skip Already Processed</li>
<li>From Webhook? → yes: Respond to Sender → Has New Files?; no: Has New Files?</li>
<li>PDF or Image? → pdf: Extract PDF Text → Merge PDF + Image; image: Merge PDF + Image</li>
<li>Prepare LLM Request → Needs LLM? → yes: AI Extract (LLM, OpenAI) → Validate &amp; Route; no: Validate &amp; Route</li>
<li>Approve or Review? → approve: Append to Invoices → Telegram: Ask Approval; review: Append to Review → Telegram: Review Alert</li>
</ol>
<h2>Pitfalls</h2>
<ul>
<li><strong>Don't trust the model's arithmetic.</strong> Have it extract the numbers, then check them in code. That's the whole design.</li>
<li><strong>Scanned PDFs without a text layer</strong> have no text to extract. In this design they end up in Review. No OCR engine is included, so add one if most of your documents are scans.</li>
<li><strong>Idempotency by hash, not filename.</strong> Suppliers resend "invoice.pdf" all the time.</li>
<li><strong>Lock down the approval buttons.</strong> Check the chat ID and the current state, or anyone who can forward a message can mark things paid.</li>
<li><strong>Draft, don't send.</strong> Let a human hit send on the accountant email until you trust it.</li>
<li><strong>Retries are fixed-wait.</strong> n8n's built-in retry here is 3 tries, 3 seconds apart, not exponential backoff.</li>
</ul>
<h2>Testing it</h2>
<p>I tested on self-hosted n8n 2.43.7 against local mocks of Gmail, Sheets, Telegram and OpenAI, with a set of fictional sample documents in Hebrew and English. Cases worth reproducing:</p>
<ul>
<li>one clean invoice per supplier type</li>
<li>a wrong-VAT invoice, a bad business number, and a missing invoice number</li>
<li>an exact duplicate</li>
<li>a low-confidence handwritten receipt</li>
<li>a document that isn't an invoice</li>
<li>a non-JSON reply from the AI</li>
<li>wrong file type, missing file and wrong token on the upload</li>
<li>approve, reject, stale, unauthorised and forged taps</li>
<li>a forced AI outage: retries, then a DeadLetter row and an alert, with nothing half-written</li>
</ul>
<p>One honest caveat: the mock returned correct extractions, so this proves the plumbing, not the AI's accuracy on <em>your</em> documents. Run a batch of your real invoices before relying on it, which the approval step is there to catch anyway.</p>
<h2>Setup</h2>
<p>You'll need: n8n (tested on self-hosted 2.43.7), a Gmail account, Google Sheets, an OpenAI API key (you pay OpenAI's usage costs), and a Telegram bot.</p>
<hr />
<p>If you'd like the finished version (four workflows, sample documents with expected results and a setup guide), it's here: <a href="https://payhip.com/b/Xk4M0">https://payhip.com/b/Xk4M0</a></p>
]]></content:encoded></item><item><title><![CDATA[How to Build a WhatsApp Booking Receptionist in n8n That Doesn't Double-Book]]></title><description><![CDATA[By Ilan Pinhasov (Ilan Flows) If you run a clinic, a studio or any business that lives on appointments, a lot of your WhatsApp traffic is the same handful…]]></description><link>https://ilanflows.hashnode.dev/how-to-build-a-whatsapp-booking-receptionist-in-n8n-that-doesn-t-double-book</link><guid isPermaLink="true">https://ilanflows.hashnode.dev/how-to-build-a-whatsapp-booking-receptionist-in-n8n-that-doesn-t-double-book</guid><category><![CDATA[No Code]]></category><category><![CDATA[n8n]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Ilan Flows]]></dc:creator><pubDate>Wed, 21 Oct 2026 16:42:56 GMT</pubDate><content:encoded><![CDATA[<p><em>By Ilan Pinhasov (Ilan Flows)</em></p>
<p>If you run a clinic, a studio or any business that lives on appointments, a lot of your WhatsApp traffic is the same handful of messages: "Do you have something tomorrow?", "Can I move my Thursday slot?", "Where do I park?". Meta's webhook has quirks, customers write dates a dozen different ways, two people can grab the same slot, and sooner or later someone needs a real human.</p>
<p>Here's how I structured one in n8n to handle those cases.</p>
<h2>The problem, in concrete terms</h2>
<p>A booking bot has to get five things right:</p>
<ol>
<li><strong>The webhook.</strong> Meta verifies your endpoint with a GET request, expects a fast 200 on every POST, signs payloads with <code>X-Hub-Signature-256</code>, sends status callbacks (sent, delivered, read) you don't care about, and can deliver the same message twice.</li>
<li><strong>Understanding the request.</strong> "מחר בארבע וחצי", "next Thursday", "Oct 23rd at 4pm".</li>
<li><strong>Availability.</strong> The slot must actually be free in the calendar, inside opening hours, with enough notice.</li>
<li><strong>Race conditions.</strong> A slot that was free when you offered it may be gone by the time the customer says "yes".</li>
<li><strong>Handoff.</strong> When the bot doesn't understand, it should say so and get a person involved, not guess.</li>
</ol>
<h2>The architecture</h2>
<p>Six workflows, each with its own Config node:</p>
<ul>
<li><strong>0 Error Handler</strong>: catches failures from the others, writes a DeadLetter row to Google Sheets and sends a Telegram alert.</li>
<li><strong>1 Inbound</strong>: the Meta webhook. Verification, signature check, filtering and de-duplication.</li>
<li><strong>2 Conversation Engine</strong>: understands the message, checks Google Calendar, books, moves or cancels, and replies.</li>
<li><strong>3 Staff Telegram</strong>: lets staff reply to customers from Telegram and hand the chat back to the bot.</li>
<li><strong>4 Reminders</strong>: runs every 30 minutes and sends a reminder template before appointments.</li>
<li><strong>5 Daily Summary</strong>: a Telegram summary for the owner every evening.</li>
</ul>
<p><strong>Diagram: the six workflows.</strong> A customer's WhatsApp message reaches Meta's WhatsApp Cloud API, which calls workflow 1 (Inbound) on <code>/webhook/whatsapp</code>. Inbound acknowledges Meta, verifies the signature, drops delivery receipts and duplicates, then calls workflow 2 (Conversation Engine) once per message. The engine reads and saves conversation state and appointments in Google Sheets, checks and writes Google Calendar, replies through the Cloud API, and alerts staff in Telegram on handoff. Staff actions in Telegram (Resume, or a reply) run workflow 3, which sends the reply on WhatsApp. Two scheduled workflows run alongside: workflow 4 checks every 30 minutes for appointments in the next 24 hours and sends an approved reminder template, and workflow 5 posts the owner's daily summary to Telegram at 20:00 (instance timezone). Workflow 0 logs any failure to a DeadLetter tab and alerts Telegram.</p>
<ul>
<li><strong>0 Error Handler:</strong> Error Trigger → Config → Fetch Failed Execution (optional) → Build Dead-Letter Row → Append to DeadLetter + Telegram Alert</li>
<li><strong>1 Inbound:</strong> WhatsApp Verify (GET) / WhatsApp Events (POST) → Config → Verification Request? → (GET) Verify Token OK? → Respond: Challenge or Respond: Forbidden; (POST) Respond: 200 OK → Verify Signature &amp; Parse → Customer Messages Only → Read Processed → Skip Duplicates → Mark Processed → Restore Messages → Run Conversation Engine</li>
<li><strong>2 Conversation Engine:</strong> see the next diagram</li>
<li><strong>3 Staff Telegram:</strong> Telegram Trigger → Config → Parse Admin Update → Resume, Reply or Refuse? → resume: Read Conversation → Resume Row → Save Conversation → Answer: Resumed → Edit Alert; reply: Read Conversation → 24h Window Check → Send to Customer → Confirm to Staff; refuse: Answer: Not Allowed</li>
<li><strong>4 Reminders:</strong> Every 30 Minutes / Run now → Config → Read Appointments → Read Conversations → Due Reminders → Send Reminder Template → Check Results → Sent or Failed? → sent: Row Update → Mark Reminder Sent; failed: Telegram: Reminder Failed</li>
<li><strong>5 Daily Summary:</strong> Every Day 20:00 / Run now → Config → Read Appointments → Read Conversations → Build Daily Summary → Telegram: Daily Summary</li>
</ul>
<h2>Step by step: the node logic</h2>
<h3>Inbound</h3>
<p>Two webhook nodes share the path: <strong>WhatsApp Verify (GET)</strong> and <strong>WhatsApp Events (POST)</strong>. For GET, an IF node checks the verify token and responds with the challenge or a 403.</p>
<p>For POST, the very first thing is <strong>Respond: 200 OK</strong>. Meta retries slow webhooks, so you acknowledge first and work after. Then:</p>
<ul>
<li><strong>Verify Signature &amp; Parse</strong> (a Code node) computes the HMAC-SHA256 of the raw body with your App Secret and compares it to <code>X-Hub-Signature-256</code>. Forged calls are dropped.</li>
<li><strong>Customer Messages Only</strong> discards status callbacks.</li>
<li><strong>Read Processed</strong> and <strong>Skip Duplicates</strong> look up the WhatsApp message id in a Processed sheet, and <strong>Mark Processed</strong> records new ones. That is your idempotency layer.</li>
<li><strong>Run Conversation Engine</strong> hands each new message to workflow 2.</li>
</ul>
<h3>Conversation Engine</h3>
<ul>
<li><strong>Read Conversation</strong> and <strong>Read Appointments</strong> load the state for that phone number from Google Sheets: what was offered and what is waiting for confirmation.</li>
<li><strong>Understand &amp; Decide</strong> is a rule-based Code node. It parses Hebrew and English dates and times and matches your service names and keywords. It understands Hebrew and English only; voice notes, images and documents aren't read. I chose rules over an LLM here on purpose: it is predictable, costs nothing per message, and when it doesn't understand, it knows it doesn't.</li>
<li><strong>Next Step</strong> routes to booking, rescheduling, cancelling, a FAQ answer or a handoff.</li>
<li><strong>Calendar: Free/Busy</strong> and <strong>Pick Slot</strong> find the requested slot or the nearest free alternatives, same day first, respecting opening hours, minimum notice and closed days.</li>
<li>When the customer confirms, free/busy is checked <strong>again</strong> right before <strong>Calendar: Create Event</strong>. This second check is what prevents most double-bookings.</li>
<li>Rescheduling moves the same event (<strong>Calendar: Move Event</strong>). Cancelling deletes it after a confirmation. Every change is mirrored to an Appointments sheet. Bookings go into one Google Calendar (<code>calendar_id</code>); there's no per-staff calendar routing.</li>
<li>FAQ answers come only from your own clinic info, with a whole-word coverage guard. If nothing matches, the bot says it doesn't have a reliable answer and offers a person. Optional AI rephrasing (<strong>AI Answer (LLM)</strong>) only rewords the matched text and falls back to the exact text.</li>
<li><strong>Send WhatsApp Reply</strong> and <strong>Save Conversation</strong> close the loop.</li>
</ul>
<h3>Staff Telegram and handoff</h3>
<p>Handoff triggers on words like "נציג" or "human", urgent keywords, or three misunderstood messages in a row. Staff get a Telegram alert, the bot goes quiet for that chat, and new customer messages are forwarded. Staff reply in Telegram and it goes out on WhatsApp. A <strong>Resume</strong> button gives the chat back to the bot.</p>
<p>A <strong>24h Window Check</strong> node handles WhatsApp's 24-hour rule. Inside the window, the staff text goes out as is. Outside it, an approved template is sent instead and staff are told why.</p>
<p><strong>Diagram: Conversation Engine branches.</strong> The engine loads the customer's state, then a rule-based Code node decides what the message is. A six-way switch routes it: booking goes to a free/busy lookup, then Pick Slot, which offers a time or alternatives. Once the customer says "yes", Pick Slot checks free/busy <strong>again</strong> and only then branches to Create Event (new booking) or Move Event (reschedule). Cancel deletes the event. An FAQ answer goes through the optional AI rephrasing step. Handoff alerts staff in Telegram, and a paused chat just forwards the message to Telegram. Every branch except "paused" ends in one Prepare Reply → Send WhatsApp Reply step, and the conversation state is saved last.</p>
<ol>
<li>From Inbound → Config → Read Conversation → Read Appointments → Understand &amp; Decide (rules, Hebrew + English)</li>
<li>Next Step (switch):<ul>
<li>calendar → Calendar: Free/Busy → Pick Slot (re-checks free/busy before writing) → Create, Move or Reply?<ul>
<li>create → Calendar: Create Event → Booked → Save Appointment → Prepare Reply</li>
<li>move → Calendar: Move Event → Moved → Update Appointment (moved) → Prepare Reply</li>
<li>reply → Prepare Reply</li>
</ul>
</li>
<li>cancel → Calendar: Delete Event → Cancelled → Update Appointment (cancelled) → Prepare Reply</li>
<li>handoff → Telegram: Handoff Alert → Prepare Reply</li>
<li>paused → Telegram: Forward Message → Conversation Row</li>
<li>ai (answer_mode = llm) → AI Answer (LLM) → Use AI Answer → Prepare Reply</li>
<li>reply → Prepare Reply</li>
</ul>
</li>
<li>Prepare Reply → Send WhatsApp Reply → Conversation Row → Save Conversation</li>
</ol>
<h2>Pitfalls I hit, so you don't have to</h2>
<ul>
<li><strong>Don't do work before the 200.</strong> If your webhook runs the whole flow before responding, Meta will retry and you'll get duplicates.</li>
<li><strong>De-duplicate on message id, not content.</strong> People do send "yes" twice on purpose.</li>
<li><strong>Ignore status callbacks early.</strong> Otherwise every outgoing message triggers your engine.</li>
<li><strong>Check availability twice.</strong> Once to offer, once to book. There's still a tiny race window if two bookings land in the same second, but it's very small.</li>
<li><strong>Templates need Meta approval.</strong> Reminders and out-of-window staff replies depend on approved utility templates, and Meta may charge for template messages; check current WhatsApp pricing. Submit them early.</li>
<li><strong>Voice notes and images aren't understood</strong> in this design. The bot asks for text.</li>
<li><strong>Set the timezone.</strong> Set the timezone in your clinic config (and in the Reminders/Summary Config). Also set n8n's <code>GENERIC_TIMEZONE</code> so the 20:00 summary runs at your local time.</li>
</ul>
<h2>Testing it</h2>
<p>I tested on self-hosted n8n 2.43.7 with local mocks of the WhatsApp Cloud API, Google Calendar, Sheets, Telegram and OpenAI, on self-hosted n8n 2.43.7. The cases worth copying into your own test plan:</p>
<ul>
<li>GET verify with the right and the wrong token</li>
<li>a bad or missing signature</li>
<li>the same message delivered twice</li>
<li>a busy slot leading to alternatives and then a booking</li>
<li>a slot taken between the offer and the "yes"</li>
<li>reschedule and cancel</li>
<li>handoff, then a staff reply, then Resume</li>
<li>a staff reply outside the 24-hour window</li>
<li>a forced Google Calendar outage: retries, then one DeadLetter row and an alert, and nothing half-booked</li>
</ul>
<p>What I have <em>not</em> tested: a real Meta app and template approval, real Google and Telegram accounts, and real customer phrasing beyond my test sentences. Test with your own number before you put it in front of customers.</p>
<h2>Setup</h2>
<p>You'll need: n8n (tested on self-hosted 2.43.7) reachable over public HTTPS, a Meta app with the WhatsApp Cloud API (not the regular WhatsApp Business app), Meta-approved utility templates, Google Calendar, Google Sheets and a Telegram bot. An OpenAI key is optional, only for AI rephrasing of FAQ answers. Meta's fees may apply.</p>
<hr />
<p>If you'd rather not build it from scratch, I sell the finished version (six workflows, Hebrew and English sample configs and a setup guide) here: <a href="https://payhip.com/b/BPTjN">https://payhip.com/b/BPTjN</a></p>
]]></content:encoded></item></channel></rss>