Skip to content

Survivorship

Survivorship determines which source's value "wins" when multiple sources provide the same field. After records are matched and merged into a canonical entity, survivorship rules resolve conflicts to produce a single golden record.

Without survivorship rules, you'd be left with duplicate values for every field: three different emails, two phone numbers, conflicting addresses. Survivorship is the final step that turns matched clusters into clean, authoritative records.

How It Works

Source A ─┐                      ┌─ Golden Record
Source B ─┼─ Match & Merge ──→  │  (one value per field)
Source C ─┘                      └─ Survivorship decides each field

Survivorship runs after matching and after the decision step has determined which records belong together. For each field in the merged cluster, the survivorship strategy picks exactly one value (or computes an aggregate).

Strategies

source_priority

The simplest and most predictable strategy. You define a ranked list of sources per field (in the override's priority list), and for each field, the value is taken from the highest-priority source that has a non-null value.

yaml
survivorship:
  default: source_priority
  overrides:
    - field: name
      strategy: source_priority
      priority: [crm, billing, support]
    - field: email
      strategy: source_priority
      priority: [crm, billing, support]

Example: Three sources provide data for a matched customer entity, with priority [crm, billing, support]:

FieldCRM (priority 1)Billing (priority 2)Support (priority 3)Surviving Value
nameJane SmithJane S.J. SmithJane Smith (CRM)
email[email protected]null[email protected][email protected] (CRM)
phonenullnull+1-555-0142+1-555-0142 (Support)
plannullenterprisenullenterprise (Billing)
last_ticketnullnull2026-01-152026-01-15 (Support)

The rule is straightforward: walk down the priority list until you find a non-null value. CRM is checked first, then billing, then support. The first non-null value wins.

When to use: You have a clear hierarchy of data trust. CRM is the system of record, billing is authoritative for payment data, and support fills in gaps.

most_recent

The most recently updated value wins. The engine uses the record's last-modified timestamp to determine recency. This is the default strategy when no survivorship block is specified.

yaml
survivorship:
  default: most_recent

Example:

FieldCRM (updated 2026-01-10)Billing (updated 2026-01-18)Surviving Value
nameJane SmithJane S.Jane S. (Billing, more recent)
email[email protected][email protected][email protected] (Billing, more recent)
phone+1-555-0100null+1-555-0100 (CRM, only non-null)

The timestamp comparison is per-record, not per-field. If billing was updated more recently, all of billing's non-null values take precedence. Null values are never selected regardless of timestamp.

When to use: Your sources are updated at different cadences and you trust the most recent write. Common in event-driven architectures where the latest state is the most accurate.

most_complete

The source with the most non-null fields wins overall. The entire record is taken from whichever source has filled in the most columns.

yaml
survivorship:
  default: most_complete

Example:

FieldCRM (4/5 fields)Billing (3/5 fields)Support (5/5 fields)Surviving Value
nameJane SmithJane S.Jane SmithJane Smith (Support)
email[email protected][email protected][email protected][email protected] (Support)
phone+1-555-0100null+1-555-0142+1-555-0142 (Support)
plannullenterprisepropro (Support)
regionUS-WestnullUS-WestUS-West (Support)

Support has 5 non-null fields out of 5, so it wins across the board. In case of a tie (two sources with equal completeness), the tie is broken by source order as defined in the sources array of the spec.

When to use: You don't have a clear trust hierarchy but you want to favor the source that knows the most about each entity. Good for exploratory reconciliation where you're learning which systems are most complete.

aggregate

Instead of picking a single source, aggregate functions combine values across all sources. This is the only strategy that can produce values not present in any individual source. The aggregate function is specified per field in the overrides list.

yaml
survivorship:
  default: most_recent
  overrides:
    - field: tags
      strategy: aggregate
      function: union
    - field: revenue
      strategy: aggregate
      function: max
    - field: first_seen
      strategy: aggregate
      function: min
    - field: total_interactions
      strategy: aggregate
      function: sum

Available aggregate functions:

FunctionInput TypeBehaviorExample
unionarraysMerge all arrays, deduplicate["a","b"] + ["b","c"] = ["a","b","c"]
intersectionarraysKeep only values present in all sources["a","b"] + ["b","c"] = ["b"]
maxnumbers, datesTake the largest value100, 250, 80 = 250
minnumbers, datesTake the smallest value2024-01-01, 2025-06-15 = 2024-01-01
sumnumbersAdd all values100 + 250 + 80 = 430
avgnumbersAverage all values100 + 250 + 80 = 143.3

When to use: You want to combine data across sources rather than choosing between them. Common for metrics (sum revenue), tags (union all labels), or audit trails (earliest first_seen date).

WARNING

Aggregate fields require a function in the override. Fields without an override will fall back to the global default strategy.

custom

A placeholder for user-defined survivorship logic. When the engine encounters the custom strategy, it delegates to an externally registered handler. This is intended for advanced use cases where none of the built-in strategies suffice.

yaml
survivorship:
  default: custom

When to use: You have domain-specific logic that cannot be expressed through the built-in strategies. Requires a custom handler to be registered with the engine.

Per-Field Overrides

Real-world survivorship is rarely one-size-fits-all. You might trust CRM for names but want the most recent email. Per-field overrides let you mix strategies within a single spec.

yaml
survivorship:
  default: source_priority
  overrides:
    - field: email
      strategy: most_recent
    - field: phone
      strategy: most_complete
    - field: tags
      strategy: aggregate
      function: union
    - field: revenue
      strategy: aggregate
      function: max
    - field: name
      strategy: source_priority
      priority: [crm, billing, support]

The global strategy (default) applies to all fields except those listed in overrides. Each override specifies its own strategy and any strategy-specific parameters (priority for source_priority, function for aggregate).

Override resolution order:

  1. Check if the field has an entry in overrides
  2. If yes, apply the override strategy for that field
  3. If no, apply the global default strategy

Field Reference

FieldTypeRequiredDefaultDescription
defaultstringNomost_recentDefault strategy for all fields: source_priority, most_recent, most_complete, aggregate, custom
overridesarrayNo[]Per-field strategy overrides
overrides[].fieldstringYesN/AField name to override
overrides[].strategystringYesN/AStrategy for this field
overrides[].priorityarray[string]No[]Source priority order (for source_priority strategy)
overrides[].functionstringNoN/AAggregate function (for aggregate strategy): max, min, sum, avg, union, intersection

Realistic Example

Consider a B2B SaaS company that resolves customer identity across three systems:

Source data for a matched cluster (canonical ID: canon_8f3a):

FieldCRM (Salesforce)StripeZendesk
nameJohn DoeJohn D.Johnny Doe
email[email protected][email protected][email protected]
phone+1-555-0199null+1-555-0199
companyAcme CorpAcme Corpnull
plannullenterprisenull
mrrnull2500null
tags["strategic"]["high-value"]["escalated"]
last_contact2026-01-052026-01-182026-01-22
ticket_countnullnull47
updated_at2026-01-052026-01-182026-01-22

Spec configuration:

yaml
survivorship:
  default: source_priority
  overrides:
    - field: name
      strategy: source_priority
      priority: [crm, stripe, zendesk]
    - field: email
      strategy: most_recent
    - field: phone
      strategy: source_priority
      priority: [crm, stripe, zendesk]
    - field: company
      strategy: source_priority
      priority: [crm, stripe, zendesk]
    - field: plan
      strategy: source_priority
      priority: [crm, stripe, zendesk]
    - field: tags
      strategy: aggregate
      function: union
    - field: last_contact
      strategy: aggregate
      function: max
    - field: mrr
      strategy: aggregate
      function: max

Resulting golden record:

FieldSurviving ValueReason
nameJohn DoeOverride source_priority: CRM is first and non-null
email[email protected]Override most_recent: Zendesk updated most recently (Jan 22)
phone+1-555-0199Override source_priority: CRM is first and non-null
companyAcme CorpOverride source_priority: CRM is first and non-null
planenterpriseOverride source_priority: CRM is null, Stripe is next
mrr2500Override aggregate(max): only Stripe has a value
tags["strategic", "high-value", "escalated"]Override aggregate(union): merged from all three
last_contact2026-01-22Override aggregate(max): latest date across sources
ticket_count47Global default (source_priority): CRM and Stripe are null, Zendesk has it

Strategy Comparison

StrategyDeterministic?Combines Data?Best For
source_priorityYesNoClear system-of-record hierarchy
most_recentYes (with timestamps)NoEvent-driven, frequently-updated sources
most_completeMostly (ties broken by order)NoExploratory reconciliation, unknown data quality
aggregateYesYesMetrics, tags, audit fields
customDepends on handlerDepends on handlerDomain-specific logic beyond built-in strategies

Tips

  • Start with most_recent. It's the default and works well out of the box. Add overrides as you discover fields that need special treatment.
  • Use overrides liberally. Most production specs use a global strategy with 3-5 overrides for fields that need special treatment.
  • Aggregate union for tags and labels. You almost never want to lose tag data from any source.
  • Use most_recent for contact info. Email addresses and phone numbers change often. The most recent value is usually correct.
  • Put priority on each source_priority override. Priority is per-field, not global; each override that uses source_priority needs its own priority list.
  • Audit your golden records. After running reconciliation, spot-check the surviving values to make sure your strategy produces sensible results.

The identity and delegation layer for AI agents.