Training · learner work

Every Lead Has a History. Make It Possible to Follow.

Can an operator explain a lead's current state and name its next scheduled action? In an admissions team's calling system, the console in use today, at one reading on 18 Sep 2026, named the same next action in both of its lead views for open leads with pending work, but it splits the rest of the history across three views, keeps the reason for a campaign switch without showing it, and leaves call-level work out of its trace. An AI Systems Architect at Colaberry built the records and views behind that history.

verified

All student projects
How one lead's history is followed, in 105 seconds. Narrated by a synthetic voice; the figures it states are the verified metrics recorded below.
Industry
Education
Capability
Operational AI lead journey and traceability
Status
Shipped
Built by
Colaberry team
Published
2026-09-19

The situation

An admissions team's calling system decides, lead by lead, when to call, text or email and which campaign a lead belongs to. The question an operator has to answer is simple to ask: can they explain this lead's current state and identify its next scheduled action? The lead's status and campaign name say where it stands. They do not say how it got there: the calls, the messages, the move from one campaign to another, or what the system has scheduled next.

Those records exist, in separate tables: calls in one, messages in another, scheduled jobs in a third, and campaign switches on the audit log. An operator asked why a lead sits in Cold Lead, or when it will next be called, has to find the lead and then join its records by hand.

What it had to do

  • Find one lead from the identity the operator has, its phone number or its CRM id.
  • Show what happened to the lead in order, and say why its campaign changed where the system recorded a reason.
  • Keep what has happened apart from what is only scheduled.

What constrained it

  • Calls, messages and jobs are filed differently: some under the lead, some under the call, some under a phone number inside a job.
  • The voice platform reports two numbers for every call, and the system had to decide which one belongs to the lead.
  • A campaign label changes through two paths in the code, and only one of them writes an audit row.
Lead Lifecycle: one row per lead, with its first and current campaign
Lead Lifecycle: one row per lead, with its first and current campaignThe current console's per-lead table: New Lead to Cold Lead with one switch, two calls, one text and the next call, but not why it switched. Captured on a local copy with synthetic placeholder rows, not production data.

Decisions that made the difference

Three choices shaped the system. Each one lives at a specific point in the drawing above.

  1. At Resolve lead identity

    Start with the same lead

    The voice platform reports two numbers for every call, and for an outbound call one is the voice agent's own line. A lead created from the wrong one collects another lead's history.

    Take a new lead's phone from the number called, and find a call's lead by contact id first and by phone second.

    Evidence The phone fix of 26 Aug 2026, lead creation and the call-analysis fallback at the head commit, and a read of the system's tables.

    0 of 516leads created after the fix's commit carry a voice agent's line. Older leads are not a baseline, and the call-analysis fallback still tries the calling number first.

  2. At Explain campaign changes

    Explain the change, not just the label

    A lead's campaign name says where it is, not why it moved; a move that looks like it followed a call may have come from the sequence ending.

    Write an audit row with the old campaign, the new one and the reason whenever a switch follows from what the lead said, and show that reason beside the switch in the earlier console.

    Evidence The switch function and its tests at the head commit, the first console's timeline, and a count of both write paths.

    7 of 7audited switches carry from, to and reason. Entries into a campaign, including the move into Cold Lead when a sequence ends, write none, and the current console shows neither the stored reasons nor the entries.

  3. At Separate history from next action

    Separate what happened from what is planned

    A scheduled job, a message written in shadow mode and a completed call can each look like something that happened to a lead.

    Keep recorded calls and messages as history and pending jobs as the plan, take the next action from the earliest pending job, and clear an old next-action time when a lead enters a campaign.

    Evidence The lifecycle and overview queries, the message statuses and the fix of 24 Aug 2026, and a read of the system's tables.

    314 of 314open leads with a pending job showed it as their next action in both current views at the 18 Sep 2026 reading. Leads whose next-action time points at nothing are counted beside it, and a message's status still never says it was sent.

Who built it

  • AI Systems Architect at Colaberry: designed and built the Lead Journey, the campaign switch audit and the current console's lead views
Contact Drill-Down: six tables for one lead
Contact Drill-Down: six tables for one leadCalls, jobs and the text for one placeholder lead, each in a table of its own, and no campaign switch among them. Captured on a local copy with synthetic placeholder rows, not production data.

