DEV Community

Cover image for We Turned 5 Scattered Scripts into One Lead Enrichment Workflow
The Unmeshed Team
The Unmeshed Team

Posted on

We Turned 5 Scattered Scripts into One Lead Enrichment Workflow

Most signup bugs aren't really about signups. They're about what happens first. A lead comes in, and if the system tries to enrich it before saving it, pull company data, verify the email, then saves the result, that's a problem waiting to happen. The enrichment API has one bad five minutes and every signup from that window just disappears, not stored, not logged, gone, because "enrich it" quietly became a requirement for "save it" instead of the other way around. We hit our own version of this building a lead pipeline: validate, check the email, classify, send good leads to Slack, all steps talking to something outside our control, which meant the real first decision wasn't picking a vendor. It was deciding what happens the second a signup shows up, before anything outside gets called.

The shape of it

signup payload
  -> validate_input
  -> postgres_insert_user
  -> http_hunter_domain_search
  -> http_kickbox_verify
  -> classify_lead
  -> switch_lead_route
      -> slack_message_high_value | exit_skip_slack_low_value
  -> postgres_update_user_enrichment
Enter fullscreen mode Exit fullscreen mode

The order is the whole design. The lead gets saved before any external call runs, so a slow or down enrichment vendor never blocks the signup. Slack only hears about leads that actually qualify. Supabase gets updated last, once everything's known.

The steps that matter

Validate and clean the input. Trim, lowercase the email, check the format, pull the domain. Bad payloads fail here, not three steps downstream.

Save the lead first.

INSERT INTO users (email, full_name)
VALUES (:#email, :#full_name)
RETURNING email;
Enter fullscreen mode Exit fullscreen mode

Supabase is just Postgres underneath, so this is a standard Postgres integration. The row exists before any third party gets asked for anything.

Enrich with Hunter, but make it optional.

https://api.hunter.io/v2/combined/find?email={{steps.postgres_insert_user.output.results[0].email}}&api_key={{secrets.hunter_io_api_key}}
Enter fullscreen mode Exit fullscreen mode

Marked optional on purpose. Company data is nice to have. It should never be able to block a signup.

Verify the mailbox with Kickbox. The signal that matters is one flag: is the address free or disposable. That becomes the main input to the next step.

Classify the lead. One rule for now: free email means low_value, anything else means high_value. Deliberately simple. Company size, industry, or CRM data can slot in here later without touching the rest of the workflow.

Route to Slack based on the classification.

high_value  -> slack_message_high_value
low_value   -> exit_skip_slack_low_value
Enter fullscreen mode Exit fullscreen mode

Notification logic lives in the workflow, not buried in application code.

Write the result back to Supabase.

UPDATE users
SET company = :#company, is_free_email = :#is_free_email, lead_type = :#lead_type
WHERE email = :#email
RETURNING email;
Enter fullscreen mode Exit fullscreen mode

Same row the signup started with, now holding the full outcome.

Why this beats the scattered version

Not because scripts and queues can't do this. They can. The problem is how long that setup stays readable once a few more people have touched it. One workflow definition means the whole lead lifecycle lives in one place, every step has inspectable inputs and outputs, secrets stay managed instead of scattered, and routing logic is something you can actually read.

Natural next steps

  • Fold company size or industry into classify_lead for better scoring
  • Push high-value leads into a CRM in parallel with the Slack alert
  • Add retry policies around the Hunter and Kickbox calls
  • Log every outcome to an audit table for reporting

Because it's definition-driven, these are additions, not rewrites.

Top comments (0)