Training · learner work

The Conversation Happened. The CRM Needs the Right Record.

An AI Systems Architect at Colaberry built the part of an admissions team's calling platform that writes each finished call into the CRM: the contact's fields, a task for a person, the call in the conversation history, the transcript as a staff note. The review reads the platform's own record of those writes against its jobs, and states where that record keeps its promise and where a completed job wrote nothing.

verified

All student projects
What reaches the CRM after a call, and what proves it, in 125 seconds. Narrated by a synthetic voice; the figures it states are the verified metrics recorded below.
Industry
Education
Capability
Operational AI crm records written after calls
Status
Shipped
Built by
Colaberry team
Published
2026-09-22

The situation

An admissions team's calling platform places and receives calls through a voice platform and reads each finished call with a model. The CRM is where the team works, so every call has to end up there: the contact's fields updated, a task created for whoever should follow up, the call itself in the contact's conversation history, and the transcript where staff can read it. Each of those is a write to an external system through its API, from a background job, with a credential, and each can fail, be skipped, or be repeated.

Picture one call, an invented example rather than a recorded one: the admissions line calls a lead back about a programme's schedule, the lead asks two questions and the call ends after six minutes. What the CRM should hold afterwards is specific: the lead marked as a lead with a classification and an assignee, a follow-up task for that person, the call in the conversation history with its duration, and the transcript as a note staff can read. Four artifacts, three clients, one shared question: did each write happen, and how would anyone know? Other CORA records cover the shadow gate that decides whether writes go out at all, the lead's history and the next action; this one covers what reaches the CRM after a call and what the platform keeps as proof.

What it had to do

  • Resolve the contact before any write, and refuse to write when nothing resolves.
  • Keep one local evidence row per remote write, with one success per call, so a retry can see what already happened.
  • Turn every failed write into an exception a person can retry from the dashboard.

What constrained it

  • The CRM is reachable only through its API, with three credentials: a private integration token, an OAuth location token, and a second private token for staff notes.
  • The voice platform's webhook may carry a phone number where a contact id is expected, so every write has to work out which it holds.
  • The values written come from a model's reading of the call; when the model fails, the code still has to decide what to write.
  • A background job can be claimed a second time if its lease expires while it is still running, so a write can be repeated.
One call's eight jobs beside what the CRM handed back: two stored ids, one failure, one pending retry
One call's eight jobs beside what the CRM handed back: two stored ids, one failure, one pending retryA read-only query typed into the console's DB Explorer by the capture script: the call's jobs in order, with the id the CRM returned where the product stored one. The task write and the staff-note write each carry a placeholder id shaped like the CRM's; the summary write completed and stores no remote id at all; the conversation-log write failed; and the last row is the operator's retry as the product makes it, a new pending job pointing back at the failed one and carrying its error text. The column names at_central, stored_remote_id, retry_of and carried_error are the query's, not the product's, and the readable job id is a seeded id where the product's own are UUIDs. A completed row is the product's own completion record; the stored id is the CRM's acknowledgement of one request, never a view into the CRM. Keyless local stack at the pinned commit with invented placeholder rows, not production data; no CRM was reached; times are Central.

