<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zhang Lixiang</title>
    <description>The latest articles on DEV Community by Zhang Lixiang (@zhangautomation).</description>
    <link>https://dev.to/zhangautomation</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3844803%2F271dae11-c93e-4315-b6c4-04274ef2a3d5.png</url>
      <title>DEV Community: Zhang Lixiang</title>
      <link>https://dev.to/zhangautomation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhangautomation"/>
    <language>en</language>
    <item>
      <title>n8n orchestrates, LangGraph writes, a human approves: an AI cold-email pipeline that can't spam</title>
      <dc:creator>Zhang Lixiang</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:11:37 +0000</pubDate>
      <link>https://dev.to/zhangautomation/n8n-orchestrates-langgraph-writes-a-human-approves-an-ai-cold-email-pipeline-that-cant-spam-2o8j</link>
      <guid>https://dev.to/zhangautomation/n8n-orchestrates-langgraph-writes-a-human-approves-an-ai-cold-email-pipeline-that-cant-spam-2o8j</guid>
      <description>&lt;p&gt;"AI SDR" demos usually end with an agent blasting a few hundred emails. In real B2B sales that burns your domain in a week, and the emails are full of claims nobody can back up.&lt;/p&gt;

&lt;p&gt;I built a lead-generation pipeline for an export manufacturer with three hard rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every claim in an email must come from sourced data&lt;/strong&gt;, quoted from what the APIs returned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rule checker rejects bad drafts&lt;/strong&gt; and sends them back for a rewrite (max 2 times).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing is sent until a human clicks Approve.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how the work is split, which I think is the useful part.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n does the plumbing, LangGraph does the thinking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;n8n (55 nodes)&lt;/strong&gt; handles everything deterministic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggers (manual, schedule, webhook)&lt;/li&gt;
&lt;li&gt;Apollo for finding people and enriching emails, Hunter for verifying them&lt;/li&gt;
&lt;li&gt;Dedupe against what is already in the CRM, qualification filters&lt;/li&gt;
&lt;li&gt;HubSpot (or a Google Sheet) as the CRM&lt;/li&gt;
&lt;li&gt;An approval link per draft, an unsubscribe link, and a suppression list checked again right before sending&lt;/li&gt;
&lt;li&gt;SendGrid (or Gmail / SMTP) to send, behind a &lt;code&gt;SEND_ENABLED&lt;/code&gt; flag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A LangGraph service (FastAPI)&lt;/strong&gt; does the parts that need a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;research -&amp;gt; write -&amp;gt; check -&amp;gt; (rewrite, max 2) -&amp;gt; draft for approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;research&lt;/strong&gt; writes a short note about the company, and every reason must carry a verbatim quote from the lead data, or it is cleared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;write&lt;/strong&gt; drafts the email from that note only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;check&lt;/strong&gt; is plain code, not another LLM: it blocks numbers that are not in the research or approved facts, unsupported claim phrases, placeholders and empty fields, a missing unsubscribe line, and over-long drafts.&lt;/li&gt;
&lt;li&gt;Failing drafts go back to a &lt;strong&gt;rewrite&lt;/strong&gt; step with the checker's reasons. After two failed rewrites the draft is flagged &lt;code&gt;needs_human_fix&lt;/code&gt;, never sent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is any OpenAI-compatible endpoint (OpenAI, DeepSeek, Qwen, OpenRouter), so you are not locked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the checker is code, not an LLM
&lt;/h2&gt;

&lt;p&gt;An LLM grading an LLM tends to agree with itself. A regex and a list of allowed numbers does not. The checker is boring on purpose, and it is the reason I trust the drafts enough to show them to a salesperson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry is a JSON file
&lt;/h2&gt;

&lt;p&gt;Target industry, buyer type, product lines and selling points live in one profile file, and a small script turns it into the n8n variables. The kit ships with a battery-storage example and an LED-lighting example to show how to swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;21 pytest tests for the LangGraph service, 8 JS tests for the n8n Code nodes&lt;/li&gt;
&lt;li&gt;A live run against a real n8n instance (1.123.87) with local mock Apollo / Hunter / HubSpot / SendGrid: 26/26 checks pass&lt;/li&gt;
&lt;li&gt;10 fake leads end to end: 6 drafts waiting for approval, 2 disqualified, 1 deduped, 1 blocked by the unsubscribe list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be clear: these runs used mocked vendor APIs and a deterministic fake model. You bring your own Apollo, Hunter, HubSpot, SendGrid and LLM keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the template
&lt;/h2&gt;

