TRAINYOURAGENT

UD Growth Engine: 23% of a roofing quarter, credited to the wrong channel

We began with the client in June 2026, so June is where our claims start. One job since then, $18,465, traces from a website booking on 9 July to a CRM job created on 14 July, matched on phone, email and name. The CRM records its lead source as “Telemarketing”. That is 22.6% of the quarter credited to the wrong channel, and without a second record nobody would ever have known.

The problem

CORRECTION, 4 September 2026. Every figure in this section was wrong until today, and wrong in the direction that flattered us. The CRM account we harvested holds books for two unrelated companies. Our client accounts for one book in that account. The remainder sits under books belonging to a separate business that is not our client and never was. We had summed both and published the combined total as though it were one company’s history. Everything below is the client's book only. It is a smaller story and it is the true one. We began with the client in June 2026. Everything in the next three paragraphs is what we inherited, not what we produced — it matters because the shape of it is the entire reason the rest was needed. They are a long-established Mid-Atlantic roofing contractor. Across four and a half years of recorded history they booked 323 jobs, and the great majority of that approved value was collected. Then you read the lead-source column, and often you cannot. 78 of the 323 jobs — 24.1% of them, carrying $475,668 of approved value — have no source recorded at all. Not “other”. Blank. And it is not spread evenly: 2022 ran 18.8% untagged and 2023 only 3.4%, then 2024 jumped to 56.5% — 13 of 23 jobs, $178,199 — and 2025 sat at 33.3%. That one fact governs how every other number on this page may be read. Among the jobs that ARE tagged, door knocking is much the largest at $831,471 across 122 jobs, and three jobs carry the tag “Internet”. But three is the count of jobs somebody typed that word on, not the count of jobs the internet produced — and with a quarter of the history blank, the real figure is unknown and is certainly higher. An earlier version of this page reported that $12,100 as if it were the internet total. It was wrong, the client caught it, and it is precisely the failure this engine exists to remove: a blank field read as a zero. So the business had one channel, and no instrument pointed at it. An earlier version of this paragraph claimed a 93.7% revenue fall from 2024 to 2025; that was a different company's book closing, not this client. The client went the other way, roughly doubling both job count and approved value across those two years. A small business that grew, with no way to say which of its own efforts did it, which is a different problem from a collapse and the one we were actually hired into. Corroborated by an independent pull from the CRM’s own Lead Sources report showing 25 leads and $81,674 contracted across all nine location books in the most recent 90 days. Seventeen of those 25 leads were still sitting at the first stage ninety days later. Underneath that, the things you would reach for in a bad year did not work either. Meta had spent $633.25 over 90 days for one lead and zero closed revenue. And across the entire history of the database, not one Google or Meta click identifier had ever been attached to a lead — meaning every advertising dollar, past or future, was unmeasurable by construction. None of this is exotic. It is what most contractors would find if anyone went and looked, and nobody goes and looks, because looking requires the nine separate books to be one book first. That is the actual problem this engine solves; the schema below is only how. Ten weeks in, the result is small and countable: 50 inbound leads through 10 distinct doors that did not exist in June — booking, the contact page, the Spanish contact page, the satellite roof quote, the instant-quote funnel, the voice agent, Google Local Services. 41 of the 50 carry recorded call consent. 21 appointments were booked. No closed revenue is yet attributable to any of it, and in a trade where a roof takes weeks to go from first call to signed contract, ten weeks is too early to claim one. What can be claimed is that the instruments exist and are recording.

One schema, 456 migrations, nothing hand-applied

The database is defined entirely by files. There are 456 timestamped SQL migrations in supabase/migrations, totalling 52,497 lines, and they compose into 239 production tables. Nothing is applied by hand in a dashboard. That sounds like ordinary discipline until you look at what it costs to actually hold: with 456 migrations there is no such thing as remembering what the schema is, so the schema has to be able to prove things about itself. So it does. A guard called schema-drift-verify parses the declared schema and diffs it against production structurally. A second guard, column-drift-build-verify, goes further: it builds the full migration set on a real PostgreSQL instance and diffs one identical catalogue projection — type, NOT NULL, generation class — against production, hashed per table and drilled per column on mismatch. It is wired into the deploy workflow as a blocking step. Its first production run was green across 239 tables; on a manual run before that it caught two numeric-precision drifts, one of which a migration from that same week had introduced (a `numeric` column where production held `numeric(2,1)`). The practical end state is stated plainly in the repository's own ledger: a green deploy now structurally requires the schema in the files and the schema in production to agree on tables, columns, types, NOT NULL, generation, and every enumerating CHECK, with nothing grandfathered and no non-blocking steps.

54 edge functions as the only write path

