CMMC readiness, end to end
Theme
Phase 0 drill-down: onboarding

Client onboarding

How one button turns a won deal into an isolated client workspace: the company is looked up, a template base is cloned with every reference table and view, and the first readiness assessment is seeded and ready to work.

1
button on the client's CRM row starts the whole kickoff
7.7 min
from the press to Ready, measured on a live re-run
4,569
assessment objectives checked count-for-count on the clone
0
lines of per-client code: every script targets whichever base it is told to

The whole pipeline

Two buttons on one CRM row. Each goes through the same gate to a different runner.

Call Write-back to the Clients row Dashed box: outside service

The two jobs run on different containers on purpose. Enrich needs the internet and a language model, so it runs on the runner that has egress. Kickoff only talks to the NocoDB API, so it runs on the runner that has none. Both report through alerts only when something fails.

1

Identify: enrich the CRM row

Find the legal entity and its identifiers, and never guess which one it is.

The lookup, in order

  1. Website first, only when there are no identifiers yet. If the row has a domain but no UEI or CAGE, the job fetches the homepage (20-second timeout, first 400 KB, scripts and markup stripped, 8,000 characters kept) and a language model returns a one-sentence description, an industry, and a legal name only if the exact legal entity name is stated. If the fetch fails, the hop is skipped.
  2. SAM.gov second, most exact key first. The Entity API is searched by SAM UEI, then HLO CAGE, then by name. The name from the website is only ever a search term. It is never written as the legal name unless SAM.gov confirms it.
  3. Write what was confirmed. Legal name, UEI, HLO CAGE, registration status with its expiry, physical address and up to five NAICS codes.

What each result does to the row

SAM.gov returnsWritten to the rowEnrichment
An exact UEI or CAGE hitAll identifiersEnriched
One match on a name searchAll identifiers, plus a note to verify it is the right entityEnriched
Several matches on a name searchNothing. The top six candidates (name, UEI, CAGE, city, status) go in Enrichment NotesNeeds confirm
No matchA note saying soNot found

"Needs confirm" is the design working. The consultant picks the right entity, pastes its UEI, and presses Enrich again, which now takes the exact-match path.

Where the identity lands in the client base

TableHoldsWhy it is shaped this way
Client_ProfileOne row: Legal Name, DBA, SAM UEI, HLO CAGE, Target Level, Engagement Stage, Primary Contact, and later the SSP's system identity fieldsLegal Name is the single source for the {{company_name}} variable used in every generated statement and document
CAGE_CodesOne row per facility or division: CAGE Code, registered entity, address, Is PrimaryThe HLO CAGE names the one parent organization, so it is a single field. A client can have many facility CAGEs, each with its own entity name and address, so those get a table
2

Clone: kick off the engagement

A copy of the template, not a shared database with a client column.

Every client gets its own NocoDB base. There are no links between bases, so one client's rows cannot appear in another client's views, exports or jobs. The template is the working crosswalk: frameworks, requirements, assessment objectives, policy statements, triage questions and technologies, with every client table staged empty.

What travels with the clone

Part of the templateIn the cloneWhy
Reference data (37 tables in all, including 1,735 requirements and 4,569 assessment objectives)Copied in fullEvery assessment needs it, and a copy lets a client base be worked without touching the template
Views, including the 11 "what to do next" views and the phase marker on every tableCopiedBuilt once in the template, so every client inherits the same guided surface
Webhooks behind the action buttonsCopiedThe webhook URLs name no base. n8n reads the base from each call, so a button in a client base runs the job against that base
Client tables (people, inventory, flows, evidence, assessments)Copied emptyThey are staged empty in the template, so nothing needs pruning after the copy

