DEV Community

Mike Clarke
Mike Clarke

Posted on

The leads were in the table. The automation never saw them.

There were two groups of leads sitting in contacted state. One group was getting follow-up emails, call tasks, the whole sequence. The other group was getting nothing — indefinitely.

Same status. Same table. Different outcomes. No errors anywhere.

Key Takeaways

  • A "dead population path" is when one write route into a table skips a step that downstream automation assumes happened.
  • Bulk imports are the most common source of this. They write directly to the final state, skipping every intermediate code path.
  • The fix is not patching the import. It's a trigger that enforces enrollment regardless of how a row arrives.
  • For every table your automation reads: list every way rows enter it, then verify each path runs the prerequisites downstream depends on.

What was actually happening

The CRM pipeline in ARIA works like this: a lead gets contacted, the first-contact handler fires, it enrolls the lead in the follow-up sequence. That enrollment step lived exclusively inside the first-contact code path.

User action / API call
  → first-contact handler
      → write lead to `contacted` state
      → enroll in follow-up sequence  ← this is the step
Enter fullscreen mode Exit fullscreen mode

Someone ran a bulk import. It wrote records straight to contacted. No handler. No enrollment.

Bulk import
  → write lead to `contacted` state
                                      ← nothing
Enter fullscreen mode Exit fullscreen mode

The downstream automation that sends follow-ups reads the leads table and looks for enrolled leads in contacted state. The imported leads were in the right table, in the right state, but they were never enrolled. The automation skipped them every single cycle — correctly, by its own logic.

No error. No warning. Just silence.

The gap never closed because there was no reconciliation job. Nothing periodically asked "are there contacted leads that aren't in the sequence?" The imported population just sat there.


Why this is easy to miss

The table looked fine. The leads looked fine. The automation looked fine.

The bug was architectural: there were two write routes into one table, and only one of them ran the setup step the other parts of the system depended on. The system had no way to detect the discrepancy because it never compared "who should be enrolled" against "who is enrolled." It just processed whoever was already enrolled.

This is what I'd call a dead population path. The rows exist. The automation is running. The rows are just invisible to it.

Bulk imports create this constantly because they're designed to skip your application layer — that's literally their value proposition. The problem is your application layer is often where side effects live.


The fix: enforce enrollment at the database layer

The only reliable fix is moving the enrollment check to a place that runs regardless of how the row was written. Postgres triggers do this.

-- The trigger function
CREATE OR REPLACE FUNCTION enroll_contacted_lead()
RETURNS TRIGGER AS $$
BEGIN
  -- Only act on rows entering or already in 'contacted' state
  IF NEW.status = 'contacted' THEN
    -- Check whether this lead qualifies for enrollment
    IF NEW.assigned_to IS NOT NULL AND NEW.email IS NOT NULL THEN
      -- Enroll if not already enrolled
      INSERT INTO follow_up_enrollments (lead_id, enrolled_at, sequence_id)
      VALUES (NEW.id, now(), NEW.sequence_id)
      ON CONFLICT (lead_id) DO NOTHING;
    ELSE
      -- Log why it was skipped so you can find these later
      INSERT INTO enrollment_skipped_log (lead_id, reason, skipped_at)
      VALUES (
        NEW.id,
        CASE
          WHEN NEW.assigned_to IS NULL THEN 'no_assignee'
          WHEN NEW.email IS NULL THEN 'no_email'
          ELSE 'unknown'
        END,
        now()
      )
      ON CONFLICT (lead_id) DO NOTHING;
    END IF;
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Attach it to both INSERT and UPDATE
-- so it catches bulk imports (INSERT) and status changes (UPDATE) alike
CREATE TRIGGER trg_enroll_contacted_lead
AFTER INSERT OR UPDATE OF status ON leads
FOR EACH ROW
EXECUTE FUNCTION enroll_contacted_lead();
Enter fullscreen mode Exit fullscreen mode

A few things worth noting about this implementation:

ON CONFLICT DO NOTHING on the enrollment insert means existing correctly-enrolled leads are untouched. Safe to run on a table that already has good data.

The skip log is not optional. If a lead doesn't qualify, you want to know why. Without it, you're back to silent failures. The log gives you a query you can run to find every lead that got skipped and the reason.

AFTER INSERT OR UPDATE OF status covers both paths. The bulk import fires INSERT. A status change through the application fires UPDATE. Both paths now run the same enrollment logic.

In Supabase, you apply this in the SQL editor or through a migration file. No special Supabase API needed — it's just Postgres.


Backfilling the existing gap

The trigger handles new rows. But the affected leads already in the table needed a one-time backfill.

-- Enroll any existing contacted leads that slipped through
INSERT INTO follow_up_enrollments (lead_id, enrolled_at, sequence_id)
SELECT
  l.id,
  now(),
  l.sequence_id
FROM leads l
LEFT JOIN follow_up_enrollments fe ON fe.lead_id = l.id
WHERE
  l.status = 'contacted'
  AND fe.lead_id IS NULL           -- not already enrolled
  AND l.assigned_to IS NOT NULL    -- qualifies
  AND l.email IS NOT NULL;         -- qualifies
Enter fullscreen mode Exit fullscreen mode

Run this once after deploying the trigger. Then the trigger maintains the invariant going forward.


The question to ask about every table your automation reads

What are all the ways rows get into this table, and does each one run the steps downstream assumes happened?

For most tables the answer is one or two paths and they're all through your application code. Fine.

But the moment you add a bulk import, a migration script, a database-level upsert from an external tool, or a direct admin write — you've added a path that bypasses your application layer. Any side effects that lived there are now missing for those rows.

The fix isn't to never import. It's to stop trusting that write path determines setup steps. Push the invariants down to the database, where they apply to every write regardless of origin.


Honest lesson

This was a silent failure that looked like working software. The table had data. The automation was running. The logs showed no errors. The only way to notice was to ask why two groups with the same status were having different outcomes — and actually go find the answer.

AI systems like ARIA that run autonomously are more exposed to this class of bug, not less. There's no human checking each lead. The automation either processes them or it doesn't, and if it doesn't, you need something in the system that catches the gap. A trigger logging skipped records is a simple form of that. A reconciliation job that periodically diffs expected vs actual enrollment is a stronger one.

Both beat finding out months later when a customer asks why no one followed up.


— Mike Clarke, founder of Elevare Digital.

Top comments (0)