Decisions that made the difference

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

  1. At Resolve the contact

    Resolve the contact before writing

    The webhook copies a phone number into the contact id when it has none, so a write can be aimed at a phone string, and the CRM answers that with an error or a wrong record.

    Test whether the id is phone-shaped; fetch a real id, or search the CRM by the number on file and take the match; since 17 Sep 2026 the voicemail update refuses to write when nothing resolved.

    Evidence The resolution blocks in the three CRM jobs, the search adapter, the webhook route and the 17 Sep 2026 commit, all at the pinned commit.

    3 of 4write jobs refuse to write when no contact resolves: the voicemail update since 17 Sep 2026, the conversation log and the staff note; the task job still writes to the phone string or "unknown". The search returns the first contact of a free-text query, so a second matching contact is never seen, and nothing normalises the number first. No test covers two matching contacts.

  2. At Evidence row written

    Keep a local row for every remote write

    A completed job proves the job ran, not that the CRM changed; a retry that cannot see what already happened writes a second task or a second note.

    Give each write path its own evidence table with at most one "created" row per call event, read the table before writing, and store the provider's id on the row.

    Evidence The three tables and their partial unique indexes, the three row constructors, and a read-only count of the rows against the jobs on 21 Sep 2026.

    1,181 of 1,181staff-note rows carry the CRM's message id, one per call, for 1,184 completed jobs. The task row carries no id in 0 of 2,453 cases and is written whether or not a task was requested; the conversation-log path wrote no row for 1,099 completed jobs, because the token on file sits under another location id and the job counts that as a clean skip. The rows are read only by their writers and never reconciled with the CRM.

  3. At Exception with the job id

    Make a failed write something a person can retry

    A write that fails inside a background job disappears unless something keeps the failure, and a person cannot retry what nobody recorded.

    Record every failure as an exception carrying the call, the job and the error, and since 17 Sep 2026 the whole payload, so the dashboard's Retry Now re-enqueues the same work.

    Evidence The exception contexts in the three CRM job files, the retry route, the 17 Sep 2026 commit, and a read-only count of exception rows by type on 21 Sep 2026.

    9exception rows exist across the five CRM-write failure types, against 26,674 completed write jobs, because the paths that wrote nothing completed cleanly: 1,099 token skips and 259 summary rows audited as delivered on a shadow response. An exception does not say which of a two-write job's writes ran, and exceptions are not closed when a later run succeeds.

Who built it

  • AI Systems Architect at Colaberry: designed and built the CRM write paths, the contact resolution, the evidence tables and the failure and retry handling
The operator's page for one contact: a completed call, then seven jobs, one of them failed
The operator's page for one contact: a completed call, then seven jobs, one of them failedContact Drill-Down, the page a staff member opens to ask what happened to a lead: the lead's state, one completed 372-second call with an invented transcript preview, and the seven jobs it caused, five of them created in the same second by the analysis job; four CRM-related jobs completed and the conversation-log write failed. The retry job and the exception are not on this page, and that is the pinned code's behaviour: the page finds jobs by the payload's contact id, which a retry does not carry, and files exceptions under the call id. "Completed" here is the product's own completion record; the CRM's acknowledgement, the stored remote id, is not shown on this page. Keyless local stack at the pinned commit with invented placeholder rows, not production data; no CRM was reached; times are Central.

The build

  1. The CRM client
  2. The write jobs
  3. Live writes by the dashboard flag
  4. A second client, with OAuth
  5. A third client, for staff notes
  6. Disguised timeouts retried
  7. A write refused when nothing resolved
Notes on 7 of the 7 steps
  • The CRM client. The first client: a private integration token, read methods, and write methods that return a shadow response until switched on.
  • The write jobs. The task job and the voicemail field update as background jobs, with their tests, on the queue reserved for callbacks.
  • Live writes by the dashboard flag. Write methods validate only credentials when the dashboard flags are passed; the same day the database write mode was set to live.
  • A second client, with OAuth. The conversation-log write through an OAuth token stored per location, with its own evidence table and a token lifecycle.
  • A third client, for staff notes. The transcript and recording link posted as a staff-only note, with its own evidence table and credential.
  • Disguised timeouts retried. A 401 whose body is the provider's timeout message is retried in the two newer clients instead of being read as an authentication failure.
  • A write refused when nothing resolved. The core client gains the same retry; the voicemail update raises before any write when its contact is still a phone string, and its exception carries the whole payload.
