Entities — dates and numbers across sources
Two systems write the same date as
2026-07-01 14:30and01/07/2026, the same amount as1500and1500.00. Fused as written, they disagree on every row. Type them at the door, and only the real disagreements stay.
The case
The ERP and a partner each send their copy of the same orders:
order_no,due_at,amount
SO-1,2026-07-01 14:30:00,1500
SO-2,2026-07-03 09:00:00,980.50
order_no,due,amount
SO-1,01/07/2026,1500.00
SO-2,04/07/2026,980.5
The ERP's connector types its date and amount (70-due-erp.yaml). The partner's
connector reads its file as written. The entity fuses both on the order number:
# 55-order-due.yaml — the ERP's and a partner's copy of the same orders, fused on the order number.
#
# Expect, partner left as written (71): every due date and amount in _conflicts.
# Partner typed (72): SO-1 agrees, SO-2 disagrees on its due day (3 vs 4 July).
kind: Entity
name: OrderDue
match: [order_no]
scopes: [desk]
sources:
- table: t_due_erp
map: { order_no: order_no, due_at: due, amount: amount }
- table: t_due_partner
map: { order_no: order_no, due: due, amount: amount }
Left as written: every row disagrees
$ lm entity get orderdue -o json
[
{
"order_no": "SO-1",
"due": "2026-07-01 14:30:00+00",
"amount": "1500.0000",
"_conflicts": "{\"due\": [{\"src\": \"t_due_erp\", \"value\": \"2026-07-01 14:30:00+00\"}, {\"src\": \"t_due_partner\", \"value\": \"01/07/2026\"}], \"amount\": [{\"src\": \"t_due_erp\", \"value\": \"1500.0000\"}, {\"src\": \"t_due_partner\", \"value\": \"1500.00\"}]}"
},
…
]
01/07/2026 is text. The hub does not guess whether it is the first of July or the seventh of
January, so it compares it as written: a conflict on every row.
Type it at the door: four steps
A table keeps the column types it was created with. Adding parse-date to a CSV connector whose
table already holds rows does not retype the table: lm apply warns, and you create the table
again.
lm entity delete orderdue --force # 1. the entity holds the table
lm connector delete due-partner --force # 2. drops its table and rows
lm apply -f docs/apps/demo/sources/72-due-partner-typed.yaml # 3. the same connector, typed…
lm connector run due-partner # …and its table created again
lm entity apply -f docs/apps/demo/entities/55-order-due.yaml # 4. the entity, again
Step 3 adds two hooks to the partner's connector:
mapping:
cell:
- parse-date: { column: due, formats: ["dd/MM/yyyy"] }
- parse-number: { column: amount, decimal-separator: "." }
The entity's columns now have types:
$ lm source schema --scope desk
…
connector.v_orderdue
order_no text
due timestamp with time zone
amount numeric
Typed: only the real disagreement stays
$ lm entity get orderdue -o json
[
{
"order_no": "SO-1",
"due": "2026-07-01T14:30:00Z",
"amount": "1500",
"_conflicts": "{}"
},
{
"order_no": "SO-2",
"due": "2026-07-03T09:00:00Z",
"amount": "980.5",
"_conflicts": "{\"due\": [{\"src\": \"t_due_erp\", \"value\": \"2026-07-03 09:00:00+00\"}, {\"src\": \"t_due_partner\", \"value\": \"2026-07-04 00:00:00+00\"}]}"
}
]
$ lm conflicts list
ENTITY KEY COLUMNS SOURCES
OrderDue SO-2 due t_due_erp,t_due_partner
- SO-1 agrees. The partner knows only the day; the ERP's 14:30 is on that day.
1500.00and1500are the same number. - SO-2 disagrees for real: the ERP says the 3rd, the partner the 4th. That is the one row to look at.
980.50and980.5agree. A number is written without its trailing zeros.
The rules
Dates — the fused column follows what its sources hold:
| The sources hold | The entity's column | Compared |
|---|---|---|
| dates only | a date | as dates |
| dates and times | a time; a date is its midnight, in the entity's zone (UTC without one) | at the day: a time agrees with its day |
| times only | a time | to the instant |
| one source as text | text | as written |
Numbers — the same:
| The sources hold | The entity's column | Compared |
|---|---|---|
| whole numbers only | a whole number | by value |
| one decimal among them | a decimal, without trailing zeros | by value: 7.0000 = 7 |
| one source as text | text | as written: "10.0" ≠ 10 |
A typed column orders and filters as its type everywhere: lm entity get --filter amount=1500.00
finds 1500, a view sorts 7, 12, 100. Each value still reads as a string in JSON.
A time of day and its zone
An ERP writes 2026-08-02 23:30 with no zone. Read as UTC, it is two hours off a Paris CSV parsed
with zone: Europe/Paris: a conflict on every row. Declare where each source writes:
- the ERP's JDBC connection:
zone: Europe/Paris(A time without a time zone); - the CSV's
parse-date: the samezone(parse-date).
A date alone has no such trap: both sides are its midnight.
The business's day
A date and a time meet at the day. Which day a time falls on depends on where you stand: a sample
registered at 00:30 in Paris on the 6th is still the 5th in UTC. The ERP's goods receipt for that
batch says the 6th. Without a zone the entity reads the UTC day, and the pair disagrees:
BATCH_NO RECEIVED _CONFLICTS
L-104 2026-11-05T23:30:00Z {"received": [{"src": "t_lims", …}, {"src": "t_erp", …}]}
Say where the business is, once, on the entity:
kind: Entity
name: Batch
zone: Europe/Paris
With zone: Europe/Paris:
- a date is its midnight in Paris, and a time falls on its Paris day;
L-104agrees;- a real disagreement, the 7th against the 6th, is still one.
The zone holds where the entity compares a date with a time: the fused rows, lm conflicts list, a
join keyed by day, the search, and a --filter on a date (Read API). It does
not reach the day in an lm ask question, which still starts at midnight UTC.
- Without
zone:, nothing moves. An entity applied before keeps the UTC day. - A region, not an offset.
Europe/Paris, not+01:00: the region knows the summer time. - Apply the entity again after you add it: the view is rebuilt with the zone.
- It does not change how a source writes its times without a zone: that stays on the
connector's
zoneand onparse-date.
The files
All under docs/apps/demo/, landed like the demo desk
(docker cp the two CSVs into /tmp/acme-desk/):
| File | Role |
|---|---|
data/due-erp.csv, data/due-partner.csv |
the two copies of the orders |
sources/70-due-erp.yaml |
the ERP's connector, date and amount typed |
sources/71-due-partner.yaml |
the partner's connector, as written |
sources/72-due-partner-typed.yaml |
the partner's connector, typed |
entities/55-order-due.yaml |
the fusion |