Merges
How records from several producers fold together under a type's merge policy, and how people merge two records by hand.
What it is
Merging decides when two findings are the same record. There are two mechanisms:
- Merge policy, per type: whether different producers that find the same key share one record (
merge) or keep their own (separate). - Hand merge, per record: a person says this record is really that one.
Why it exists
Two routines on one Desk can both find Northwind Goods. Most of the time people want one company with both routines’ findings. Sometimes they want each routine’s view kept apart. And identity rules are not perfect: a company seen once by domain and once by a slightly different name needs a person to say “same thing”.
How it works
Merge policy
Each type declares merge in the definition (merge by default). People can override it per type in settings/workspace under mergePolicy, which seed fills from the definition:
"mergePolicy": {"company": "merge", "role": "merge"}
merge: the id is<type>--<slug(key)>. Every producer that finds the key writes its ownbySourceslice of the same record.separate: the id is<type>--<slug(producerId--key)>. Each producer gets its own record.
Each run records the policy it used as mergePolicy on the run document. Flipping the policy applies to new ids only; existing documents are never re-keyed.
Hand merge
On a record page, a person opens “Merge into another merges/<entityId> {into: <targetId>, at, by} and says: “A hand merge is recorded now. Applies from the next run, which folds the two records together.”
From then on, the write contract resolves the merged id to its target whenever a run sees it, so new sightings land on the target. Merges can chain (A into B, B into C); a cycle fails the plan with hand merge cycle: A -> B -> A. Unmerge deletes merges/<entityId>.
Since 0.5.0: a side-by-side merge on the record page (pick two records, choose each field’s value, confirm, undo) and bulk Merge for exactly two selected records. It still writes
merges/<entityId>.
Worked example
Hiring Signals and a second producer, lead-signals, share a Desk. Both find northwind.example. With company: merge, there is one record, company--northwind-example, with bySource slices for both producers and sources: ["hiring-signals", "lead-signals"].
A third run records a company as “Northwind Goods Pty Ltd” with no domain, so it keys by name as company--northwind-goods-pty-ltd. A person merges it into company--northwind-example. The next time any run sees that name, the sighting goes to company--northwind-example.
Reference
| Item | Path or field | Writer |
|---|---|---|
| Type default | definition.json types.<type>.merge |
Builder |
| Workspace override | settings/workspace values.mergePolicy.<type> |
People |
| Policy used by a run | runs/<runId> mergePolicy |
Producers |
| Hand merge | merges/<entityId> {into, at, by} |
People |
When producers share a type, compile requires the same merge default in each definition.
Common pitfalls
- Flipping to
separateand expecting a split. Old records stay merged. Only new ids follow the new policy. - Merging across types. The Desk only offers records of the same type.
- Expecting the merge to show at once. The hand merge is recorded immediately; the fold happens on the next run.