The product's own failure record: the error, the job id, the call id, and two operator actions
The product's own failure record: the error, the job id, the call id, and two operator actionsExceptions Monitor with the "conversation log failed" group expanded: the exception the product's own code wrote when the contact could not be resolved, carrying the job id, the call id and the call event id a recovery needs, with Resolve and Ignore for the operator. The error text was written by the product and says the contact could not be resolved; it does not record why the search failed. The "SYSTEM" chip is the page's category map not knowing this exception type, and there is no Retry button at this commit: the retry in the first image was made through the product's API. Keyless local stack at the pinned commit with invented placeholder rows, not production data; no CRM was reached; times are Central.

What was built

A completed call arrives as a webhook, becomes a call event, and its analysis job fans out five background jobs on a queue reserved for callbacks. Three of them write to the CRM, each through its own client and credential: the task job writes the contact's fields and, when the model asks for one, a task, through a private integration token; the conversation-log job writes the call into the contact's conversation history through an OAuth token stored per location; the staff-note job posts the transcript and recording link as a note staff can read, through a second private token. A fourth job, after a voicemail-tier message, updates the contact's fields again through the first client. Thirteen call sites in the code write to the CRM in all.

Stack

  • Python
  • Fastapi
  • PostgreSQL
  • Redis
  • Rq
  • Docker
More on what was built

Before any of those writes, the job works out who the contact is. The webhook copies a phone number into the contact id when it has none, so each job tests whether the id is phone-shaped; a real id is fetched, a phone is searched, and the first contact the search returns wins. Since 17 Sep 2026 the voicemail update refuses to write when nothing resolved; the conversation-log and staff-note jobs fail with an exception; the task job still writes to the phone string or "unknown". What is written comes from the model's reading of the call, copied into fields as returned; when the model call fails, a fixed fallback (not a lead, no campaign, no task) is written to the contact as if it were a result.

Every write sits behind a switch, and there are two definitions of the switch. Six call sites pass the dashboard's flags, which read the app_config table first, so a person can turn writes live from the System Controls page; five pass nothing and so read the environment the worker started with; the two newer clients read their own environment switches. Five per-action toggles are defined, accepted by the mode endpoint and drawn on the page, and read by nothing. On 21 Sep 2026 the database said live (since 18 Apr 2026, after a three-day shadow period) and every worker's environment said shadow, so the six sites were live and the five were returning a shadow response while the dashboard showed live.

What the platform keeps as proof is a row per write path: task_events, ghl_conversation_log_events and ghl_internal_comment_log_events, each with a partial unique index allowing one "created" row per call event, each read before the write. The protection is internal: no request to the CRM carries an idempotency key, a timed-out request is re-sent as it was, the provider's ids are stored but never read back, and there is no reconciliation. A completed job is the job's own status: it covers a live write, a shadow response, a clean skip (no token, no transcript, no consent) and a field write dropped when the label lookup returned nothing; the response body of a field write is never inspected.

Read on 21 Sep 2026 against the jobs, the proof rows say this. The staff-note path keeps its promise: 1,181 rows for 1,184 completed jobs, every row carrying the CRM's message id. The task path writes its row on every completion whether or not a task was requested, and 0 of 2,453 rows carry a task id. The conversation-log path completed 1,099 jobs and wrote 0 rows: its only completion without a row is the "OAuth app not installed" skip, and the one token on file is stored under a location id that is not the one the worker resolves, which the repository's own July note already records as a no-op. The summary path wrote 259 "delivered" audit rows on a gate that reads the environment, which says shadow. Nine exception rows exist across the five CRM-write failure types, against 26,674 completed write jobs, because none of those paths counts as a failure.

The screenshots were captured on a local copy of the platform at the pinned commit with no keys and invented rows; times are Central.

Capabilities

  • Crm integration
  • Contact resolution
  • Write gating
  • Evidence records
  • Failure accountability
  • Runtime controls

Integrations

  • Voice platform
  • Crm
  • OpenAI

Data stores

  • PostgreSQL
  • Redis
