A lead generation and cold outreach pipeline targeting German real estate companies (developers + brokers, starting in Leipzig). Fully automated from discovery to first reply — once a lead responds, automation stops and the conversation is handed off to Huy Ngo (Sales Lead, NEG AG).
Stack: n8n (orchestration), NocoDB (master data + analytics), Pipedrive (CRM), Google Maps Places API (source), Crawl4AI + SearXNG + Hunter.io (enrichment), SMTP with tracking pixel (outreach), IMAP (reply listener).
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ WF-1 │ │ WF-2 │ │ WF-3 │
│ Google Maps │───►│ Enrich Lead │───►│ Pipedrive Sync │
│ Scraper │ │ (crawl website, │ │ (create Contact │
│ → NocoDB (raw) │ │ extract email) │ │ + Deal) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ WF-5 │◄───│ WF-4 │
│ Reply Handler │ │ Email Outreach │
│ → stop + │ │ (4-stage German │
│ notify Huy │ │ sequence) │
└─────────────────┘ └─────────────────┘
Every step writes to NocoDB. Errors go to an error log table.
Send / delivered / reply / booking events feed the analytics table.
No tracking pixel — first-party events only (GDPR/TTDSG safe).
Trigger: Daily 06:00
Job: Query Google Places API across 50 German cities × 5 search terms (Immobilienmakler, Projektentwickler, Bauträger, Immobilienentwickler, Hausverwaltung). Runs 10 queries/day → full Germany rotation every 25 days. Each place is saved to NocoDB with enrichment_status = pending.
Dedup: by Google place_id.
Writes: NocoDB leads table (raw row per company).
Cost: ~$10/month.
Trigger: Runs every 15 minutes, picks up leads where enrichment_status = pending.
Job: For each lead, enrich contact info:
/impressum, /kontakt, /imprintenrichment_status = enriched (or failed)Writes: updates leads row with email, phone, contact_person, website, type, impressum_content.
Trigger: Runs every 15 minutes, picks up leads where enrichment_status = enriched AND pipedrive_deal_id IS NULL.
Job: For each qualified lead:
type, stage = "New"pipedrive_org_id, pipedrive_person_id, pipedrive_deal_id back to NocoDBOnly leads with a valid email proceed. Everything else stays in NocoDB for manual review.
Trigger: Weekdays 09:00
Job: Pull leads ready for their next stage. LLM generates a personalized German email using company context (Impressum, website, type). Send via SMTP. Log email_sent event to analytics. Update lead state.
4-stage sequence:
| Stage | Day | Tone |
|---|---|---|
| 1 | 0 | Cold opener, value prop, soft CTA |
| 2 | +3 | Follow-up with market insight |
| 3 | +7 | Direct, "letzte Nachricht" |
| 4 | +14 | Breakup, door open |
Rate limit: 30 emails/day across all stages (SMTP warm-up). Random 20–60s delay between sends.
Filter: only send if last_email_date != today (prevents double-send on re-runs).
GDPR: opt-out link + NEG AG Impressum in every email footer.
Trigger: IMAP inbox listener (always-on) + Cal.com booking webhook
Job on any incoming reply:
reply_status = received, reply_date = nowoutreach_stage = handed_offNo LLM classification, no auto-reply. Every reply goes to Huy to handle personally.
Cal.com webhook branch: on booking → move Pipedrive deal to "Meeting Booked", create Pipedrive activity, update NocoDB with meeting_date, log to analytics.
Single row per company. Key fields:
Append-only event log. One row per tracked event.
| Field | Purpose |
|---|---|
| lead_id | FK to leads table |
| event_type | lead_created / enriched / pipedrive_synced / email_sent / email_delivered / email_bounced / reply_received / meeting_booked / error |
| stage | stage_1 / stage_2 / stage_3 / stage_4 (where relevant) |
| workflow | WF-1 … WF-5 |
| metadata | JSON — SMTP response, bounce reason, error message, etc. |
| created_at | timestamp |
NocoDB views on top aggregate: send volume per stage, bounce rate, reply rate by type, conversion funnel, drop-off per workflow.
| Field | Purpose |
|---|---|
| workflow | WF-1 … WF-5 |
| node | n8n node name that failed |
| lead_id | FK if applicable |
| error_message | raw error |
| input_data | JSON of the failing item (for replay) |
| retry_count | 0–3 |
| resolved | bool |
| created_at | timestamp |
Decision: we do not embed tracking pixels or wrap outbound links. German law makes this a bad tradeoff:
Instead, we use first-party signals only. These don't touch the recipient's device and don't require consent:
| Signal | Source | Logged as |
|---|---|---|
| Sent | WF-4 SMTP send success | email_sent |
| Delivered | SMTP server confirmation | email_delivered |
| Bounced | Bounce-inbox listener (IMAP on the bounce address) | email_bounced |
| Replied | WF-5 IMAP listener | reply_received |
| Meeting booked | Cal.com webhook | meeting_booked |
These four events cover the funnel that actually matters: did it land, did they answer, did they book. Open rate and click rate are intentionally out of scope.
Cal.com already provides its own booking analytics (visit → booking conversion) on their dashboard — use that for CTA effectiveness instead of wrapping the link.
Every workflow follows the same shape so they stay readable and recoverable:
enrichment_status = failed (or equivalent per workflow), keep moving with the rest of the batchRule: a single bad lead never kills a whole run.
analytics and errors tables with fields aboveemail_bounced eventslast_email_date != today filter to WF-4 query so double-runs don't double-sendhuy.ngo@neg.ag, signed as "Huy Ngo, NEG AG"searxng:8080, crawl4ai:11235), not localhost