Hey everyone,
I just finished a production-ready AI lead generation workflow in n8n and wanted to share the full architecture with the community. Happy to answer questions and hear how you'd improve it.
🔧 What it does (end-to-end):
- You send a search query to a Telegram bot (e.g. "IT consulting firms Amsterdam")
- Google Maps Places API returns up to 10 businesses
- Leads are split, deduplicated across runs (persistent memory), and paginated
- Prepare Keys node enriches each item with company_name, website, phone, address and generates a company_key for merging
- Two parallel Serper searches run simultaneously:
- Two sequential Merge nodes combine everything by company_key
- Edit Fields normalizes all data into a clean unified object
- Research Agent (GPT-4o-mini) generates 3 automation opportunities + personalized icebreaker per company
- JSON Cleaner (Code node) parses the AI response and merges it with contact data
- Contact Validator scores email/LinkedIn quality (0–100)
- Lead Scoring calculates total score: corporate email (+40), LinkedIn (+25), website (+15), phone (+10), icebreaker quality (+10)
- IF Score gates the outreach — only leads ≥ 55 proceed
- Google Sheets Append logs everything to CRM
- Email Generator (GPT-4o-mini) writes a personalized cold email
- Gmail creates a draft → waits 3 days → follow-up draft created → Telegram report sent
🐛 Interesting bugs I hit and fixed:
- $json context loss after Serper nodes — fixed by using $('Prepare Keys').item.json.company_key explicitly
- websiteUri returning undefined for businesses without a website — fixed with || $json.nationalPhoneNumber || 'unknown' fallback in the key
- JSON Cleaner failing with raw.slice is not a function — GPT-4o-mini returns a parsed object, not a string, when json_object mode is enabled
- Merge nodes not joining — caused by null company_key on one input branch
- Remove Duplicates discarding all items — cleared deduplication history after test runs
- 💡 Key design decisions:
- Used company_key = displayName.text + '_' + (websiteUri || phone || 'unknown') as a composite dedup key
- All Serper queries include city/country extracted from formattedAddress for geo-accuracy
- JSON Cleaner checks typeof content before parsing — handles both string and pre-parsed object responses from OpenAI node
- Error handler workflow connected globally — sends SOS to Telegram on any node failure
Would love to hear your thoughts — especially on the scoring logic and whether you'd handle the Merge differently. Drop your questions below 👇
#n8n #automation #leadgeneration #AI #GPT4 #workflow #nocode #lowcode