The switches an operator sees: one master at Live, five sub-toggles that no write path reads
The switches an operator sees: one master at Live, five sub-toggles that no write path readsSystem Controls, the CRM writes card only: the master switch at Live with its last-changed line, and five per-action sub-toggles, all on. At this commit every write that passes the dashboard's flags checks the master alone; the five sub-toggles are rendered and audited but read by no write path; and the conversation-log and staff-note paths are gated by environment settings with no switch on this page. On this stack the master was configured to the documented live posture directly, because the product's own endpoint refuses the change without provider keys. Keyless local stack at the pinned commit with invented placeholder rows, not production data; no CRM was reached; times are Central.

The measurement

The record measures the CRM writes from the platform's own tables, never from the CRM. Staff notes: 1,181 of 1,181 rows carry the CRM's message id. Tasks: 0 of 2,453 rows carry a task id, and the row is written on every completion. Call history: 0 of 1,099 completed jobs wrote a row; the one token on file is stored under a location id other than the one the worker resolves. Summaries: 259 of 2,380 completed jobs wrote a "delivered" audit row, on a gate that reads the environment.

The controls: 13 write sites, 6 on the dashboard flag, 5 on the environment, 2 on their own switch; on 21 Sep 2026 the database said live and the environment said shadow. Nine exception rows across the five failure types against 26,674 completed jobs. Tests: 286 of 292 offline, the six failures named. Refused: write latency, duplicate remote artifacts, remote-record correctness, and any savings figure.

