Skip to content

Identity Graph

How Kanoniv builds and maintains the many-to-one identity mapping — and how you control it with merge and split.

The Graph Model

Kanoniv maintains an identity graph where:

  • Nodes are external entities (records from source systems)
  • Edges connect external entities to canonical entities
  • Each canonical entity is a hub linking multiple external records
                    ┌─────────────────────┐
                    │   Canonical Entity   │
                    │     550e8400-...     │
                    │                     │
                    │  name: John Doe     │
                    │  email: john@acme   │
                    │  plan: enterprise   │
                    └──────────┬──────────┘

            ┌──────────────────┼──────────────────┐
            │                  │                  │
            ▼                  ▼                  ▼
   ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
   │ crm:contact_1  │ │ billing:cus_42 │ │ mktg:lead_789  │
   │                │ │                │ │                │
   │ John Doe       │ │ Jonathan Doe   │ │ J. Doe         │
[email protected]  │ │ [email protected]  │ │ [email protected]
   │ 555-0101       │ │ plan:enterprise│ │ source:webinar │
   └────────────────┘ └────────────────┘ └────────────────┘

Three different systems, three different records, three different names — but the same person. The identity graph connects them to a single canonical entity with survivorship rules determining which values win.

How the Graph Gets Built

The graph is built automatically during reconciliation based on the matching rules in your spec.

Step 1: Reconcile

python
from kanoniv import Spec, Source, reconcile

spec = Spec.from_file("customer-spec.yaml")
sources = [
    Source.from_csv("crm", "crm.csv"),
    Source.from_csv("billing", "billing.csv"),
]

result = reconcile(sources, spec)

Step 2: Inspect Clusters

The result contains clusters — groups of record IDs that resolved to the same canonical entity:

python
for i, cluster in enumerate(result.clusters):
    print(f"Cluster {i + 1}: {cluster}")
Cluster 1: ['crm:1', 'billing:cus_001']
Cluster 2: ['crm:2', 'billing:cus_002']
Cluster 3: ['crm:3', 'billing:cus_003']
Cluster 4: ['crm:4']
Cluster 5: ['billing:cus_006']

Clusters 1–3 are merged pairs (the engine matched them above the threshold). Clusters 4 and 5 are singletons — records with no match in the other source.

Step 3: Inspect Golden Records

Each cluster produces a golden record with survivorship applied:

python
for record in result.golden_records:
    print(record)
python
{
    'name': 'John Doe',           # from crm (source_priority winner)
    'email': '[email protected]',     # from both (identical)
    'phone': '555-0101',          # from crm (only source with phone)
    'card_last4': '4242',         # from billing (only source with card)
    'plan': 'enterprise',         # from billing (only source with plan)
}

Step 4: Understand Why

Every cluster comes with a decision explaining which rules fired:

python
for decision in result.decisions:
    print(decision)
python
{
    'left': 'crm:1',
    'right': 'billing:cus_001',
    'score': 1.0,
    'outcome': 'match',
    'rules': [{'name': 'email_exact', 'score': 1.0, 'field': 'email'}]
}

Score 1.0, matched on email_exact — that's why crm:1 and billing:cus_001 ended up in the same cluster.



Merge: Combining Identities

Merge is the most common graph mutation. It happens in two ways:

Automatic Merge (Engine)

During reconciliation, the engine scores every candidate pair against your spec's matching rules. Pairs scoring above the match threshold are merged automatically:

yaml
# In your spec
decision:
  thresholds:
    match: 0.9     # auto-merge above this
    review: 0.7    # flag for review between 0.7–0.9

When two records merge, the engine:

  1. Links both external entities to the same canonical ID
  2. Applies survivorship rules to build the golden record
  3. Logs the decision with confidence score and matched rules

Manual Merge (Force Merge)

Sometimes the engine doesn't catch a match - different email domains, typos in every field, or a confidence score that falls just below the threshold. Force merge lets a human override the engine's decision.

What happens after a force merge:

BEFORE                              AFTER

crm:contact_123 -> can_001          crm:contact_123  -> can_001
  name: John Doe                    billing:cus_456  -> can_001  (now linked)
  email: [email protected]
                                    Golden Record:
billing:cus_456 -> can_002            name: John Doe         <- crm (priority)
  name: Jonathan Doe                  email: [email protected]   <- crm (priority)
  email: [email protected]                plan: enterprise       <- billing (only source)
  plan: enterprise                    card_last4: 4242       <- billing (only source)

The old can_002 is absorbed into can_001. Survivorship re-runs to produce an updated golden record.


Split: Correcting False Positives

Split is the inverse of merge — it separates records that were incorrectly linked. False positives happen when two different people share a field value (same name, shared company email alias, recycled phone number).

Example: Two John Smiths

Your CRM has two contacts named "John Smith" at Acme Corp. One is the CFO, the other is a sales rep. The engine merged them because they share name + company:

INCORRECTLY MERGED

crm:contact_100 -> can_500          <- John Smith, CFO, [email protected]
crm:contact_200 -> can_500          <- John Smith, Sales Rep, [email protected]
billing:cus_777 -> can_500          <- John Smith, enterprise plan

The golden record now has data from three external records, but two of them are different people. A split override separates them:

BEFORE (incorrect)                  AFTER (corrected)

crm:contact_100 -> can_500          crm:contact_100  -> can_500   (CFO)
crm:contact_200 -> can_500          billing:cus_777  -> can_500   (CFO's billing)
billing:cus_777 -> can_500
                                    crm:contact_200  -> can_901   (new canonical for sales rep)

A new canonical (can_901) is created for the split-out record. The original canonical's golden record is recomputed without the removed record's data.

Preventing Re-Merge with Locks

After splitting, the engine might re-merge the same records on the next reconciliation run (they still share name + company). A lock override prevents this permanently. The engine will skip this pair even if it scores above the match threshold.


Graph Integrity

The identity graph enforces strict structural rules:

  • No orphans — Every external entity links to exactly one canonical
  • No cycles — The graph is a forest of trees rooted at canonicals
  • Idempotent — Running the same reconciliation twice produces the same graph
  • Audit trail — Every mutation (merge, split, lock) is logged with timestamp, actor, and reason

Audit Trail

Every graph mutation is permanently recorded - merges, splits, locks, and field overrides. Each event includes a timestamp, actor, and reason, so you can always answer "who changed this record, when, and why?"

Next Steps

Need real-time graph queries, overrides API, and audit history?

Kanoniv Cloud adds a persistent identity graph with sub-millisecond resolution, search, and a full overrides API.

The identity and delegation layer for AI agents.