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 fieldSurvivorship 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.
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]:
| Field | CRM (priority 1) | Billing (priority 2) | Support (priority 3) | Surviving Value |
|---|---|---|---|---|
name | Jane Smith | Jane S. | J. Smith | Jane Smith (CRM) |
email | [email protected] | null | [email protected] | [email protected] (CRM) |
phone | null | null | +1-555-0142 | +1-555-0142 (Support) |
plan | null | enterprise | null | enterprise (Billing) |
last_ticket | null | null | 2026-01-15 | 2026-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.
survivorship:
default: most_recentExample:
| Field | CRM (updated 2026-01-10) | Billing (updated 2026-01-18) | Surviving Value |
|---|---|---|---|
name | Jane Smith | Jane S. | Jane S. (Billing, more recent) |
email | [email protected] | [email protected] | [email protected] (Billing, more recent) |
phone | +1-555-0100 | null | +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.
survivorship:
default: most_completeExample:
| Field | CRM (4/5 fields) | Billing (3/5 fields) | Support (5/5 fields) | Surviving Value |
|---|---|---|---|---|
name | Jane Smith | Jane S. | Jane Smith | Jane Smith (Support) |
email | [email protected] | [email protected] | [email protected] | [email protected] (Support) |
phone | +1-555-0100 | null | +1-555-0142 | +1-555-0142 (Support) |
plan | null | enterprise | pro | pro (Support) |
region | US-West | null | US-West | US-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.
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: sumAvailable aggregate functions:
| Function | Input Type | Behavior | Example |
|---|---|---|---|
union | arrays | Merge all arrays, deduplicate | ["a","b"] + ["b","c"] = ["a","b","c"] |
intersection | arrays | Keep only values present in all sources | ["a","b"] + ["b","c"] = ["b"] |
max | numbers, dates | Take the largest value | 100, 250, 80 = 250 |
min | numbers, dates | Take the smallest value | 2024-01-01, 2025-06-15 = 2024-01-01 |
sum | numbers | Add all values | 100 + 250 + 80 = 430 |
avg | numbers | Average all values | 100 + 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.
survivorship:
default: customWhen 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.
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:
- Check if the field has an entry in
overrides - If yes, apply the override strategy for that field
- If no, apply the global
defaultstrategy
Field Reference
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
default | string | No | most_recent | Default strategy for all fields: source_priority, most_recent, most_complete, aggregate, custom |
overrides | array | No | [] | Per-field strategy overrides |
overrides[].field | string | Yes | N/A | Field name to override |
overrides[].strategy | string | Yes | N/A | Strategy for this field |
overrides[].priority | array[string] | No | [] | Source priority order (for source_priority strategy) |
overrides[].function | string | No | N/A | Aggregate 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):
| Field | CRM (Salesforce) | Stripe | Zendesk |
|---|---|---|---|
name | John Doe | John D. | Johnny Doe |
email | [email protected] | [email protected] | [email protected] |
phone | +1-555-0199 | null | +1-555-0199 |
company | Acme Corp | Acme Corp | null |
plan | null | enterprise | null |
mrr | null | 2500 | null |
tags | ["strategic"] | ["high-value"] | ["escalated"] |
last_contact | 2026-01-05 | 2026-01-18 | 2026-01-22 |
ticket_count | null | null | 47 |
updated_at | 2026-01-05 | 2026-01-18 | 2026-01-22 |
Spec configuration:
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: maxResulting golden record:
| Field | Surviving Value | Reason |
|---|---|---|
name | John Doe | Override source_priority: CRM is first and non-null |
email | [email protected] | Override most_recent: Zendesk updated most recently (Jan 22) |
phone | +1-555-0199 | Override source_priority: CRM is first and non-null |
company | Acme Corp | Override source_priority: CRM is first and non-null |
plan | enterprise | Override source_priority: CRM is null, Stripe is next |
mrr | 2500 | Override aggregate(max): only Stripe has a value |
tags | ["strategic", "high-value", "escalated"] | Override aggregate(union): merged from all three |
last_contact | 2026-01-22 | Override aggregate(max): latest date across sources |
ticket_count | 47 | Global default (source_priority): CRM and Stripe are null, Zendesk has it |
Strategy Comparison
| Strategy | Deterministic? | Combines Data? | Best For |
|---|---|---|---|
source_priority | Yes | No | Clear system-of-record hierarchy |
most_recent | Yes (with timestamps) | No | Event-driven, frequently-updated sources |
most_complete | Mostly (ties broken by order) | No | Exploratory reconciliation, unknown data quality |
aggregate | Yes | Yes | Metrics, tags, audit fields |
custom | Depends on handler | Depends on handler | Domain-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
overridesliberally. Most production specs use a global strategy with 3-5 overrides for fields that need special treatment. - Aggregate
unionfor tags and labels. You almost never want to lose tag data from any source. - Use
most_recentfor contact info. Email addresses and phone numbers change often. The most recent value is usually correct. - Put
priorityon eachsource_priorityoverride. Priority is per-field, not global; each override that usessource_priorityneeds its ownprioritylist. - Audit your golden records. After running reconciliation, spot-check the surviving values to make sure your strategy produces sensible results.