Full notes on all 7 metrics
  • 0 of 1,099 completed conversation-log jobs wrote an evidence row

    Completed conversation-log jobs that wrote an evidence row

    verified

    Baseline
    None: the count is the finding.
    Sample
    Every write_conversation_log job with status completed from 10 Jul 2026 to 22 Sep 2026: 1,099.
    Methodology
    Read-only counts on scheduled_jobs and ghl_conversation_log_events; the code's only completion path without a row is the no-token skip; the token row's location id was compared with the worker's resolved location on the host without printing either.

    Limitations

    • The repository's own July note records this path as a no-op for want of a token for the production location; the record states what the tables show on 21 Sep 2026, not why the token was stored where it was.
    • The application's skip log line is not in the container log, so the branch is established from the table and the code, not from a log.
  • 1,181 of 1,181 staff-note rows carry the CRM's message id

    Staff-note evidence rows carrying the CRM's message id

    verified

    Baseline
    None: one cohort.
    Sample
    Every row in ghl_internal_comment_log_events from 16 Jul 2026 to 22 Sep 2026: 1,181, against 1,184 completed write_internal_comment_note jobs.
    Methodology
    Read-only counts on ghl_internal_comment_log_events and scheduled_jobs.

    Limitations

    • A message id on the local row is the CRM's acknowledgement of one request; whether the note is visible to staff, or exists twice after a retried timeout, was not checked, because the CRM was not read.
    • The three completed jobs without a row took the no-transcript or dedupe skip.
  • 0 of 2,453 task evidence rows carry the CRM's task id

    Task evidence rows carrying the CRM's task id

    verified

    Baseline
    None: one cohort.
    Sample
    Every row in task_events from 13 Apr 2026 to 22 Sep 2026: 2,453, one per call event, against 2,471 completed create_crm_task jobs.
    Methodology
    Read-only counts on task_events, scheduled_jobs and classification_results; the constructor at the pinned commit writes the row on every completion and reads the id from the response's top level.

    Limitations

    • Two readings fit the table and neither is verified here: no analysis ever asked for a task (the decision is not persisted), or the CRM nests the id below the top level of its response.
    • The dashboard's task success rate reads this table and can only be 1.0 or empty.
  • 259 of 2,380 completed summary jobs wrote a "delivered" audit row

    Completed summary jobs that wrote a "delivered" audit row

    verified

    Baseline
    None: one cohort.
    Sample
    Every send_student_summary job with status completed from 13 Apr 2026 to 22 Sep 2026: 2,380; 259 audit rows with action student_summary_delivered, 17 of them in September.
    Methodology
    Read-only counts on scheduled_jobs and audit_log; the job's gate reads the environment (no mode_flags passed), which said shadow on every worker on 21 Sep 2026.

    Limitations

    • Under the environment observed on 21 Sep 2026 the write returns a shadow response and the audit row is written regardless; whether the environment differed on earlier dates is not established.
    • The other 2,121 completions took a skip branch: no consent recorded, or no summary.
  • 13 write sites: 6 read the dashboard flag, 5 the environment, 2 their own switch

    CRM write sites at the pinned commit, by which switch they read

    verified

    Baseline
    None: a census of the code.
    Unit
    call sites
    Sample
    Every call of the six write methods in app/ at the pinned commit, definitions and docstring examples excluded: 13.
    Methodology
    A grep over app/ at e4af0ebc for the six write method names, each hit read for its arguments.

    Limitations

    • A census of code, not of writes made: how many writes each site made is in the job counts, not here.
    • On 21 Sep 2026 the database flag said live and the environment said shadow, so the six were live and the five were returning shadow responses.
  • 9 exception rows across the five CRM-write failure types, against 26,674 completed write jobs

    Exception rows across the five CRM-write failure types

    verified

    Baseline
    For scale, the two call-placement types hold 1,509 and 1,347 rows.
    Sample
    Every row in exceptions whose type is one of the five raised by the three CRM job files, all dates: 9 (2 conversation log, 1 task, 3 voicemail update, 2 staff note, 1 voicemail tier); 7 ignored, 2 resolved.
    Methodology
    Read-only counts on exceptions grouped by type and status.

    Limitations

    • A low count is not a low failure rate: the 1,099 token skips and the shadow summary writes raised no exception because the code treats them as clean completions.
    • Exceptions are not closed by a later success, so a resolved row means a person acted.
  • 286 of 292 tests pass offline in the thirteen files nearest the CRM writes

    Tests passing offline at the pinned commit in the thirteen files nearest the CRM writes

    verified

    Baseline
    None.
    Sample
    The thirteen test files behind the CRM clients, the CRM jobs, claims, exceptions, lifecycle, the dashboard retry and the enrolled intent: 292 tests.
    Methodology
    pytest over the thirteen files in an isolated container with no network, the checkout mounted read-only at the pinned commit; the six failures each read: two stale payload assertions, one stale label assertion, three conversation-log tests that need a location id in the environment (with one set, 9 of 9 pass).

    Limitations

    • No test contacts a real CRM: every adapter call is patched or served by a fake HTTP client.
    • The task job's happy-path test patches only the task call, so its model call falls back and no field write is exercised; no test covers two matching contacts, a retried timeout that duplicates a task, or the two definitions of live disagreeing.
The same call as a timeline: received, analysed, four writes completed, one failed with its error inline
The same call as a timeline: received, analysed, four writes completed, one failed with its error inlineLive Activity, newest first: the exception created and the job failed, with the product's error text inline, above the six completed jobs and the call processed, each with the second it happened. The two red rows were written by the product's own failure path; the green and purple rows were seeded in the product's event shape. "Polling" is the product's own fallback because the capture blocked a connection to an off-host address; the feed's contents are the same either way. Keyless local stack at the pinned commit with invented placeholder rows, not production data; no CRM was reached; times are Central.

What happened next

Shipped

  • The contact resolved before the write
  • One evidence table per write path
  • An exception per failed write, retried from the dashboard
  • A switch on every write path
  • A task only when the model asks for one
  • The staff note's message id kept on its row
  • A token per location, refreshed near expiry

Unknown

  • The CRM's task id on the task row
  • A token under the location the worker resolves
  • One definition of live
  • Per-action flags that gate something
  • An idempotency key on the request
  • The stored ids read back
  • A fallback that is not written as data
  • A "delivered" audit tied to a live result
  • A completed job that means a write