The build

  1. The Lead Journey and the switch audit
  2. Shadow messages join the timeline
  3. The new console's pipeline trace
  4. Contact Drill-Down
  5. A call finds its lead by phone
  6. Lead Lifecycle
  7. Voicemail steps keep their campaign
  8. A stale next action cleared
  9. Calls filed under the right campaign
  10. The lead's own number
Notes on 10 of the 10 steps
  • The Lead Journey and the switch audit. One commit gives each campaign switch an audit row with its reason, derives the lead's phone when the lead is created, and adds the first console's Lead Journey: a phone lookup, one timeline of calls, messages and switches, and the next pending job.
  • Shadow messages join the timeline. Messages written in shadow mode are stored beside the others and marked in the Lead Journey; the last change to that page.
  • The new console's pipeline trace. The Next.js dashboard's API gains a per-lead trace of scheduled jobs, and its page.
  • Contact Drill-Down. Six tables for one lead, and a next action on the campaign overview.
  • A call finds its lead by phone. Call analysis falls back to the lead's phone when the contact id does not match.
  • Lead Lifecycle. One row per lead: first and current campaign, switch count and next pending job.
  • Voicemail steps keep their campaign. A voicemail step trusts the campaign in its own job over a lead row that has drifted.
  • A stale next action cleared. Entering a campaign clears the lead's old next-action time, so it no longer outranks a real scheduled call on the overview.
  • Calls filed under the right campaign. A call takes its campaign from the system's own launch record instead of the voice platform's label.
  • The lead's own number. Lead creation prefers the number called over the agent's line, after leads were found carrying the agent's line as their phone.
Pipeline Trace: the lead's jobs in the order they run
Pipeline Trace: the lead's jobs in the order they runFive lead-level steps, the last one still pending, with no times and none of the call-level work. Captured on a local copy with synthetic placeholder rows, not production data.

What was built

The history is assembled from four stores the system already writes: call events, outbound messages, scheduled jobs and the audit log, with the lead's own row holding its campaign, status and next-action time. The first console, a Streamlit page called Lead Journey built on 31 Mar 2026, looked a lead up by phone, falling back to the raw call payloads, and drew one chronological timeline of its calls, messages and campaign switches, each switch with its reason, under the next scheduled contact. That page is now marked legacy, and no deployment runs it.

Stack

  • Python
  • Fastapi
  • PostgreSQL
  • Next.js
  • TypeScript
  • Streamlit
More on what was built

The console that replaced it from April, a Next.js dashboard, reads the same stores through three views. Lead Lifecycle gives one row per lead: its call and message counts, its first and current campaign, how many audited switches it had, and its next pending job. Contact Drill-Down lays out six tables for one lead, found by its contact id. The Pipeline Trace lists the lead's jobs in the order they run. None of the three draws a merged timeline, none shows why a campaign changed, and the trace reads lead-level jobs only: for 200 sampled leads it lists 3,551 of the 6,203 jobs recorded for them. A merged Lead Journey was specified for the new dashboard and was not built.

Side by side, the two consoles differ in four ways. A combined, chronological history: the earlier page drew one timeline of calls, messages, campaign switches and shadow actions; the current console has none and splits the history across its three views. The reason for an audited switch: the earlier page printed it beside the switch; the current console shows only that a switch happened, and the reason stays on the audit log. The next scheduled action: both take the lead's earliest pending job filed under the lead, and Campaign Overview also reads a separate next-action time. Call-level processing: neither shows it; the earlier timeline shows the calls but not the jobs that process them, and the current trace reads lead-level jobs only.

The campaign history depends on how the label moves. A switch decided from what a lead said on a call goes through one function, which writes an audit row with the old campaign, the new one and the reason. Entry into a campaign, which is how a new lead arrives and how a lead moves into Cold Lead after its sequence, goes through a second function, which schedules the next call and writes no audit row. So the audit log explains the switches it holds: all 7 carry the old campaign, the new one and the reason. It says nothing about at least 2,174 recorded campaign entries, some of which give a lead its first campaign rather than move it; an entry that finds no dialable phone, or a call already pending, changes the label and leaves no record at all.