&lt;p&gt;The full kit (importable n8n workflow, LangGraph + FastAPI service, Docker Compose, tests, sample profiles) is here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://zhangautomation.gumroad.com/l/lwnqd" rel="noopener noreferrer"&gt;B2B AI Lead Generation: n8n + LangGraph template, $29&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I also wrote about a WhatsApp bot where the LLM is only allowed to pick an FAQ id: &lt;a href="https://zhangautomation.gumroad.com/l/ecvlr" rel="noopener noreferrer"&gt;WhatsApp AI Customer Service Bot kit, $19&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions about splitting work between n8n and LangGraph are welcome in the comments.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>langgraph</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>How I stopped my n8n WhatsApp bot from inventing prices (the "LLM picks an ID" pattern)</title>
      <dc:creator>Zhang Lixiang</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:06:50 +0000</pubDate>
      <link>https://dev.to/zhangautomation/how-i-stopped-my-n8n-whatsapp-bot-from-inventing-prices-the-llm-picks-an-id-pattern-4p1j</link>
      <guid>https://dev.to/zhangautomation/how-i-stopped-my-n8n-whatsapp-bot-from-inventing-prices-the-llm-picks-an-id-pattern-4p1j</guid>
      <description>&lt;p&gt;Most "AI WhatsApp bot" tutorials hand the customer's message to an LLM and send back whatever it writes. That works great in a demo and terribly for a real small business: the model will happily quote a price, a warranty, or an opening hour that does not exist.&lt;/p&gt;

&lt;p&gt;I built a WhatsApp FAQ bot for small service businesses (the sample data is an aircon-servicing company) and made one rule: &lt;strong&gt;the LLM never writes the reply.&lt;/strong&gt; Here is the pattern, which you can reuse in any n8n workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: the LLM picks an ID, never the words
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep the FAQ in a table (Google Sheet or CSV): &lt;code&gt;id, question, keywords, answer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;First try plain keyword scoring. Most messages ("how much for a general service?") match without any AI at all, which is fast and free.&lt;/li&gt;
&lt;li&gt;Only when nothing matches, send the question plus the list of FAQ ids and questions to the LLM, and ask for &lt;strong&gt;one id or &lt;code&gt;null&lt;/code&gt;&lt;/strong&gt;, nothing else.&lt;/li&gt;
&lt;li&gt;Validate: if the response is not exactly a known id, treat it as "unknown".&lt;/li&gt;
&lt;li&gt;Send the stored answer for that id, &lt;strong&gt;word for word&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Unknown, &lt;code&gt;null&lt;/code&gt;, free text, or an API error all go to a &lt;strong&gt;human handoff&lt;/strong&gt;: the customer gets one "a staff member will reply" message, the owner gets a WhatsApp alert, and the bot stays silent in that chat until a human resumes it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: the worst case is "a human answers", never "the bot made up a price".&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer message -&amp;gt; Webhook (GET verify / POST messages)
  -&amp;gt; X-Hub-Signature-256 HMAC check (bad signature -&amp;gt; 401, nothing processed)
  -&amp;gt; Dedupe on message id (Meta retries webhooks)
  -&amp;gt; STOP / START opt-out handling
  -&amp;gt; Chat in human handoff? -&amp;gt; stay silent
  -&amp;gt; Asked for "agent / human" or sent a photo/voice note? -&amp;gt; handoff
  -&amp;gt; FAQ match (keywords) -&amp;gt; fallback: LLM picks an FAQ id
  -&amp;gt; Reply via Graph API POST /{phone_number_id}/messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things that bit me and are worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify the signature before anything else.&lt;/strong&gt; Meta signs every webhook with your app secret. In n8n you need the raw body for the HMAC, so turn on the raw body option in the Webhook node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe on the message id.&lt;/strong&gt; Meta retries if you are slow, and without dedupe a customer gets the same answer two or three times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opt-out is not optional.&lt;/strong&gt; STOP / START with a confirmation message, and opted-out chats get nothing, including reminders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep Code node logic in separate &lt;code&gt;.js&lt;/code&gt; files&lt;/strong&gt; and generate the workflow JSON from them. That way you can unit test the logic outside n8n, which is how I got the workflow to pass a 16/16 live run against a real n8n instance with a mocked Graph API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Optional: booking reminders
&lt;/h2&gt;

&lt;p&gt;The same workflow can send an approved WhatsApp &lt;em&gt;template&lt;/em&gt; to customers whose booking starts within 24 hours, once per booking, skipping anyone who opted out. Outside the 24-hour customer-service window, WhatsApp only allows template messages, so this has to be a template.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want the finished version?
&lt;/h2&gt;

&lt;p&gt;I packaged the full thing as a kit: the importable n8n workflow, an equivalent Python FastAPI service (SQLite state, 31 pytest tests), Docker + Caddy HTTPS setup, and a step-by-step Meta Cloud API guide.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://zhangautomation.gumroad.com/l/ecvlr" rel="noopener noreferrer"&gt;WhatsApp AI Customer Service Bot kit, $19&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are doing B2B outreach instead, I also have an n8n + LangGraph lead-generation template where nothing is sent until you click Approve: &lt;a href="https://zhangautomation.gumroad.com/l/lwnqd" rel="noopener noreferrer"&gt;B2B AI Lead Generation kit, $29&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the "LLM picks an ID" pattern in the comments, it works for far more than WhatsApp.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>ai</category>
      <category>whatsapp</category>
    </item>
  </channel>
</rss>
