CRM contact ingestion and enrichment controls: what shipped in May
Our CRM was drifting from reality. Phone backups held the real contacts — every number, every name, every recent connection — but they lived in iCloud and Google exports, never making it into the system we actually worked out of. Meanwhile, the enrichment job was running on a schedule nobody could see, and the status field on a contact didn't always reflect what had actually happened to it. In May we shipped four commits that closed the phone-backup-to-CRM gap, gave operators explicit control over enrichment scheduling, and made the status field on every contact tell the truth. This post covers each one, what it does, and why operator trust in the CRM data depends on all four of them working together.
Phone backup importer with tag and merge
The phone backup importer landed in commit 1684c31. It reads standard iCloud and Google Contacts CSV exports, maps them to the CRM contact model, and lands them in a staging table where an operator can review them before they touch production records.
The interesting part is the tagging and merge logic. Every contact that comes in is tagged with a source — mark-import, iris-import, or crm-direct — so we can always answer the question "where did this record come from?" The merge step is conservative: a phone backup contact only merges into an existing CRM record when the phone number matches and the names are not in conflict. When the names disagree, the contact is staged for review with both versions side by side, and the operator picks. Nothing is overwritten silently.
The practical effect: an operator running the import gets a small, reviewable list of new contacts, a list of merges that happened automatically, and a list of conflicts that need a human eye. The CRM is no longer the place where phone contacts go to die.
Scheduled enrichment cron controls
The enrichment job used to run on a hardcoded schedule. Operators could not pause it, change its cadence, or see when it had last run. That landed in commit c4776b4.
The control surface is a single settings panel with three fields: enabled, schedule, and last-run timestamp. The schedule accepts the same cron expressions the rest of the system uses, and the enabled flag is respected at job start, not at job runtime — so a job that was already in flight when an operator disabled it will finish, and the next one will not start. The last-run timestamp is updated only when a run completes without error.
The point of the control surface is not flexibility for its own sake. It is that an operator who sees a bad enrichment pass — a vendor returned garbage, a contact got the wrong industry — can stop the bleeding immediately, fix the data, and turn the job back on. Before this commit, the only way to stop the bleeding was to wait for the next pass to fail and hope it didn't compound the problem.
Disabled cron stays quiet
This is the small commit that took the longest to land. 3d4804a is a one-line fix in spirit, but the user-visible behavior is what matters: when enrichment is disabled, nothing about it surfaces in the operator's view. No phantom run logs. No "skipped because disabled" entries cluttering the activity feed. The job is off, the system acts like the job is off, and an operator opening the dashboard on a Monday morning sees a clean activity log.
The reason this is a separate commit is that the previous behavior — logging every disabled run as a "skipped" event — was actively corrosive. Operators stopped trusting the activity log because most of it was noise. Removing the noise was a precondition for operators trusting the activity log when something real did happen.
Enrichment status fix for failed and ineligible attempts
4aa16a4 is the correctness fix. Before this commit, a contact that enrichment had attempted and failed on would show the same status as a contact that had never been attempted at all. The operator could not tell the difference between "we haven't tried" and "we tried and it didn't work."
The fix introduces three states where there used to be one:
enriched— the contact was successfully enriched on the most recent attempt.failed— the most recent attempt returned an error or a non-matchable result. The reason is recorded.ineligible— the contact does not meet the criteria for enrichment (missing required fields, opted out, vendor returned a permanent rejection). The reason is recorded.
The last_attempt_at timestamp is set in all three cases, so an operator can answer the follow-up question — when did we last try? — without leaving the contact record.
Why this matters for operator trust
Operator trust in CRM data is not built by a single feature. It is built by a small number of properties that hold consistently over time: a record is what it says it is, the activity log is what actually happened, and the controls do what they advertise. The four commits in this post each fix one of those properties.
The phone backup importer fixes the property of origin: every record has a source tag, and a merge never silently overwrites. The scheduling controls fix the property of control: an operator can stop the job, change the cadence, and see the last run, and the system respects that control at job boundaries. The quiet-when-disabled commit fixes the property of signal-to-noise: the activity log is real events, not a stream of "skipped" entries. The status fix fixes the property of truthfulness: the status field on a contact reflects what actually happened, and the operator can tell "not yet attempted" from "attempted and failed."
When all four hold, the CRM is a tool an operator can act on. When any one of them fails, the CRM becomes a thing an operator second-guesses, and second-guessing is the most expensive activity in any small-team workflow.
What to watch
The next round of work covers three areas. First, the merge conflict review UI needs a "show me the diff" view, so an operator resolving a name conflict can see the full source records side by side without leaving the screen. Second, the enrichment schedule will move to per-source controls, since operators have asked for the ability to run the email enrichment pass on a different cadence than the phone enrichment pass. Third, the eligibility rules for enrichment are getting a small audit, because the current rules were written when there was one vendor and now there are three.
The commits to look at for this work are the import module in 1684c31, the scheduling controls in c4776b4, the quiet-disabled fix in 3d4804a, and the status correctness fix in 4aa16a4.