The five stages describe the system as it is. This page is the plan to get it to production: the questions still open, the seven blockers that needed a Made-side action — one now cleared — each with step-by-step instructions, the optimizations we can offer, and the build order. The honest headline: the 33-task critical path contains zero Made-blocked tasks — we can build the identity spine, the review loop, the custodian and the backlog drain without waiting on anyone.
Three findings from measuring the running databases directly — rather than reading our own older documents — reordered this entire plan. The short version: the part we cannot touch is working, and the part that is broken is the part we can fix ourselves.
Ingestion is healthy. Mirrors were fetched this evening (21:15–21:39 UTC) against source records modified the same day. Salesforce and HubSpot are flowing.
The loop is live, not dormant. Account proposals were written at 08:30, a human recorded a decision at 09:04, the audit log is active, and the custodian ran again at 22:00.
1,941 of 3,209 Salesforce accounts have no MADE-ID match (60%). 7,244 of 17,843 HubSpot contacts have no derived company (41%).
public.contacts holds 0 rows. Two rival contact-proposal tables hold ~24k rows between them. Both learning stores are empty. Two match queues have had nothing enqueued since May/July while their 10-minute drains still run.
Three silent-failure defects, all clustered on the empty contacts table. The largest: lead-list generation reads person fields from that empty table, so any list it produced would be empty while reporting success. Across 65 campaigns / 5,207 members the old path resolves 0 leads; the repaired path resolves 4,869 across 62 campaigns.
These were latent, not active. That function is deployed on neither database and no schedule calls it; the HubSpot sync function last actually ran 2026-08-06. So all three were caught before they could cost anything — the honest framing, and a correction to our own first write-up of it.
| Measure | Value | What it tells us |
|---|---|---|
| companies (canonical) | 4,719 | 100% carry a made_id — zero without. The company anchor is sound. |
| sfdc_accounts · unmatched | 3,209 · 1,941 | 60% open The real account backlog. This is the number to move. |
| hubspot_contacts · no company | 17,843 · 7,244 | 41% open The real contact backlog. |
| public.contacts | 0 | empty The intended rich person record was never populated. Canonical person IDs live in made_id_contacts (109,324). |
| er_identity_map | 9,336 | Account crosswalk links resolved to canonical. |
| er_contact_identity_map | 112,617 | Person identity links — the deepest store in the system. |
| er_decisions | 1,919 | live Most recent 2026-09-09 09:04. Humans are reviewing today. |
| made_id_contact_match_proposals | 12,489 | authoritative Current to 2026-09-06. |
| er_contact_identity_proposals | 11,446 | superseded Stopped 2026-08-26. A half-finished migration — two producers still coexist. |
| made_id.match_queue | 3,731 pending | abandoned Nothing enqueued since 2026-05-15; drain still runs every 10 min. |
| made_id.contact_match_queue | 2,963 pending | abandoned Nothing enqueued since 2026-07-29; drain still runs every 10 min. |
| name_aliases · er_learned_rules | 0 · 0 | unbuilt The “Learn” skill has no store in use. Learning is designed, not running. |
| hubspot_v2_* (all six tables) | 0 | dead The MSS rebuild is empty — yet mss-watchdog still runs every 30 minutes guarding it. |
| er_custodian_runs | 69 | Last 2026-09-09 22:00. Runs nightly; an apply run occurred 2026-08-26. |
An earlier version of this plan proposed a “budgeted blitz” to drain the two match queues. That would have burned reviewer capacity on 6,694 rows that are four months stale while the genuine backlog — 1,941 accounts and 7,244 contacts — sat untouched. Abandoned inventory gets retired; the live backlog gets worked. Those are different operations.
Reporting’s scheduler contains no Salesforce or HubSpot contact job at all — core object extraction runs in Joe’s n8n Cloud, which we cannot edit. It is also the piece that is working correctly. Hardening it is worth doing, but doing it first would have parked the critical path behind the one dependency we do not control.
Thirty-nine questions came out of this work. We answered the thirty-one that are ours to answer, so Made is left with the eight that genuinely encode your risk appetite, business meaning, and people’s time. Every one carries a default we will proceed on if the answer is slow — nothing here stops the build.
| # | Question | Why it’s yours, not ours | What we do while we wait |
|---|---|---|---|
| M-Q1 | What is an acceptable wrong link? If we auto-link two companies and get it wrong, does that corrupt a report someone acts on, or is it a shrug? | Only Made knows the downstream commercial cost. This one number drives every automation threshold. | Assume expensive. Human review for anything short of a unique authoritative ID match. |
| M-Q2 | Should ~1,941 unmatched accounts become ~1,941 new canonical companies? Or are many duplicates, dead records, or out of scope? | It changes what your account universe means. | Mint only on unique-ID evidence; everything else queued for review. |
| M-Q3 | When Salesforce and HubSpot disagree on a company’s name, domain, industry or address — which wins? | A data-ownership policy, not a technical one. | Salesforce for account facts, HubSpot for marketing engagement; human overrides beat both. |
| M-Q4 | Who reviews, and how much time per week? | Your staffing. This sets the entire backlog drain rate — it is the hard ceiling on throughput. | Assume very limited; optimize for a small high-value queue over volume. |
| M-Q5 | What CRM data may leave the CRM, be retained raw, or enter a model’s context? Especially notes, activity bodies, personal contact details. | Privacy and compliance exposure is yours. | Named fields only. No free text into model context. Raw retention minimized. |
| M-Q6 | How fresh does this need to be? Daily is current behavior — is that fine, or is near-real-time needed? | Drives whether change-data-capture is worth the complexity and extra permissions. | Daily incremental polling. No CDC. |
| M-Q7 | Do you want automated linking at all — or is a well-run human queue with good evidence the actual goal? | A philosophy call that changes the target system. | Build the queue and the evidence first; automation stays shadow-only until M-Q1 is answered. |
| M-Q8 | Who owns this after handover? Named on-call owner, escalation path, and who may approve a repair. | Operational ownership cannot sit with a vendor indefinitely. | We document runbooks and operate it transitionally. |
M-Q1 (acceptable wrong-link rate), M-Q4 (reviewer capacity) and M-Q5 (data and privacy scope). Those three change what gets built. The other five change how it is tuned.
Listed for transparency, not as an ask. Each was resolved toward correctness over coverage, reversibility over speed, and no autonomous mutation. Any of them can be overridden — but none is waiting on anyone.
| # | Question | Our decision |
|---|---|---|
| A-Q1 | Audience for this documentation site | Made leadership with per-person accounts and server-side auth; internal-only until that exists. |
| A-Q2 | Acceptable false-link rate | Target ≤1 in 1,000 for automated classes, measured on a labeled holdout; effectively zero for merges. Made may override — M-Q1. |
| A-Q3 | Which fixtures define “correct” | Shared domains, role mailboxes, parent/subsidiaries, employer changes, cross-CRM duplicates, campaign ID mismatch, source deletion, duplicate approvals, denied fields. |
| B-Q1 | The canonical rich person record | Publish a v_person_canonical view over made_id_contacts + census + mirrors; repoint consumers. Physical table only if performance demands it. |
| B-Q2 | Which identifier the UI and exports show | MADE-ID everywhere a human reads or exports; internal UUIDs stay internal. |
| B-Q3 | What happens when a CRM record is deleted | Soft-tombstone: keep the ID, mark the source binding inactive, retain history. Never delete canonical identity. |
| B-Q4 | The legacy account crosswalk | Bypass it; keep it read-only behind a compatibility view until every consumer is traced. |
| B-Q5 | How reversible a merge must be | Soft merge only — redirect, preserve both IDs and before-images, fully reversible. Physical consolidation is later, separate work. |
| B-Q6 | Where the Reporting→Transfer outbox lives | Change journal in Reporting; Transfer pulls and owns the checkpoint. |
| B-Q7 | Quarantine visibility | Quarantine plus a visible count and a stale-quarantine alert. Invisible quarantine is how the abandoned queues happened. |
| C-Q1 | Which review path becomes the single one | Keep the currently-live path (1,919 real decisions) and port the better scoring ideas into it. |
| C-Q2 | Where reviewer identity comes from | Supabase Auth user + role claim, derived server-side. A client-supplied reviewer name is not attribution. |
| C-Q3 | Is bulk approval allowed | Yes, bounded: same rule class, cap of 50, per-item outcomes recorded. Never unbounded select-all. |
| C-Q4 | If evidence changes after approval | Return to needs_review. An approval applies to the evidence reviewed, not the record forever. |
| D-Q1 | Match before a semantic-search key exists | Yes. Deterministic and fuzzy generation now; semantic is a recall upgrade, not a prerequisite. |
| D-Q2 | Does the custodian get write authority | Never. It proposes; a separate restricted executor applies approved decisions. |
| D-Q3 | What the custodian may retry alone | Bounded retries of preapproved idempotent jobs only. Never secrets, grants, schema, deploys or cursor resets. |
| D-Q4 | Who approves a learned rule promotion | Human review plus a versioned evaluation. No automatic promotion after N confirmations. |
| D-Q5 | Model and budget for the agent | BC-absorbed for build and shadow; move to a Made key with a monthly cap before steady-state. |
| D-Q6 | Who holds the kill-switch | Brian and Joe. It halts approved execution and agent runs, never source ingestion. Fails closed. |
| E-Q1 | The 6,694 abandoned queue rows | Archive with a record; re-derive from the current mirror. Their four-month-old evidence is untrustworthy. Never delete. |
| E-Q2 | Minting policy when no match is found | Auto-mint only on a unique authoritative external ID with no plausible candidate; otherwise review. Made may override — M-Q2. |
| E-Q3 | Backlog pass shape | Canary of 100 → measure precision → budgeted batches sized to reviewer capacity, with a stop condition. |
| E-Q4 | Drain to zero, or leave ambiguity visible | Leave it visible and counted. “100% dispositioned” is the goal; “100% matched” corrupts the data. |
| F-Q1 | Secret storage | Edge Function secrets for EF-run feeds, n8n credentials for n8n-run feeds — matching where the code runs. No third store. |
| F-Q2 | Fix the ignored upsert error now or later | Now. done A failed write could report success; it now fails loudly. |
| F-Q3 | mss-watchdog over an empty pipeline | Retire the watchdog; document the tables as dead. A watchdog guarding nothing trains people to ignore alerts. |
| G-Q1 | Can one person have multiple employers | One current employer canonically, with full relationship history retained. Otherwise advisors and board members break the model. |
| G-Q2 | Free text and notes | Only named fields with a stated purpose, never into model context without redaction. Privacy dimension — M-Q5. |
| H-Q1 | Where the review console lives | Build on bc-made behind server-side auth, port into the Made app once proven. Keeps a merge off the critical path. |
| H-Q2 | Which roles can do what | Viewer / reviewer / steward / admin, enforced server-side and tested with real tokens. |
These are the only items we cannot do ourselves. Each one below is written to be executed without a follow-up call. They are not equally urgent, and we have deliberately marked which are not — two of the seven are genuinely low-impact, and treating all seven as emergencies would waste your attention on the wrong ones.
None of these blocks the 33-task critical path. All of them sit in the last third of the plan. We can build the identity spine, the review loop, the custodian and the backlog drain to completion before any of them is cleared. What they unblock is depth (more CRM fields), consolidation (retiring duplicate writers), and scale (automated linking).
Blocked: we hold no Salesforce credential. The current pipeline uses a username/password grant whose secrets live outside our hands.
Who clears it: a Salesforce admin.
describe run under a genuine integration identity. Cost of waiting Field coverage stays inferred from whatever the current pipeline happens to pull; we cannot prove coverage or detect a field we are silently missing. Not an outage risk — the existing feed keeps working.Blocked: no HubSpot credential in our hands. The contacts feed pulls ~18 hand-picked properties rather than enumerating what exists.
Who clears it: a HubSpot super admin.
0-136) is actually used in your portal.Blocked: core CRM extraction runs in Joe’s n8n Cloud. We can read those workflows but editing our repo copies does not push back, so we cannot fix an extraction defect at source.
Who clears it: Joe.
madescientific.app.n8n.cloud;Was blocked: merging to main deploys the app, and the branch policy was ambiguous.
main is the integration branch. develop had diverged, so it has been pointed back at main and the two are now identical — a PR can no longer be misrouted there.develop had changed nothing since its merge-base, and every made-id file was already on main, byte-identical — the review surfaces have been live all along. That conclusion had been drawn from commit counts rather than content.main and building clean, so this is no longer a queue.bc-made. Not urgent, and deliberately off the critical path.Blocked: semantic candidate generation is off — there is no VOYAGE_API_KEY on the box, and no agreed model-spend cap.
Who clears it: the Made account owner.
voyage-3).Blocked: automation scale, field expansion and reviewer throughput all depend on decisions that encode your risk appetite.
Who clears it: Joe plus a business owner and a privacy owner.
Blocked: dead crons keep running and two proposal tables keep diverging, but we will not disable anything in your production without approval.
Who clears it: Joe, after we present parity evidence.
made_id.match_queue and made_id.contact_match_queue drain crons — dead since May/July 2026.er_contact_identity_proposals path once every outstanding decision is accounted for.mss-watchdog and the empty hubspot_v2_* surface.pg_cron assignment and auto-merge drains are the working foundation and are not touched. Cost of waiting Alert noise, divergence, and wasted compute.The ≥0.999 auto-merge floor stays exactly where it is. Nothing here requires new privileges for us — in fact the final stage of this plan reduces our access and hands you a documented consultant-access model. See the Build order tab, Stage I.
Options at each stage, ordered by return on your effort. These are offers, not requirements. The first one is the highest-leverage thing on this page relative to what it costs you.
Grant a metadata-only CRM credential — describe and properties access with no record access at all. It unlocks the entire field catalog for both systems with almost no data-exposure risk, because it can read the shape of your CRM without reading a single customer record. If you do one thing from this page beyond answering M-Q1, do this.
| Stage | Optimization | Payoff |
|---|---|---|
| A — baseline | Metadata-only CRM credential (describe/properties, no record access) | Unlocks the whole field catalog at near-zero exposure risk. The cheapest unblock available. |
| B — identity | Confirm the account system-of-record policy early (M-Q3) | Removes rework across canonical field selection, UI display and conflict handling. |
| C — review | Nominate one reviewer as decision owner for ambiguous cases | Turns an unbounded queue into a triaged one. The biggest throughput lever that needs no automation. |
| C — review | Approve bounded bulk approval for one low-risk rule class | Multiplies reviewer output where the risk is lowest. |
| D — custodian | One holdout labeling session — ~200 pairs, roughly an hour of a reviewer’s time | Produces the precision evidence that makes every later automation decision defensible instead of speculative. Without it, thresholds are guesses. |
| E — backlog | Tell us which of the 1,941 unmatched accounts are known dead or out of scope | Could remove a large fraction of the backlog before a single match is computed. |
| F — ingestion | Move CRM secrets into managed credential storage during the same session as blockers 1–2 | Rotation and least privilege land at zero marginal cost. |
| G — fields | Confirm HubSpot tier and Leads usage in one message | Prevents us building against endpoints your portal does not have. |
| H — handover | Name the on-call owner before cutover | Avoids the classic post-handover gap where nobody is watching the queue. |
Sequenced so that what is broken and unblocked comes first, and what is working but gated comes last. An earlier draft had this backwards — it started with ingestion, which is both healthy and the one thing we cannot edit.
Reproducible baseline; protect the review surface
One person record, one proposal path, real lineage
Durable, replay-safe, attributable approvals
Five skills, shadow first, holdout evaluation
Work the real 1,941 + 7,244
Contracts + ownership — gated
Catalog + relationships — gated
Invariants, console, steady state
Least privilege + handover
| Stage | Tasks | We can do now | Needs a decision | Blocked on Made |
|---|---|---|---|---|
| A — Ground truth and safety | 5 | 3 | 2 | 0 |
| B — Consolidate the identity spine | 9 | 7 | 2 | 0 |
| C — Review loop and approved execution | 6 | 5 | 1 | 0 |
| D — Custodian, five skills | 8 | 7 | 1 | 0 |
| E — Work the real backlog | 5 | 3 | 2 | 0 |
| Critical path subtotal (A–E) | 33 | 25 | 8 | 0 |
| F — Ingestion hardening and ownership | 8 | 3 | 2 | 3 |
| G — Field depth and relationships | 8 | 4 | 1 | 3 |
| H — Registry acceptance, UX, steady state | 6 | 3 | 2 | 1 |
| I — Narrow access and hand over | 4 | 3 | 1 | 0 |
| Total | 59 | 38 | 14 | 7 |
Stages A–E contain zero Made-blocked tasks. Eight need a decision, but every one of those can be built and shadow-tested to the point where the decision is a yes/no on evidence rather than a request for permission to start. We do not need to wait to make progress.
Our working credential currently has broad rights on both databases. That is wider than a consultant should hold in steady state. We are keeping it for now because it is precisely what lets us build the 33-task critical path without putting a request in your queue for every step — and we are scheduling the reduction as a committed deliverable, not a vague intention.
| Task | What we will do |
|---|---|
| I1 | Document the real grant and correct the stale access claims in our own documentation. |
| I2 | Define the least-privilege roles the finished system actually needs — a restricted ingestion writer, a review/executor role, a read-only analyst role — and prove the system runs on them. |
| I3 | Move our automation onto the narrowed roles, retaining logged break-glass only. |
| I4 | Hand you a consultant-access model: scoped tokens, rotation schedule, attribution, and an offboarding checklist. |
Why last and not first: narrowing before the roles are known means guessing at permissions, then repeatedly asking Joe to widen them — slower and less secure than building first and narrowing once, from evidence of what the system genuinely requires.
This is the fourth pass over the same problem. Each pass was run with a different model and a stricter evidence standard, and each one found that the previous pass had been too confident. That history is worth stating, because it is the reason to trust the current version — and the reason to keep treating parts of it as unverified.
| Pass | Method | What it produced — and what was wrong with it |
|---|---|---|
| Audit | freeze-and-map | Mapped the live system: producers, crons, proposal tables, agent internals. Correctly found that the matching logic was sound and the runtime was fragmented. Established the five-stage picture this site documents. |
| Revision 1 | Astra | The first seven-phase roadmap. Structurally sound, but it inherited undated claims as if they were current facts — a field count from a code comment, a “never wrote proposals” status that was already out of date, and a stale table treated as proof that a whole subsystem was broken. |
| Revision 2 | Astra — evidence review | Introduced evidence classes: inspected code vs dated notes vs vendor docs vs proposal. Corrected roughly a dozen overconfident claims, found the field-mapping collisions and the discarded upsert error, and separated scorer eligibility from write authority. Its sequence, however, was still wrong. |
| Revision 3 | Opus 5 — live measurement | Measured both production databases directly instead of reading our own documents. That inverted the plan: ingestion turned out healthy, the identity layer turned out to be where records are lost, the “backlog” turned out to be four months of abandoned rows, and three silent-failure defects surfaced — including lead-list generation that would have produced empty lists while reporting success. |
| Revision 3a | Opus 5 — self-correction | Checking deployment state before shipping those fixes showed that none of the three defects was actually executing: one function is deployed nowhere, one has not run since 2026-08-06, one hook is unreferenced. Our own first write-up had called them live production data loss. Corrected here, in the roadmap and in the register — the same standard we applied to the older documents, applied to ourselves. |
| Consolidation | 2026-09-10 — landed on main | Everything above is now merged to main and building clean: the defect fixes, this site, the roadmap and register, and the whole research corpus (five wiki deep-dives plus the review deck). A second correction came out of it — we had reported the MADE-ID review UX as stranded off production; it never was. develop had changed nothing since its merge-base and every made-id file was already on main, byte-identical. The divergence has been closed by pointing develop back at main. Also rescued in passing: a completed commit that existed on exactly one branch and no other reference, one branch-delete away from being lost. |
A column existing does not prove it is populated, current, copied downstream, or read by anything. A function name does not prove its behavior. An old audit is not a measurement. Revision 2 made every claim carry its evidence class.
Revision 2 was still reasoning from repository files and dated notes. Measuring the running system contradicted several of them — including one of our own access documents, which described capabilities we no longer had, and which had quietly shaped the previous plan.
The measurement pass was not supposed to change code. It found three defects that were failing silently in production — so those were fixed, verified against live data, and are recorded on the Where we are tab.
Deployed function bodies have not been diffed against the repository. Tenant permissions and API entitlements are unconfirmed (that is blockers 1 and 2). The real CRM field metadata is unknown for the same reason. We have not established why enqueueing to the two match queues stopped in May 2026. And the “593 fields” figure that appears in older notes is a code comment, not a measured count — we have not reproduced it.
Every figure on this page was measured on 2026-09-09 at 22:00 UTC. Several of them moved during the day we measured them — the account and contact counts each shifted by one between the morning and evening pulls. That is normal for a live system, and it is exactly why an undated number is a liability rather than a fact.
main. Prepared for Made Scientific by BioCreative Strategies. All figures live-measured against Reporting xyopyttkhoxvnyeyijzb + Transfer jrfcfayphcmaxsixxupu, 2026-09-09 22:00 UTC. Source documents: docs/made-id-consolidation/03_OPTIMIZATION_ROADMAP.md (revision 3) + 04_DECISION_REGISTER.md. Internal — not for external share.