I built a set of four n8n templates around one job: checking whether email addresses are worth sending to, before they go into a sequence, a CRM, or an agent’s hands.
The templates
Google Sheets lead list - reads rows with no verdict, verifies in batches, writes status / score back. Re-runs only pick up new rows, so it’s safe on a schedule.
Clean a CSV before cold email - upload an Apollo / Instantly / HubSpot export, get back three lists: send_safe, review, drop. Greylisted addresses wait and get retried once instead of being thrown away.
Signup webhook - returns allow | review | block, with the policy in a single Code node so you can make it stricter or looser.
AI agent tool - an AI Agent with the verifier as its only tool, and a system prompt that tells it what each result means and what to do with it.
A few details that took longer than expected
Results are matched back by address, not row order. The verifier groups addresses by domain, so they come back reordered; matching by position silently writes the wrong verdict on the wrong row.
unknown and undeliverable are kept apart. “We couldn’t find out” (greylisting, timeouts) is not “this mailbox is dead”, and merging them deletes real contacts.
Gmail / Outlook / Yahoo / iCloud accept any address at SMTP time, so those come back as risky / free_provider_unverifiable rather than a fake “valid”. Same for catch-all domains.
Every record has the same shape: status is always one of four values and all 11 checks keys are always present. No ?? false scattered through your Code nodes.
Everything uses the core HTTP Request node (no community node) and a Header Auth credential, so nothing secret lives in the JSON. Chinese versions of all four are included.
Also in the repo: an OpenAPI file (imports into Dify, Coze, GPT Actions), MCP config, and llms.txt.
Disclosure: the templates are MIT, but they call an email-verification Actor I built and run on Apify, which is pay-per-verified-address (there’s a free tier).
Great work! Matching verification results by email address instead of row order is an important reliability detail. I also focus on idempotency, data validation, and failure handling in my n8n + Python + PostgreSQL projects. Have you tested these templates across different n8n versions, especially credential handling and retry behavior?
Yes, absolutely! It fits right into a Lusha enrichment pipeline.
Enrichment platforms like Lusha, Apollo, or ZoomInfo often provide pattern-guessed or slightly outdated work emails, so running verification right after enrichment is the safest way to keep your cold outreach bounce rate under 2%.
You can use it with Lusha in two ways depending on your setup:
Batch/File Workflow (Recommended): If you export contacts from Lusha into a CSV or Google Sheet, import either t1-google-sheets.json or t3-clean-csv.json. It will chunk them into batches and separate the leads into send_safe (confirmed mailbox), review (catch-all / role-based accounts), and drop (dead mailboxes).
Real-time Pipeline in n8n: If you are calling the Lusha API/node inside an n8n workflow, simply place the HTTP Request node for the verifier immediately after Lusha returns the email: Lusha Node -> HTTP Request (Verify Email) -> IF (status === 'deliverable') -> Push to CRM / Sequencing tool (Instantly, Lemlist, HubSpot)
Because the template matches verdicts by email and normalized_email rather than row index, any missing or null fields from Lusha won’t cause row misalignment.
Thanks @Shital_Katre! Really appreciate the feedback — especially around matching by address. With connection reuse and domain grouping under the hood, positional mapping would silently corrupt lead lists, so keyed lookup on email + normalized_email (to handle Gmail aliases/dots) was a non-negotiable requirement.
Regarding your questions on versions, credentials, and retries:
Version Compatibility:
Tested across n8n 1.x (including 1.40+ through current stable releases). All templates strictly use core built-in nodes (n8n-nodes-base.httpRequest, n8n-nodes-base.code v2, standard Google Sheets v4.5). There are zero custom community nodes, so imports work cleanly out of the box on both self-hosted Docker and n8n Cloud without dependency mismatches.
Credential Handling:
Templates use a standard n8n Header Auth credential named Apify API (Authorization: Bearer <token>). No API tokens or secrets are ever stored in the workflow JSONs. When imported into a fresh n8n instance, n8n automatically highlights the credential field and prompts you to select or create that single Header Auth once.
Retry & Failure Behavior:
Transport level (Network / 5xx): Handled via the HTTP Request node’s built-in “Retry on Fail” settings for transient timeouts.
Protocol level (SMTP Greylisting): This is where most verifiers fail — in t3-clean-csv.json, addresses flagged with status: "unknown" and sub_status: "greylisted" are explicitly not dropped. The workflow isolates them, pauses via a Wait node (default 60 mins), and re-verifies once before finalizing the verdict.
Idempotency: Batch chunking deduplicates addresses via a seen set in the Code node, ensuring you never double-pay or verify the same address twice in a single execution.
If you test this alongside your PostgreSQL stack, I’d love to hear how it performs or if you hit any edge cases on your specific n8n setup!
Thanks for the detailed explanation! The keyed matching and greylisting retry logic sound useful.
One edge case I’d like to explore is duplicate prevention across separate or concurrent executions. Since the seen set is execution-local, a persistent idempotency mechanism may be needed to prevent repeated verification API calls across runs.
I’d also be interested in testing rate-limit responses, exhausted retries, and partial batch failures with synthetic data. If you can share the repository or template links, I’d be happy to review the workflow and report any reproducible edge cases alongside my n8n + PostgreSQL setup.
The in-memory seen Set in the Code node is deliberately kept execution-local to deduplicate within the active batch slice.
For cross-execution idempotency, the design delegates state to the source of truth rather than adding external cache dependencies:
In the Google Sheets template (t1), it checks if (row.status) continue; — meaning any previously verified row is skipped on subsequent/scheduled runs.
In a PostgreSQL pipeline, the idiomatic pattern is querying unverified records (e.g., SELECT email FROM leads WHERE verified_at IS NULL LIMIT 100 FOR UPDATE SKIP LOCKED), and writing back status and score transactionally.
Keeping persistent state in the database/sheet keeps the n8n templates stateless, lightweight, and zero-dependency (no Redis required). For concurrent webhook triggers, n8n’s execution settings (Wait node or workflow concurrency controls) prevent race conditions.
2. Partial Batch Failures & Error Handling
One core architectural choice of the API is that individual mailbox failures never throw a 500 for the whole batch.
If 1 out of 100 domains hits a timeout or network anomaly, the batch still returns HTTP 200 with 100 structured objects:
The 99 working addresses get their standard deliverable / undeliverable verdicts.
This allows n8n to process the response cleanly without complex rollback logic.
I’d be thrilled to have you test it with synthetic data and your n8n + PostgreSQL setup! Feel free to report any edge cases you uncover or open an issue/PR on the repo.
Thanks, Sumitr! I’ll look at these edge cases using synthetic data. Could you share the repository or workflow JSON links so I can reproduce the tests? If you need help with further testing or implementation, I’d also be open to discussing a small paid task.