Skip to content

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:

FieldCRM (Salesforce)Billing (Stripe)Marketing (HubSpot)
email[email protected][email protected][email protected]
nameJohn DoeJonathan DoeJohnny Doe
companyAcmeAcme CorporationACME Corp
phone555-0101(empty)555-0199
plan(empty)enterprise(empty)
industry(empty)(empty)SaaS
tags(empty)vip, annualenterprise, 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.

yaml
survivorship:
  strategy: source_priority
  source_order: [crm, billing, marketing]

Applied to our example:

FieldWinnerValueWhy
emailCRM[email protected]CRM is #1 priority and has a value
nameCRMJohn DoeCRM is #1 priority
companyCRMAcmeCRM is #1 priority
phoneCRM555-0101CRM is #1 priority and has a value
planBillingenterpriseCRM has no value, falls through to #2
industryMarketingSaaSCRM 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:

yaml
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:

FieldWinnerValueStrategy
emailBilling[email protected]Per-field: billing is authoritative
nameCRMJohn DoeDefault: crm first
companyCRMAcmeDefault: crm first
industryMarketingSaaSPer-field: marketing is authoritative

Most Recent

Use the most recently updated value. Best for fields that change frequently — phone numbers, addresses, job titles.

yaml
survivorship:
  phone:
    strategy: most_recent

If 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.

yaml
survivorship:
  company:
    strategy: most_complete
SourcecompanyLength
CRMAcme4
BillingAcme Corporation16
MarketingACME Corp9

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:

yaml
survivorship:
  tags:
    strategy: aggregate
    method: union
FunctionWhat It DoesExample
unionMerge all unique values["vip", "annual", "enterprise", "target"]
intersectionKeep only values present in all sources[] (no overlap)
maxLargest numeric valueUseful for employee_count across sources
minSmallest numeric valueUseful for first_seen_date
sumAdd numeric valuesUseful for total_spend across products
avgAverage numeric valuesUseful 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:

yaml
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: union

Run reconciliation and inspect the golden record:

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"),
    Source.from_csv("marketing", "marketing.csv"),
]

result = reconcile(sources, spec)

for record in result.golden_records:
    print(record)
python
{
    '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:

json
{
  "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

StrategyPicks One Winner?Best ForDeterministic?
source_priorityYesFields with a clear trust hierarchyYes (same sources -> same result)
most_recentYesFrequently changing fields (phone, address)No (depends on update timestamps)
most_completeYesFields where more detail is betterYes (same data -> same result)
aggregateNo (combines all)Tags, lists, numeric rollupsYes

Starting out? Use source_priority as your default strategy and override individual fields as needed. It's the most intuitive and predictable.

The identity and delegation layer for AI agents.