The history also depends on which number is the lead. The voice platform reports the number called and the number calling, and for an outbound call the second is the voice agent's own line. On 26 Aug 2026 the builder changed lead creation to prefer the number called, after leads were found carrying an agent's line as their phone. Among leads created after that fix's commit, none carries one of the two voice-agent lines, by a heuristic that treats a number seen on calls for 20 or more different leads as an agent's line; 5 older leads still do. The tables record the commit's time, not when the fix was deployed. The fix changed how leads are created, not how a call finds its lead: when call analysis has to fall back to a phone, it still tries the calling number first, and a number shared by several leads matches whichever row the database returns.

What happened and what is planned are different rows. Recorded calls and message records are history, and a message's status says only whether shadow mode was on when the message was written: handing it to the CRM is a separate job with its own switch, and 0 of 21,408 stored messages record a sent or delivered status, which shows delivery is not recorded, not that nothing arrived. A pending job is the plan. Both current views take the next action from the lead's earliest pending job, and at the 18 Sep 2026 reading they agreed for every open lead that had one. Campaign Overview also reads a separate next-action time on the lead, and on 24 Aug 2026 the builder made entry into a campaign clear an old one, so it no longer outranks a real scheduled call.

How the four screenshots were made: the repository's own consoles, the current Next.js dashboard and the earlier Streamlit page, were run on a laptop at commit cf233ae with no keys, so they could reach neither the voice platform nor the CRM, against synthetic placeholder rows written for the purpose: one lead whose number sits in a range that cannot be dialled, two calls, one text and one campaign switch. No real lead, number, message or transcript appears. The current console labels every time CST, including in daylight time, and the earlier page prints its own formatting tags as text; both are how those pages draw.

A synthetic walkthrough, from those placeholder rows and not from production: one lead with two calls and a text moves from New Lead to Cold Lead. The earlier console shows the calls, the text and the switch with its reason in one timeline. In the current console, Lead Lifecycle shows the lead with its first and current campaign, one switch and its next call; Contact Drill-Down shows the calls and the text in tables of their own; and the Pipeline Trace shows five lead-level steps, the last one still pending. None of the three says why the lead switched.

Capabilities

  • Lead journey
  • Traceability
  • Audit trail
  • Identity resolution
  • Operator console

Integrations

  • Voice platform
  • Crm

Data stores

  • PostgreSQL
Historical, not deployed: the earlier console's one timeline, with the reason for the switch
Historical, not deployed: the earlier console's one timeline, with the reason for the switchThe earlier console's Lead Journey, marked legacy, not deployed and since replaced by the current dashboard: two calls, a text and the switch from New Lead to Cold Lead with its reason. Captured on a local copy with synthetic placeholder rows, not production data.

The measurement

What the current console answers, at one reading on 18 Sep 2026: for 314 of 314 open leads with a pending job, both lead views named that job as the next action, while 28 of 4,403 open leads showed a next-action time with nothing scheduled behind it; and 0 of 516 leads created after the phone fix's commit carry a voice agent's line. What it cannot yet answer: why a campaign changed, since all 7 audited switches carry their reason but 2,174 recorded campaign entries carry none and the console shows neither; what the call-level work did, since the trace lists 3,551 of the 6,203 jobs recorded for 200 sampled leads; and whether a message arrived, since 0 of 21,408 stored messages record a delivery status. Nothing here measures how long an investigation takes.

Evidence maturity: a shipped console and repository fixes with operating counts, read on 18 Sep 2026 in a read-only session on the system's own tables, and source tests run offline at the head commit. No figure compares the same measure before and after. The phone fix comes nearest, and it is not one: the builder's write-up records 35 rows before it and a backfill afterwards, so the 5 older leads that carry an agent's line today describe what the backfill left. The headline row is empty for that reason.