The kickoff, in run order

  1. Check for an existing base. If the row already carries an Engagement Base Id, the job skips the clone and resumes the seeding against that base.
  2. Duplicate the template with data, views and hooks. The API answers at once with the new base id; the copy itself runs in the background.
  3. Wait for the tables with an uncached read, up to 4 minutes. Then clear the table lookup cache for the new base (see the decision log for why).
  4. Wait for the data until the clone's Frameworks, Requirements and Assessment Objectives counts equal the template's, checked every 6 seconds for up to 20 minutes. Counting against the template means the wait calibrates itself as the template grows.
  5. Rename the base to the company name, and write the base id, link and time back to the Clients row.
  6. Seed the identity: Client_Profile (Engagement Stage "Onboarding"), the facility CAGE codes from the CRM, and the client variables, so {{company_name}} already resolves in step 7.
  7. Open one assessment per selected level. Each is titled "Company: CMMC Level N readiness assessment", linked to its framework, then populated with its objectives and requirement rows, the review console, and draft implementation statements seeded from the policy library. A CMMC Level 2 assessment comes out at 110 requirements and 320 objectives.
  8. Open the client's intake surfaces: a read-only list of evidence requests plus "Submit evidence" and "Submit a document" forms, each with its public share link turned on.
  9. Mark the row Stage "Active" and Kickoff Status "Ready". Any failure on the way sets "Failed" and exits non-zero, which is what raises the alert.
Re-pressing is safe after step 5. Every later step finds or patches what exists instead of adding a second copy: the profile, each CAGE code, each assessment by title, each framework link, the variables and the objectives. Before step 5, a re-press clones again; that gap is listed under known gaps below.
3

Target: base-aware automation

One set of scripts, pointed at whichever client base pressed the button.

What stays the same across clones, and what doesn't

Stable in every cloneNew in every clone
Table titles and column titlesThe base id
Foreign-key field names such as frameworks_idEvery table id and column id
Business keys: a requirement's number within its framework, an objective within its requirementLink column ids and view ids

So scripts look tables up by title inside the target base and never carry a hard-coded id. Sixteen engagement scripts and 86 hard-coded ids were converted when cloning was built.

How a job finds its base

  1. The NocoDB webhook sends the calling base's id with the row. n8n passes it to the runner as base.
  2. The runner checks the id against a strict pattern and hands it to the job only as the NOCODB_BASE_ID environment variable, never on a command line or through a shell.
  3. The job resolves every table by title within that base. With no base given, it targets the template, so shared-base behavior never changed.
  4. Jobs run one at a time per job and base. Different clients' jobs run side by side; a second press of the same job on the same base gets "busy". For Enrich and Kick off engagement, n8n then retries every 30 seconds, up to eight times, before alerting.
  5. For these two buttons the runner clears the trigger field itself, confirms the clear by reading it back, and only then runs the job. If the clear can't be confirmed, the job doesn't run.

Reference corrections after the clone

A clone is a snapshot, so a fix to a control or a policy statement in the template does not reach existing clients by itself. push_reference_data.py carries it out, touching only an allowlist of 15 reference tables. Client tables are never read or written.

Template row vs client row, matched by business keyAction
Matched, with a field that differsUpdate those fields
Missing in the client baseInsert it
Only in the client baseFlag it, never delete
Two client rows share the keyRefuse, and report the ambiguity
A column is missing or renamed in the client baseSkip that table and flag it; other tables and bases still run

Matching is by business key, not row id, so a re-run finds nothing to do. It dry-runs by default and writes only with --apply.

Decision log

The calls that shaped onboarding, and when they were made.

DecisionWhyWhen
One base per client, cloned from the full crosswalkHard isolation; the paid sync feature replaced by a push script2026-07-17
Client identity tables built into the templateAdding them after the first clone would mean retrofitting every client2026-07-20
Resolve tables by title; pass the base as an environment variableThe same scripts drive any base with no per-client code2026-07-23
Clone with the webhooksButtons work in the new base immediately; routing follows the caller's base id2026-07-23
The guided views live in the template, with no filtersClones inherit them, and view filters were what broke duplication2026-07-27
One-button kickoff from the CRM, rename includedRename turned out to work with the API identity, so no step is left for a person2026-07-29
Several levels can be kicked off at once; CAGE and UEI are optionalOne assessment per level; a base can be stood up before the identifiers are known2026-07-29
Enrich flags, never guessesA wrong legal entity poisons every document generated after it2026-07-29
Clear the trigger inside the runner, confirmed by read-backA write-back loop once fired 64 enrich runs in 25 seconds2026-09-17
Busy means retry, then alertA second press during a run should wait, not vanish silently2026-09-17

Two traps the clone taught us

Known gaps