Survivorship
When multiple source systems describe the same customer, they rarely agree. Your CRM says "John Doe", billing says "Jonathan Doe", and marketing says "Johnny Doe". After the matching engine merges these records into a single canonical entity, survivorship is the stage that decides which value makes it into the golden record — and which values get left behind.
Every field in every golden record was chosen by a survivorship rule. Without survivorship, you'd just get the last value written, which is almost never the right answer.
The Problem
Consider three source systems that all describe the same customer:
| Field | CRM (Salesforce) | Billing (Stripe) | Marketing (HubSpot) |
|---|---|---|---|
| [email protected] | [email protected] | [email protected] | |
| name | John Doe | Jonathan Doe | Johnny Doe |
| company | Acme | Acme Corporation | ACME Corp |
| phone | 555-0101 | (empty) | 555-0199 |
| plan | (empty) | enterprise | (empty) |
| industry | (empty) | (empty) | SaaS |
| tags | (empty) | vip, annual | enterprise, target |
The matching engine merged all three into one canonical entity. But which email goes in the golden record? Which name? Who's right about the phone number?
That's what survivorship answers. You declare the rules, and the engine applies them deterministically on every reconciliation run.
Strategies
Survivorship is configured in the survivorship section of your spec. You can set a global default and override it per field.
Source Priority
Rank sources by trustworthiness. The highest-priority source with a non-null value wins.
survivorship:
strategy: source_priority
source_order: [crm, billing, marketing]Applied to our example:
| Field | Winner | Value | Why |
|---|---|---|---|
| CRM | [email protected] | CRM is #1 priority and has a value | |
| name | CRM | John Doe | CRM is #1 priority |
| company | CRM | Acme | CRM is #1 priority |
| phone | CRM | 555-0101 | CRM is #1 priority and has a value |
| plan | Billing | enterprise | CRM has no value, falls through to #2 |
| industry | Marketing | SaaS | CRM and Billing have no value, falls to #3 |
Source priority is the most common strategy — use it when you have a clear hierarchy of trust. CRM data entered by sales reps is usually more reliable than auto-captured marketing data.
Per-Field Priority
Different fields can trust different sources. Your billing system might be authoritative for payment data, while CRM is authoritative for contact data:
survivorship:
# Default: trust CRM first
strategy: source_priority
source_order: [crm, billing, marketing]
# Override: billing is authoritative for email (it's verified by Stripe)
email:
strategy: source_priority
priority: [billing, crm, marketing]
# Override: marketing has the best enrichment data
industry:
strategy: source_priority
priority: [marketing, crm, billing]Now the golden record changes:
| Field | Winner | Value | Strategy |
|---|---|---|---|
| Billing | [email protected] | Per-field: billing is authoritative | |
| name | CRM | John Doe | Default: crm first |
| company | CRM | Acme | Default: crm first |
| industry | Marketing | SaaS | Per-field: marketing is authoritative |
Most Recent
Use the most recently updated value. Best for fields that change frequently — phone numbers, addresses, job titles.
survivorship:
phone:
strategy: most_recentIf marketing updated the phone number yesterday and CRM's phone is from 6 months ago, marketing wins. On the next reconciliation run, if CRM updates their phone number, CRM would win.
Best for: Contact info, mailing addresses, job titles — fields where the latest update is most likely to be correct.
Most Complete
Use the value with the most data. For strings, the longest non-empty value wins. For objects, the one with the most non-null fields wins.
survivorship:
company:
strategy: most_complete| Source | company | Length |
|---|---|---|
| CRM | Acme | 4 |
| Billing | Acme Corporation | 16 |
| Marketing | ACME Corp | 9 |
Winner: Billing's "Acme Corporation" (longest string).
Best for: Company names, addresses, descriptions — fields where more detail is better and sources vary in completeness.
Aggregate
Instead of picking one winner, combine values from all sources. Choose an aggregation function:
survivorship:
tags:
strategy: aggregate
method: union| Function | What It Does | Example |
|---|---|---|
union | Merge all unique values | ["vip", "annual", "enterprise", "target"] |
intersection | Keep only values present in all sources | [] (no overlap) |
max | Largest numeric value | Useful for employee_count across sources |
min | Smallest numeric value | Useful for first_seen_date |
sum | Add numeric values | Useful for total_spend across products |
avg | Average numeric values | Useful for nps_score across touchpoints |
Best for: Tags, categories, labels, and numeric aggregations — fields where data from all sources adds value rather than conflicts.
Worked Example
Here's a complete 3-source spec showing survivorship in action:
api_version: kanoniv/v1
identity_version: "1.0"
entity:
name: customer
sources:
crm:
adapter: snowflake
location: ANALYTICS.CORE.CRM_CONTACTS
primary_key: contact_id
schema:
email: { type: string, pii: true }
name: { type: string }
company: { type: string }
phone: { type: string, pii: true }
billing:
adapter: postgres
location: public.stripe_customers
primary_key: customer_id
schema:
email: { type: string, pii: true }
full_name: { type: string }
plan: { type: string }
marketing:
adapter: csv
location: hubspot_export.csv
primary_key: lead_id
schema:
email: string
name: string
industry: string
tags: string
rules:
- name: email_exact
type: exact
field: email
weight: 1.0
decision:
thresholds:
match: 0.9
review: 0.7
survivorship:
# Default for most fields: CRM is source of truth
strategy: source_priority
source_order: [crm, billing, marketing]
# Billing email is Stripe-verified
email:
strategy: source_priority
priority: [billing, crm, marketing]
# Phone changes often — use latest
phone:
strategy: most_recent
# Marketing has the best enrichment data
industry:
strategy: most_complete
# Merge all tags from every source
tags:
strategy: aggregate
method: unionRun reconciliation and inspect the golden record:
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"),
Source.from_csv("marketing", "marketing.csv"),
]
result = reconcile(sources, spec)
for record in result.golden_records:
print(record){
'email': '[email protected]', # billing (per-field priority)
'name': 'John Doe', # crm (default priority)
'company': 'Acme', # crm (default priority)
'phone': '555-0199', # marketing (most recent update)
'plan': 'enterprise', # billing (only source with value)
'industry': 'SaaS', # marketing (most complete)
'tags': 'vip, annual, enterprise', # all sources (aggregate union)
}Every field was chosen by a rule. Every rule is declared in the spec. Every reconciliation run produces the same result.
Field Provenance
The canonical record always tracks which source contributed each field — you never have to guess where a value came from:
{
"canonical_data": {
"email": "[email protected]",
"name": "John Doe",
"company": "Acme",
"phone": "555-0199",
"plan": "enterprise",
"industry": "SaaS"
},
"field_provenance": {
"email": "billing",
"name": "crm",
"company": "crm",
"phone": "marketing",
"plan": "billing",
"industry": "marketing"
}
}This is critical for debugging and compliance - when a customer asks "where did you get my phone number?", the answer is in the provenance.
Override Priority
Manual field overrides always take the highest precedence:
Manual Override > Survivorship Strategy > Default (first non-null)If an admin pins email = "[email protected]" via a field override, that value persists regardless of what any source reports - even if a higher-priority source has a different value. The override is preserved across reconciliation runs until it's explicitly revoked. See Overrides for details.
Strategy Comparison
| Strategy | Picks One Winner? | Best For | Deterministic? |
|---|---|---|---|
source_priority | Yes | Fields with a clear trust hierarchy | Yes (same sources -> same result) |
most_recent | Yes | Frequently changing fields (phone, address) | No (depends on update timestamps) |
most_complete | Yes | Fields where more detail is better | Yes (same data -> same result) |
aggregate | No (combines all) | Tags, lists, numeric rollups | Yes |
Starting out? Use source_priority as your default strategy and override individual fields as needed. It's the most intuitive and predictable.