Full notes on all 8 metrics
  • 3,551 of 6,203 jobs listed by the current trace

    Jobs recorded for a lead that the current Pipeline Trace lists

    verified

    Baseline
    None. Nothing measured the first console's timeline, or either view, against an investigation.
    Sample
    200 leads with a call in the 90 days before 18 Sep 2026, chosen by the md5 of the contact id: 6,203 jobs.
    Methodology
    Read-only query on the system's tables: for each sampled lead, the distinct scheduled jobs whose payload names the lead or whose entity is the lead, split by whether entity_type is lead (what app/services/pipeline_trace.py reads at the head commit). 3,551 are lead-level and listed; 2,652 are call-level and not. The same sample found no calls or messages filed under a different identity for any of the 200 leads.

    Limitations

    • A sample of 200 is not every lead, but the gap is structural: the trace reads lead-level jobs only, so call-level jobs are missing for every lead that has them.
    • None of the 200 had calls or messages under another identity, so the lookup found everything keyed to them. That says nothing about a lead whose calls arrived under a different number.
  • 7 of 2,181 recorded campaign writes carry a reason

    Recorded campaign writes that carry a reason, over a lower-bound count of writes

    verified

    Baseline
    None. Both paths have existed since March 2026.
    Sample
    Every such row: the first switch on 13 Apr 2026, the first entry on 16 Apr 2026, to 18 Sep 2026.
    Methodology
    Read-only counts on the system's tables. apply_campaign_switch (app/core/campaigns.py at the head commit) writes one audit row and schedules nothing; enter_campaign writes a campaign-entry job and no audit row, so the two never overlap and 7 plus 2,174 counts each write once. Of the 236 later entries for 41 leads already in a campaign, none has an audit row within five minutes.

    Limitations

    • The 7 audited switches are complete: 7 of 7 carry from, to and reason, and none records a switch to the same campaign.
    • Recorded writes only: 2,174 is a lower bound for campaign entries, because an entry that schedules no call leaves no row. 236 of the entries were for leads already in a campaign (41 leads).
    • Most of the 2,174 entries are the move into Cold Lead when a lead finishes its sequence. The current console counts a lead's audited switches and shows its first campaign, so such a move appears as a changed campaign with no reason.
  • 7 of 7 audited switches carry from, to and reason

    Audited campaign switches that carry the old campaign, the new one and the reason

    verified

    Baseline
    None: a single reading.
    Sample
    The 7 audited switches in the system.
    Methodology
    Read-only count on the audit log: every campaign_switch row has a from campaign, a to campaign and a reason, and none records a switch to the same campaign. The switch function writes one such row and schedules nothing, so no switch is also counted as an entry.

    Limitations

    • Seven is a small set: the audited path runs only when what a lead says on a call leads to a switch.
  • 314 of 314 open leads with a pending job show it as their next action

    Open leads with a pending job whose next action matched in both current views, 18 Sep 2026

    verified

    Baseline
    None.
    Sample
    4,403 open leads (not closed or terminal, not marked do not call), 314 of them with a pending job.
    Methodology
    Lead Lifecycle takes the earliest pending lead-level job; Campaign Overview takes the earlier of that and the lead's next-action time. Both matched the earliest pending job for 314 of 314 leads, and none of those jobs was past due at the moment of reading.

    Limitations

    • A point-in-time reading. The next metric names the 28 leads whose next-action time points at nothing.
  • 28 of 4,403 open leads show a next action nothing is scheduled to take

    Open leads with a next-action time that has passed and no pending job, 18 Sep 2026

    verified

    Baseline
    None.
    Sample
    4,403 open leads.
    Methodology
    Read-only count on the system's tables, the same reading as the previous metric.

    Limitations

    • It says nothing about how long each time has been stale.
  • 0 of 516 new leads carry a voice agent's line

    Leads created after the phone fix's commit whose phone is a voice agent's line

    verified

    Baseline
    5 of 4,601 older leads still carry an agent's line. They are not a before figure, because a backfill ran in between.
    Sample
    516 leads created from 26 Aug 2026 12:14 PM CDT to 18 Sep 2026; 4,601 created before.
    Methodology
    Read-only query: the agent lines are the agent-number values seen on calls for 20 or more different leads (2 values, on 13,310 of 20,209 calls); a lead counts when its phone is one of them.

    Limitations

    • The boundary is the fix commit's time, not the deploy's, which the tables do not record.
    • The call table's agent-number column also holds lead numbers, so a looser test that matched any value in it counted leads that are not affected.
  • 0 of 21,408 stored messages record delivery

    Stored messages that record a sent or delivered status

    verified

    Baseline
    None.
    Sample
    All 21,408 rows.
    Methodology
    Read-only count by channel and status: emails 4,863 pending and 585 shadow, texts 14,914 pending and 1,046 shadow. The code writes only these two statuses.

    Limitations

    • The current console has a colour for a sent status, which the system never writes.
    • A zero here is the absence of a delivery status, not evidence that no message was delivered.
  • 218 of 220 tests pass, offline

    Tests passing in the eight test files behind this record

    verified

    Baseline
    None.
    Sample
    220 tests.
    Methodology
    pytest over test_campaign_switching, test_campaigns, test_lifecycle_jobs, test_inbound_call_processing, test_dashboard_v2, test_webhook_normalization, test_voicemail_jobs and test_call_processing: 218 passed, 2 failed, 0 skipped. The two failures, in test_inbound_call_processing, raise a TypeError comparing a mocked row count inside CRM task creation. The four lead-resolution tests, the three phone-derivation tests and the trace's phone fallback all pass.

    Limitations

    • A test that passes on a mocked database shows the logic, not the data it will meet.

