lumnik. / scenarios All scenarios · Français

An established customer does not switch records in silence

Goal See when a CRM contact starts pointing at another customer, instead of moving it in silence.

Yesterday, contact CC-42 and account AC-700 meant Nova Pumps. Today its email points at Helios Valves.

erp-customers.csv
account_code,company_name,email
AC-700,Nova Pumps,service@nova-pumps.example
AC-800,Helios Valves,maintenance@helios-valves.example
crm-contact-day-1.csv
contact_code,display_name,email,phone
CC-42,Nova Pumps,service@nova-pumps.example,+33 4 72 42 42 42
manifests/30-customer.yaml
kind: Entityname: ScenarioResolutionDriftDdescription: "A resolved CRM contact whose matching data later contradicts its established identity."scopes: [entity-resolution]sources:  - table: t_scenario_resolution_drift_erp_d    map:      account_code: account_code      company_name: name      email: email  - table: t_scenario_resolution_drift_crm_d    map:      contact_code: crm_id      display_name: name      email: email      phone: phoneresolution:  identity:    t_scenario_resolution_drift_erp_d: account_code    t_scenario_resolution_drift_crm_d: crm_id  cascade:    - id: email      exact:        fields: [email]        normalize: [lower]
crm-contact-day-2.csv
contact_code,display_name,email,phone
CC-42,Nova Pumps,maintenance@helios-valves.example,+33 4 72 42 42 42
$ lm entity assert-same ScenarioResolutionDriftD --left t_scenario_resolution_drift_crm_d:CC-42 --right t_scenario_resolution_drift_erp_d:AC-700 --reason "The CRM email was changed in error; the contact remains Nova Pumps"
recorded: t_scenario_resolution_drift_crm_d:CC-42 and t_scenario_resolution_drift_erp_d:AC-700 are same

$ lm entity resolve ScenarioResolutionDriftD
rows seen: 3
resolved: 3
ambiguous: 0
unresolved (no rule): 0
unresolved (provider): 0
newly minted: 0
provider calls: 0
provider failures: 0
rule hits:
  scenarioresolutiondriftd:email: 2

$ lm entity ambiguous ScenarioResolutionDriftD -o json
[]

$ lm entity get ScenarioResolutionDriftD --filter crm_id=CC-42 -o json
[
  {
    "account_code": "AC-700",
    "name": "Nova Pumps",
    "email": "service@nova-pumps.example",
    "crm_id": "CC-42",
    "phone": "+33 4 72 42 42 42",
    "_conflicts": "{\"email\": [{\"src\": \"t_scenario_resolution_drift_erp_d\", \"value\": \"service@nova-pumps.example\"}, {\"src\": \"t_scenario_resolution_drift_crm_d\", \"value\": \"maintenance@helios-valves.example\"}]}"
  }
]
$ lm entity resolve ScenarioResolutionDriftD
rows seen: 3
resolved: 3
ambiguous: 0
unresolved (no rule): 0
unresolved (provider): 0
newly minted: 0
provider calls: 0
provider failures: 0
rule hits:
  scenarioresolutiondriftd:email: 2

$ lm entity get ScenarioResolutionDriftD --filter crm_id=CC-42 -o json
[
  {
    "account_code": "AC-700",
    "name": "Nova Pumps",
    "email": "service@nova-pumps.example",
    "crm_id": "CC-42",
    "phone": "+33 4 72 42 42 42",
    "_conflicts": "{\"email\": [{\"src\": \"t_scenario_resolution_drift_erp_d\", \"value\": \"service@nova-pumps.example\"}, {\"src\": \"t_scenario_resolution_drift_crm_d\", \"value\": \"maintenance@helios-valves.example\"}]}"
  }
]

$ lm entity history ScenarioResolutionDriftD t_scenario_resolution_drift_crm_d CC-42 -o json
[
  {
    "ruleDeclaredId": "email",
    "ruleVersion": "ce968f5902d73417@engine-v1@hmac-k1@lower:1.0.0",
    "provider": "",
    "candidates": [
      "27c8919d-7484-4832-8508-0ea80c566d88"
    ],
    "discardedCandidates": [],
    "setAside": [],
    "outcome": "RESOLVED",
    "decidedBy": "automatic",
    "runId": "",
    "occurredAt": "2026-11-03T15:24:39.894428Z",
    "assertedBy": null,
    "assertedByLogin": null,
    "reason": null
  },
  {
    "ruleDeclaredId": "email",
    "ruleVersion": "ce968f5902d73417@engine-v1@hmac-k1@lower:1.0.0",
    "provider": "",
    "candidates": [
      "27c8919d-7484-4832-8508-0ea80c566d88",
      "afcec735-875d-422f-92fc-45d24763a5f0"
    ],
    "discardedCandidates": [],
    "setAside": [],
    "outcome": "AMBIGUOUS",
    "decidedBy": "automatic",
    "runId": "0140870a-0e0f-4b91-b81f-e68ba077c789",
    "occurredAt": "2026-11-03T15:24:40.030132Z",
    "assertedBy": null,
    "assertedByLogin": null,
    "reason": null
  },
  {
    "ruleDeclaredId": "",
    "ruleVersion": "",
    "provider": "",
    "candidates": [
      "27c8919d-7484-4832-8508-0ea80c566d88"
    ],
    "discardedCandidates": [],
    "setAside": [],
    "outcome": "RESOLVED",
    "decidedBy": "asserted",
    "runId": "",
    "occurredAt": "2026-11-03T15:24:40.214667Z",
    "assertedBy": "1",
    "assertedByLogin": "integrator",
    "reason": "The CRM email was changed in error; the contact remains Nova Pumps"
  },
  {
    "ruleDeclaredId": "",
    "ruleVersion": "",
    "provider": "",
    "candidates": [
      "27c8919d-7484-4832-8508-0ea80c566d88"
    ],
    "discardedCandidates": [],
    "setAside": [],
    "outcome": "RESOLVED",
    "decidedBy": "asserted",
    "runId": "",
    "occurredAt": "2026-11-03T15:24:40.322616Z",
    "assertedBy": "1",
    "assertedByLogin": "integrator",
    "reason": "The CRM email was changed in error; the contact remains Nova Pumps"
  }
]

Limits

  • The scenario guards against a move; it does not say why the CRM changed.
  • Assertions and source cursors are durable: a full replay needs a new instance.
  • Synthetic data.

Customers · ERP and CRM

A contact already tied to its customer.

The CRM changes its email.

Two accounts, one contact, two days

The problem

The email points at another customer.

  • Yesterday: Nova Pumps.
  • Today: Helios Valves.

What is not changed

The sources stay as they are.

  • One CRM file a day.
  • No fix by hand.

The trap

Moving in silence.

  • The contact switches customer.
  • Nobody sees it.

What had to be written

normalize: [lower]

One line.

  • One rule: the email.
  • An established link does not move alone.
  • A 51-line scenario, three manifests.

The decision

A human decides.

  • CC-42 stays on Nova Pumps.
  • The history names the author and the reason.

The limit

The hub does not know why the CRM changed.

  • It shows it, it does not explain it.

Try it

The scenario replays locally.

Every step is published with its output.

This scenario runs on lumnik Open, free and open source.

Try lumnik Open →