Notes on 16 of the 16 items
  • The contact resolved before the write. A phone-shaped id is searched and a real id fetched; three of the four write jobs refuse to write when nothing resolves.
  • One evidence table per write path. task_events, the conversation-log table and the staff-note table, each allowing one "created" row per call event.
  • An exception per failed write, retried from the dashboard. The call, the job and the error on every failure; the whole payload since 17 Sep 2026 so Retry Now re-enqueues it.
  • A switch on every write path. Six sites read the dashboard flag, five the environment, two their own switch; all return a shadow response when off.
  • A task only when the model asks for one. The task is created only when the analysis says so, with the title, assignee and due date; its description is dropped because the API rejects it.
  • The staff note's message id kept on its row. Every live success stores the id the CRM returned, one row per call event.
  • A token per location, refreshed near expiry. The conversation-log write uses an OAuth token stored per location and refreshes it near expiry; a missing token is a clean skip by design.
  • The CRM's task id on the task row. 0 of 2,453 rows carry one; the row is written whether or not a task was requested; no decision about it is recorded.
  • A token under the location the worker resolves. The one token on file sits under another location id, so 1,099 conversation-log jobs completed with nothing written; no decision about it is recorded.
  • One definition of live. The dashboard flag and the environment property disagreed on the running system on 21 Sep 2026; five paths follow the environment; no decision about it is recorded.
  • Per-action flags that gate something. Five toggles are drawn on System Controls and read by no adapter or job; no decision about it is recorded.
  • An idempotency key on the request. A timed-out write is re-sent as it was and the dedupe row is invisible until the job commits; no decision about it is recorded.
  • The stored ids read back. The provider's ids are stored and never read, and nothing reconciles the local rows with the CRM; no decision about it is recorded.
  • A fallback that is not written as data. When the model fails, a fixed result is written to the contact as if it were an analysis; no decision about it is recorded.
  • A "delivered" audit tied to a live result. The summary path audits delivered on a shadow response, 259 rows; no decision about it is recorded.
  • A completed job that means a write. Completed covers a live write, a shadow response, a clean skip and a field write dropped at label lookup; no decision about it is recorded.

Meet the builder

AI Systems Architect

Project contribution

Built the CRM client in March 2026 and the write jobs the same month; put every write behind a switch and, on 18 Apr, let the dashboard's flag decide the live writes; added the conversation-log write with its own OAuth token lifecycle on 8 Jul and the staff-note write on 16 Jul, each with an evidence table allowing one success per call; taught the clients to retry the provider's disguised timeouts on 14 and 17 Sep, and on 17 Sep made the voicemail update refuse a write to an unresolved contact. Of the seven commits this record cites, the three from March and April 2026 name no AI coding assistant as co-author; the four from July 2026 onward name Claude.

Skills demonstrated

  • External writes with proofOne evidence table per write path, one success per call, the provider's id stored (architecture, decisions).
  • Identity before actionA phone-shaped id searched and a real id fetched before any write; the write refused when nothing resolves (decisions).
  • Three clients, three credentialsA private integration token, an OAuth location token with refresh, and a second private token, each behind a switch (architecture).
  • Verification against production dataEvidence rows read against jobs, the token location compared, the two switches read where they live (measurement).

From the repository record.

What this project shows

This project shows an AI Systems Architect treating a CRM write as something to be proved, not assumed: a contact resolved before the write, a switch on every path, a local row per remote write with one success per call, and an exception a person can retry from. The record also shows what that proof leaves out. The task row never carries the CRM's task id, one path completed 1,099 times while writing nothing, and the switch has two definitions that disagreed on the running system on 21 Sep 2026. Those are stated here as observed, with their dates, and nothing in the CRM itself was read.

Build one of these

Start the program that produced this work.

See the program