What happened next

Shipped

  • Legacy: one timeline for a lead, in the earlier console
  • Lifecycle, drill-down and trace in the current console
  • An audit row for each campaign switch made from a call
  • Repairs to the lead's number, a stale next action and a call's campaign

Unknown

  • A merged Lead Journey in the current console
  • The reason for a switch, on the current console
  • An audit row for campaign entries
  • Call-level work in the trace
  • A delivery status on messages
  • A call-analysis lookup that tries the called number first
  • Older leads still carrying an agent's line
  • Time labels that follow daylight time
Notes on 12 of the 12 items
  • Legacy: one timeline for a lead, in the earlier console. Built on 31 Mar 2026 and still in the repository. Its documentation marks the page legacy, replaced by the current console, and it is not deployed.
  • Lifecycle, drill-down and trace in the current console. Three views of the same records, built from 5 to 20 Apr 2026.
  • An audit row for each campaign switch made from a call. Old campaign, new campaign and reason on every audited switch.
  • Repairs to the lead's number, a stale next action and a call's campaign. Four fixes on 24 and 26 Aug 2026.
  • A merged Lead Journey in the current console. Specified in the current dashboard's requirements, reusing the earlier console's query. Not built, and no decision about it is recorded.
  • The reason for a switch, on the current console. The reason stays on the audit log; the current views show that a lead switched and its first campaign, not why. No decision about it is recorded.
  • An audit row for campaign entries. Entry into a campaign, including the move into Cold Lead when a sequence ends, changes the label without an audit row. No decision about it is recorded.
  • Call-level work in the trace. The trace lists lead-level jobs only; call processing, voicemail steps and staff notes are not in it. No decision about it is recorded.
  • A delivery status on messages. Handing a message to the CRM is a separate job, and nothing writes back whether one was sent or delivered. No decision about it is recorded.
  • A call-analysis lookup that tries the called number first. When call analysis falls back to a phone to find a call's lead, it still tries the calling number first, and a number shared by several leads matches whichever row comes back. No decision about it is recorded.
  • Older leads still carrying an agent's line. Leads created before the fix that still carry a voice agent's line as their phone. No clean-up of them is recorded.
  • Time labels that follow daylight time. The current console labels every time CST, including from March to November. No decision about it is recorded.

Meet the builder

AI Systems Architect

Project contribution

Built the Lead Journey timeline and the audit row for each campaign switch on 31 Mar 2026; the current console's pipeline trace, drill-down and lead lifecycle table between 5 and 20 Apr; and, on 24 and 26 Aug, four fixes to the records under the views: a voicemail step's campaign, a stale next action, the campaign a call is filed under and the number that identifies a lead. The lifecycle table of 20 Apr and the four August fixes name an AI coding assistant as co-author; the commits before them name none.

Skills demonstrated

  • Joining what the system already keepsOne timeline from call events, messages, jobs and the audit log, with no new store (architecture, build timeline).
  • Recording why, not only whatA switch made from a call writes its old campaign, new campaign and reason, and every audited switch carries all three (measurement).
  • Fixing the data under the viewChanged which number identifies a lead; no lead created after the fix's commit carries an agent's line (measurement).
  • Naming where the history stopsThe record says the current console does not show why a lead switched and that no status proves a message arrived (roadmap, measurement).

From the repository record.

What this project shows

This project shows a lead's state made explainable from the records the system already keeps: one timeline in the earlier console, an audit row that says why a campaign switched, and repairs to the number that identifies a lead and to a next action that had gone stale. At one reading on 18 Sep 2026, the current console named the same next action in both of its lead views for open leads with a pending job. It does not yet show why a campaign changed, what the call-level work did, or whether a message arrived.

Build one of these

Start the program that produced this work.

See the program