Application logic lives in 54 Supabase edge functions, all prefixed `ud-`, each a Deno TypeScript module with a single job. They divide roughly into intake (`ud-lead-inbound`, `ud-email-inbound`, `ud-sms-inbound`, `ud-bulk-leads`), the voice and booking loop (`ud-riley-lookup`, `ud-riley-dial`, `ud-riley-book`, `ud-riley-outcome`), field and claims work (`ud-damage-ai`, `ud-adjuster-packet`, `ud-supplement`, `ud-inspection-deliver`, `ud-roof-quote`, `ud-proposal`), growth (`ud-leadgen`, `ud-canvass`, `ud-segment`, `ud-review-blast`, `ud-meta-capi`, `ud-ads-sync`), and operations (`ud-orchestrator`, `ud-metrics`, `ud-measure`, `ud-integration-probe`, `ud-staff-ops`). The rule that keeps this from becoming spaghetti is that exactly one component writes downstream. A durable orchestrator owns the lead → confirmation call → booking workflow, and nothing else is permitted to book, dial or email. That is not an aesthetic preference; it is the fix for double-dialling and double-booking, which are the two failure modes that make a homeowner hang up on you forever. The orchestrator is worth being precise about because it is a piece of deliberate scope reduction. The original design called for Temporal as the durable engine. Temporal needs its own multi-container cluster and a worker host, which this operation does not run. So migration 20260620000101 reverse-engineers the durability contract Temporal provides — workflow rows, idempotent run IDs with a reject-duplicate reuse policy, retries, and a tick-driven worker — directly in Postgres, and `ud-orchestrator` is the worker side of it. It is named after the thing it replaces, and the file says so in its first three lines.

40 scheduled jobs and 25 workflows doing the unattended work

Forty distinct pg_cron jobs run inside the database itself (45 `cron.schedule` calls across the migrations, some of which reschedule an existing job). They do the work that has to happen whether or not a human is awake: re-probing integration health every two hours, refreshing canvassing territory data, rolling up metrics, ageing lead states, sweeping outboxes, and firing the speed-to-lead escalation clock. Above the database, 25 GitHub Actions workflows handle everything that touches the outside world — Supabase deploys, page deploys, Brevo list synchronisation, storm-data ingestion, asset fetching and optimisation, live smoke tests, health checks, security scanning, and a daily article generator. The scheduled work is where a system like this either becomes an asset or becomes a liability, because unattended jobs fail quietly. The engine's answer is the integration probe: a job that re-tests every external credential on a two-hour cycle and writes the result to an `integration_status` table, so that 'is the ad platform connected' is a query rather than an opinion. As of the last recorded read, that table was reporting four integrations as NOT CONNECTED — see 'What we are not claiming' below. The probe being able to say so is the feature.

1,378 static pages, and a ledger that refuses unproven claims

The public marketing surface is 1,378 static HTML pages under `website/`, generated and deployed by workflow rather than authored by hand — service pages crossed with service areas, storm reports, resource articles, and a control centre. Static HTML was chosen over a framework for a specific reason: a roofing site's job is to be fast on a phone in a driveway with two bars of signal, and there is no rendering strategy that beats a file. The part of this repository we would point at first, though, is not code. It is `brain/STATE.md`, a verified-state ledger with a rule in its frontmatter: nothing enters this file as VERIFIED without a proof command and its output. It defines a five-word status vocabulary — VERIFIED, SHIPPED-UNVERIFIED, BUILT-NOT-LIVE, CLAIMED, BROKEN — and it defines CLAIMED as 'someone said it works, nobody proved it; treat as broken.' That file exists because on 5 August 2026 three systems previously reported as done were found completely broken: a password reset that never sent, an opt-out system that silently dropped every rep email, and six files a prior AI agent claimed to have deployed that existed in no branch. The ledger is the institutional response to being lied to by a tool. It is also, directly, the reason this portfolio page is written the way it is.

The numbers, and the command behind each one

What is genuinely hard about this

Not the feature count. Any team with enough time can produce 54 functions. The hard part is that a system with 239 tables and 40 unattended jobs has an enormous surface for silent divergence — between the schema you declared and the schema that exists, between the job you scheduled and the job that is running, between the integration you configured and the integration that is actually authenticated. Every one of those gaps produces the same symptom: a dashboard that looks fine while the business quietly loses money. Closing that surface is what most of the 157 guards are for, and it required accepting a genuinely unpleasant trade: the deploy pipeline is allowed to fail on things that are not obviously broken. A no-op `ALTER COLUMN TYPE` — one where the new type equals the old — halted a deploy because PostgreSQL checks view dependencies before it notices the change is a no-op. The right response was not to make the check non-blocking. It was to guard the ALTER on `atttypmod` and confirm against production that both branches skip. Non-blocking checks are how you get a green pipeline and a broken database. The second hard part is cultural and it is the reason the state ledger exists. When a large fraction of the code is written by AI agents, the dominant failure mode stops being bugs and becomes confident false reports of completion. The mitigation is not better prompting. It is a rule that a claim without a proof command and its literal output is treated as broken, and a deploy gate that does not care what anybody claimed.

What this entry does not claim

Stack and status

Built with Supabase (Postgres + Edge Functions), Deno / TypeScript, pg_cron, Row Level Security, GitHub Actions, Stripe, Brevo, Meta Conversions API, GA4, the CRM, Twilio, Anthropic Claude and Static HTML generation. Relationship: Our own company. Stage: In production. Period: 13 June 2026 — ongoing. Our role: Architecture, schema, edge functions, CI enforcement, site generation Figures re-derived on 2026-08-23.

Where a supplied figure was wrong

Re-counted against the working tree on 2026-08-25: commits 2,245 → 2,326; migrations 456 → 498; tables 239 → 244; CI guards 157 → 161; static pages 1,378 → 1,381. Edge functions (54), workflows (25) and scripts (89) held. Two days moved five of nine figures, which is the argument for leading this entry with the RLS ratio rather than a commit count: the ratio is held at parity by a guard, so it is the one number here that cannot quietly go stale.