Merge operation
MDMbox provides two merge operations: server-managed $merge/v2 builds the transaction with a selected algorithm; client-plan $merge executes a transaction Bundle supplied by the caller. Both commit resource changes, Task, Provenance, and AuditEvent atomically.
Server-managed merge
$merge/v2 merges a duplicate (source) into a surviving resource (target) of the same resourceType, such as two Patients. Task, Provenance, AuditEvent, and Device are protected from merging. Supply the desired target result, choose an algorithm, and list the related resource types whose references should move to the target.
POST https://<mdmbox-host>/api/fhir/$merge/v2
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "source",
"valueReference": { "reference": "Patient/duplicate-123" }
},
{
"name": "target",
"valueReference": { "reference": "Patient/primary-456" }
},
{ "name": "merge-algorithm", "valueString": "simple" },
{
"name": "result",
"resource": {
"resourceType": "Patient",
"id": "primary-456",
"name": [{ "given": ["John"], "family": "Smith" }]
}
},
{ "name": "related-resource-type", "valueString": "Encounter" },
{ "name": "related-resource-type", "valueString": "Observation" },
{ "name": "preview", "valueBoolean": true }
]
}
| Name | Type | Required | Description |
|---|---|---|---|
source | valueReference | Yes | Relative reference to the duplicate resource that will be deleted |
target | valueReference | Yes | Relative reference to a different resource of the same type |
result | resource | No | Desired target state, with the target's resourceType and id; the built-in algorithm leaves the current target unchanged when omitted |
merge-algorithm | valueString | No | Server-side algorithm id; defaults to simple |
related-resource-type | valueString | No | Repeat once for each resource type whose references should be reassigned |
preview | valueBoolean | No | Return the final audited plan without executing or persisting it (default: false) |
The default simple algorithm replaces target content with result when supplied, moves source references within the selected related-resource-type values to the target, and deletes the source.
When moving related references, simple changes only the Reference.reference string. It preserves sibling fields such as type, display, identifier, and extensions. References nested inside those extensions are also moved when they point to the source; references to other records remain unchanged.
result is the complete desired target content, not a partial update. Omit it to keep the target unchanged. Its meta.versionId is not a precondition; MDMbox protects writes using the current version read when computing the merge.
Only the listed related resource types are searched. If you omit them, related references are not moved. Include every type your workflow needs; $referencing can help you inspect them. Related resources may have the pair's type, such as Organizations referring to another Organization through partOf.
Reference search indexes
For large related-resource tables, add GIN expression indexes in the PostgreSQL resource database used by MDMbox. Each index covers references to one source resource type within one related-resource table, including references nested in extensions. These indexes accelerate reference discovery for server-managed merge and unmerge.
For example, when merging two Patients with related-resource-type=Encounter, create the following index. It is an index on the encounter table containing the IDs of Patient references inside Encounters:
CREATE INDEX CONCURRENTLY IF NOT EXISTS encounter_patient_reference_ids_gin_idx
ON encounter USING gin (
jsonb_path_query_array(
resource,
'$.** ? (@.resourceType == "Patient").id'::jsonpath
) jsonb_path_ops
);
ANALYZE encounter;
Run CREATE INDEX CONCURRENTLY outside a transaction. MDMbox does not create these indexes automatically. Without a matching index, reference discovery can scan the entire related-resource table even when the source has no related records, and large scans can exceed the operation's time limit.
Create an index for each source-type and related-table combination used by your workflow. For Patient references in Observation, use the same expression on observation with a different index name. For Organization references in Encounter, keep the encounter table and change "Patient" to "Organization" in the JSONPath expression. The expression must match the source resource type; an index containing all reference IDs or a full-resource GIN index does not match this type-specific expression.
The index narrows the candidate set. MDMbox still checks the exact source reference and discovers all its paths before building the plan. Processing many genuinely related resources can remain expensive even with an index.
Preview and response
The example above uses preview=true. It returns HTTP 200 with FHIR Parameters containing outcome (OperationOutcome) and plan (the proposed transaction Bundle). No business or audit resources are written.
To execute, send the request with preview=false. Success returns outcome and the merge task. Save its ID to unmerge later. A preview does not reserve versions: execution computes a plan against the then-current state.
Algorithm warnings appear in the outcome with HTTP 200. An error or fatal issue blocks the operation and returns an OperationOutcome with HTTP 409.
Validation and conflicts
Source and target must exist, be different resources of the same type, and not be marked not a match. Neither can already be the source of an active merge.
| Status | Meaning |
|---|---|
| 400 | Invalid request or unavailable algorithm |
| 404 | Source or target does not exist |
| 409 | The algorithm refuses the merge, or concurrent changes invalidate the plan |
| 422 | Invalid resource state or FHIR transaction data |
| 500 | Algorithm or server failure |
All merge writes are version-protected. On a concurrent-change conflict, the transaction rolls back; inspect current state before retrying.
Audit and history
A successful merge commits the business changes, Task, Provenance, and AuditEvent together. Preview writes none of these; failed execution attempts are audited separately. See Audit.
The Task saves the pair's versions, algorithm identity, and related-resource-type scope. Provenance identifies pre-merge and actual post-merge versions, including resources created by the merge. Deleted resources retain a pre-delete version reference. Keep these records and their FHIR history for as long as server-managed unmerge is needed.
Find the operation's Provenance through Aidbox:
GET https://<aidbox-host>/fhir/Provenance?target=Task/<task-id>
Custom algorithms
Use merge(input, mdm) to build a custom plan and read additional request parameters through input.parameters. A custom script may change existing resources outside the requested related-resource-type scope and validates its own assignment rules. Changes require version preconditions and are audited.
See the JavaScript algorithm API for plan rules and Algorithm management to create or configure scripts.
Git algorithm storage
See Git algorithm storage for repository layout, private access, synchronization, and limits.
Managing algorithms in the Admin UI
See Create or edit a script and Add and synchronize a source.
Client-plan merge
The $merge operation merges two FHIR resources by executing a client-provided FHIR transaction Bundle. The client controls the changes; MDMbox executes them atomically and adds audit records.
How it works
- Build a FHIR transaction Bundle to update the target, move related references, and delete the source.
- Send it to
$mergewith the source and target references. MDMbox validates the request and adds audit records. - Save the returned Task ID to unmerge later. The transaction commits or rolls back as a whole.
Request
POST https://<mdmbox-host>/api/fhir/$merge
Content-Type: application/json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "source",
"valueReference": { "reference": "Patient/duplicate-123" }
},
{
"name": "target",
"valueReference": { "reference": "Patient/primary-456" }
},
{ "name": "preview", "valueBoolean": false },
{
"name": "plan",
"resource": {
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"request": {
"method": "PUT",
"url": "Patient/primary-456",
"ifMatch": "W/\"3\""
},
"resource": {
"resourceType": "Patient",
"id": "primary-456",
"name": [{ "given": ["John"], "family": "Smith" }],
"birthDate": "1985-03-20"
}
},
{
"request": {
"method": "PUT",
"url": "Encounter/enc-789",
"ifMatch": "W/\"1\""
},
"resource": {
"resourceType": "Encounter",
"id": "enc-789",
"status": "finished",
"class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB" },
"subject": { "reference": "Patient/primary-456" }
}
},
{
"request": { "method": "DELETE", "url": "Patient/duplicate-123" }
}
]
}
}
]
}
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
source | valueReference | Yes | The resource to be merged away (deleted) |
target | valueReference | Yes | The surviving resource |
preview | valueBoolean | No | If true, return the assembled Bundle without executing (default: false) |
plan | resource (Bundle) | Yes | FHIR transaction Bundle with the merge changes |
Plan Bundle
The plan is a standard FHIR transaction Bundle. It typically contains:
- A
PUTentry for the target resource (with updated data) PUTentries to reassign references from related resources (Encounters, Observations, etc.)- A
DELETEentry for the source resource
Use ifMatch headers (ETags) for optimistic locking. If a resource was modified between when the client read it and when the merge executes, the entire transaction rolls back with a 422 Unprocessable Entity response (code conflict).
Preview mode
In the request above, change preview to true and keep the complete plan. MDMbox returns FHIR Parameters with outcome and bundle, the assembled transaction including audit resources, without writing anything.
Client-plan preview checks request structure and current state. FHIR resource validation occurs when the transaction executes, so a successful preview does not guarantee that execution will succeed.
Response
On success, the response is a Parameters resource containing the outcome, input summary, Task, and resulting target. Resource bodies below are abbreviated:
{
"resourceType": "Parameters",
"parameter": [
{
"name": "outcome",
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"details": {
"text": "Merge completed: Patient/duplicate-123 -> Patient/primary-456"
}
}
]
}
},
{
"name": "input-parameters",
"resource": {
"resourceType": "Parameters",
"parameter": [
{
"name": "source",
"valueReference": { "reference": "Patient/duplicate-123" }
},
{
"name": "target",
"valueReference": { "reference": "Patient/primary-456" }
},
{ "name": "preview", "valueBoolean": false }
]
}
},
{
"name": "task",
"resource": {
"resourceType": "Task",
"id": "generated-task-id",
"status": "completed",
"code": { "coding": [{ "code": "merge" }] },
"businessStatus": { "coding": [{ "code": "merged" }] },
"for": { "reference": "Patient/duplicate-123" },
"focus": { "reference": "Patient/primary-456" }
}
},
{
"name": "result",
"resource": {
"resourceType": "Patient",
"id": "primary-456"
}
}
]
}
Audit trail
Every executed merge creates three audit resources inside the same transaction:
Task — records the merge event:
for— the source (merged away)focus— the target (survivor)businessStatus—mergedcode—merge
Provenance — records what was changed:
target— all affected resources plus the operation Taskentity[]— versioned references to pre-change revisions of modified or deleted resources when available; newly created resources have no pre-change revisionagent—Device/mdmboxactivity—mergefromhttp://terminology.hl7.org/CodeSystem/iso-21089-lifecycle
AuditEvent — records the initiating user or client when available, operation outcome, service, request correlation, and references to the Task, Provenance, and domain resources. Its write is mandatory: a failure rolls back the merge. See Audit for queries and failure/preview behavior.
Find the provenance for a Task through Aidbox with GET /fhir/Provenance?target=Task/<task-id>. Task, Provenance, and the referenced FHIR history versions enable future unmerge. Tasks also power Notifications — downstream systems can subscribe to merge and unmerge events via Topic-Based Subscriptions.
Unmerge
A completed merge can be reversed with $unmerge. The unmerge request points to the original merge Task and supplies a client-built reverse transaction Bundle. MDMbox executes that reverse plan atomically, creates its own Task, Provenance, and AuditEvent, and updates the original merge Task to businessStatus=unmerged.
See Unmerge operation.
Validation
MDMbox validates the merge request before execution:
Structural validation (400 Bad Request):
- Source and target must be different resources
- Plan must be a transaction Bundle with at least one entry
- No duplicate PUT/DELETE URLs in the plan
State validation (422 Unprocessable Entity):
- Both source and target must exist
- Source must not already be merged (no active merge Task)
- Target must not already be a source in another merge (no circular merges)
FHIR transaction validation (4xx OperationOutcome):
- Resources in the plan are validated when the FHIR transaction executes
- If a resource declares
meta.profile, the corresponding FHIR package must be installed and the resource must satisfy the profile - If validation fails, the transaction rolls back, including Task, Provenance, and the success AuditEvent; a separate failure AuditEvent is attempted for non-preview requests
Profiled resources in the merge plan
For resources with meta.profile, install the corresponding package using the Aidbox Implementation Guide installation guide. MDMbox validates every resource in the transaction; a validation failure returns an OperationOutcome and rolls back the complete merge.
Finding related resources
Use the $referencing operation to discover resources that reference a given resource. This is useful when building the merge plan — you need to know which Encounters, Observations, etc. point to the source and need to be reassigned.