Salesforce logoDeep dive

Anatomy of a Governed Org: How Salesforce Metadata Maps into the CoreModels Graph

A drift audit is only as trustworthy as the mapping underneath it. If an import flattens your org into undifferentiated strings, the audit can only ever tell you undifferentiated things. So this article opens the hood on the CoreModels Salesforce connector: what each describe property becomes in the governed graph, which platform semantics survive the crossing, which are approximated — and how every approximation is declared rather than hidden.

Anatomy of a Governed Org: How Salesforce Metadata Maps into the CoreModels Graph

A drift audit is only as trustworthy as the mapping underneath it. If an import flattens your org into undifferentiated strings, the audit can only ever tell you undifferentiated things. So this article opens the hood on the CoreModels Salesforce connector: what each describe property becomes in the governed graph, which platform semantics survive the crossing, which are approximated — and how every approximation is declared rather than hidden.

Start with a single field, as it arrives in a describe result:

{
  "name": "Status__c",
  "label": "Status",
  "type": "picklist",
  "custom": true,
  "nillable": false,
  "restrictedPicklist": false,
  "inlineHelpText": "Lifecycle state of the invoice.",
  "picklistValues": [
    { "value": "Draft", "active": true },
    { "value": "Sent", "active": true },
    { "value": "Void", "active": false }
  ]
}

By the time import finishes, this one JSON object has become: an Element on the Invoice__c Type; a NotNull check; a Taxonomy containing Draft and Sent (not Void — inactive values are excluded); a description from the help text; a preserved native type; and a stored flag — restrictedPicklist: false — that will make the audit warn you that the platform is not enforcing this value set. Every piece of that is worth understanding, so let's take the mapping layer by layer.

Objects and fields: the structural skeleton

Each sObject in the extract becomes a governed Type with a deterministic id derived from its API name; the API name stays as the label, and the describe label becomes the description. Each field becomes an Element on its Type, again with a deterministic per-object id, with inlineHelpText as its description — the closest thing describe has to field documentation, which is exactly why its absence on a custom field is an audit finding.

An object only parses if it has both a name and a fields array; the parser is otherwise open-world — a bare array, a single describe object, or a {"sobjects": [...]} wrapper all work, objects missing either property are skipped, and only a genuinely unusable payload errors. The parse also computes org-level facts the audit can display: total objects, how many are custom, and externalReferences — the count of lookups pointing at objects outside your extract. That last number is your governance boundary made visible: it tells you how much of the org's relational web your extract actually captured. The whole artifact is fingerprinted (a 16-hex-character SHA-256 prefix), so every later audit can say precisely which export it judged.

Native types: preserved exactly, mapped honestly

The connector keeps the platform's own type language, parameters included: string(255), currency(18,2), int(8), textarea(32768), reference(Account) — and for polymorphic lookups, the full list, as in reference(User|Group). That verbatim native type rides the vendor metadata and round-trips.

For cross-format semantics, each native type also maps to a governed primitive — and the connector records an approximation flag whenever the mapping loses something:

Salesforce typeGoverned primitiveApproximation recorded?
intIntegeronly when a digits bound is present (the bound is dropped)
double, currency, percentDoublealways — precision/scale and the currency/percent semantics are dropped
booleanBooleannever
date, datetimeDateTimenever
string, idStringonly when a length parameter is present
textarea, picklist, multipicklist, combobox, email, phone, url, encryptedstring, referenceStringalways — length and format semantics are dropped
everything else (address, location, base64, anyType, time, …)Stringalways

Read that table the way we wrote it: the governed primitive is deliberately coarse, because it exists to compare meaning across systems, while the exact platform type survives in the metadata for anything that needs Salesforce fidelity — including generation, which reads it back.

Checks: the platform's own constraints, normalized

Three describe properties become normalized checks. nillable: false becomes a NotNull check. unique: true — and type: "id", which is unique by construction — becomes a Unique check. A lookup becomes a Relationship check alongside its reference.

One boundary is worth stating plainly: the CoreModels metamodel has no first-class required/nullable slot, so "required" is persisted on a declared element-facts mixin — a stash, not a silent promotion into something it isn't. The check also survives as structured metadata on the field's vendor mixin, and the XML generator reads NotNull back from exactly there when it emits required. Nothing disappears; it just lives honestly in the bookkeeping layer.

Picklists, lookups, and the polymorphic compromise

Active picklist values become a governed Taxonomy wired to the element as a controlled list — active values only, duplicates dropped. The restrictedPicklist flag is preserved precisely because the governed side needs it: when it is false, Salesforce itself accepts values outside the set, so the taxonomy in CoreModels is the only control that exists. The audit's picklist-unrestricted warning is that fact, surfaced.

Single-target lookups become governed references to {TargetObject}.Id. Polymorphic lookups — OwnerId pointing at User or Group is the classic — force a choice, and the connector makes it visibly: the first target becomes the governed reference, the full target list is preserved in metadata, and the audit reports the narrowing on every run rather than letting you forget it happened:

{ "section": "Conformance", "severity": "Info", "code": "polymorphic-reference",
  "subject": "Case.OwnerId",
  "message": "Polymorphic lookup — only the first target is governed as a reference.",
  "detail": "targets: User,Group" }

References to objects outside your extract are narrowed with declared lossiness too — and counted in the externalReferences fact, so widening the extract is an informed decision instead of a surprise.

What rides the vendor-metadata mixin

Everything Salesforce-specific that has no neutral slot travels in the per-vendor metadata mixin (surfaced as Salesforce Metadata in the project), under sf.* keys — never as new modeling concepts:

KeyOnMeaning
sf.customobject & fieldcustom vs standard
sf.keyPrefixobjectthe 3-character record-id prefix
sf.labelobject & fieldthe display label
sf.relationshipNamefieldthe relationship traversal name
sf.restrictedPicklistfieldwhether the platform enforces the value set
sf.polymorphicfieldthe full comma-joined target list of a polymorphic lookup
sf.externalIdfieldmarked as an external id
sf.calculatedfieldformula/rollup field

Alongside the mixin, every imported node carries a vendor-identity assertion — a mapsTo value with standard salesforce and the API name as its URI. That identity is queryable and round-trips, and it is how imports and audits recognize governed nodes; the generator reads the recorded API name in the vendor metadata to tell a Type that came from Salesforce (keep its API name) from one authored governed-first (mint a new __c name).

One structural honesty note: the integration layer supports lineage edges, but the Salesforce import writes none today — childRelationships are not yet mapped to lineage, so lineageEdgesAdded is an honest zero rather than a decorative DAG.

The audit vocabulary for Salesforce

Findings come from two layers. The core engine contributes the vendor-neutral codes — Coverage (dataset-unmapped, field-unmapped) and Drift (dataset-removed, field-removed, field-type-drift, enum-constraint-removed, enum-narrowed, enum-widened, contract-drift). On top of those, the Salesforce connector contributes three rules of its own, each tied directly to a mapping decision described above:

CodeSeverityWhat it tells you
field-no-helpInfoa custom field has no inline help text — its meaning lives only in tribal knowledge
picklist-unrestrictedWarningthe platform does not enforce the value set; the governed taxonomy is the only control
polymorphic-referenceInfoonly the first lookup target is governed as a reference

The codes are stable and kebab-case by design — automation can key on them, count them per run, and trend them over the audit history.

The return trip: generation

The same mapping runs in reverse. From the governed model, the connector emits Metadata-API CustomObject XML scaffolds — one objects/{ApiName}.object per governed Type. Taxonomies come back as restricted picklist value sets (the scaffold asks the platform to enforce what the org previously didn't), references come back as Lookup fields with minted relationship names, NotNull comes back as required, Unique as unique. Numbers default to precision 18 (scale 0 for integers, 2 for doubles), text to length 255, and standard fields — Id, Name, the audit fields, OwnerId — are never scaffolded.

Generation has its own declared compromises: a Salesforce checkbox cannot be required, so a NotNull on a boolean is dropped with a lossiness record; a governed Type with nothing but standard fields is skipped, and says so. The scaffolds are for review and deployment through your org's normal change process — CoreModels never deploys to your org, and the generated file opens with a comment telling you to regenerate rather than hand-edit.

Limits, stated plainly

  • No live connection. LiveSync is a declared-but-deferred capability; CoreModels never holds your Salesforce credentials, and every import starts from an export you ran.
  • Help text only. Describe carries inlineHelpText but not the Metadata API's long object/field descriptions, so those are not extracted.
  • No lineage yet. Child relationships do not become lineage edges today.
  • Snapshot cap. Very large orgs can exceed the stored-snapshot cap (~1.5 MB encoded): import still succeeds and reports snapshotStored: false with a lossiness record, fresh-artifact audits work, but the one-call re-audit has nothing stored to run against.
  • Approximations are recorded, not silent. Currency and percent semantics, length bounds, format types, and polymorphic breadth all degrade to something coarser in the neutral model — and every one of those degradations is either an approximation flag, a metadata entry, a lossiness record, or an audit finding.

That last point is the connector's design philosophy in miniature. A governed model you can trust is not one that claims to capture everything — it is one that tells you exactly what it captured, what it approximated, and where its edges are.

For the hands-on version of everything here — extraction recipe, routes, CI gate, and MCP tools — see the Salesforce quickstart in the CoreModels docs (quickstarts/salesforce).