Record change history (`MetaRecordHistory`)
Updated
Tick tables and columns on Record logging (/record-logging, Administrator →
Monitor). That page writes a tenant overlay, not the object's model metadata,
so you can turn logging on or off for a product table (BankAccount, Item, …)
without switching working model. The Data change history journal
(/record-history, same ribbon group) and the History drawer on a
record card both read the same MetaRecordHistory table — who changed what,
from what to what, and when.
What isLogged means#
The writer uses the tenant overlay when Administration has saved the
object on /record-logging; otherwise it falls back to metadata flags
(the model default). Overlay never writes the object's model row — a
product table stays read-only, and a later model install does not wipe
the tenant switch.
| Layer | Where | Role |
|---|---|---|
Overlay object IsLogged |
MetaRecordLoggingSettings |
Tenant master switch; wins over metadata |
Overlay field IsLogged |
MetaRecordLoggingFieldSettings |
Tenant field tick; missing row inherits metadata |
Object isLogged |
Dictionary, document, table part, link table | Model default, used when no overlay |
Field isLogged |
Header fields, table-part columns, link-table columns | Model default field allow-list |
| Object | Fields ticked | What is written |
|---|---|---|
| off | ignored | nothing |
| on | none | nothing |
| on | some | Insert/Delete: snapshot of those fields only. Update: a row only if at least one ticked field changed; Changes contains only those fields |
An object left isLogged: true from an older stand with no field ticks does
not mean “log everything”. With an empty field list the writer writes nothing
until you tick at least one column.
Noise columns (Created* / Modified* timestamps and author MetaIds) are never
logged even if someone ticks them. Calculated fields are not offered on the
settings page and are not diffed.
Where to configure and read#
| Surface | Route | Who | Purpose |
|---|---|---|---|
| Record logging | /record-logging |
Administrator | Tenant overlay: on/off and field ticks for any table, including other models |
| Data change history | /record-history |
Administrator (Monitor ribbon) | Journal by object: filters, paging, expand old → new |
| History drawer | On the record card | Whoever can open the record | Per-record slice of the same table |
This is not Audit. MetaAuditLog and /audit cover metadata, posting,
security, licensing, and similar platform events — not ordinary business field
diffs. Do not point users at Audit when they ask “who changed this customer
name?”.
For who wrote the row (author columns on the business table), see
Who wrote this row: ICurrentUser and the author columns.
Record history answers a different question: which fields changed between
saves.
Writer path#
RecordHistoryService is the only writer. It runs on the HTTP
DataController path for insert, update, and delete — the same saves that
come from the web client forms and grids.
| Path | Journaled? |
|---|---|
DataController HTTP CRUD |
yes, when object + field isLogged match |
IDataService / managers from scripts, jobs, imports |
no (follow-up if you need it) |
| Database triggers | no — not ported from MIQS |
A history write failure still never fails the business save.
API (read)#
Table journal (Administrator page):
GET /api/data/{tableName}/history?from=&to=&user=&action=&take=&before=
Per-record (History drawer):
GET /api/data/{tableName}/{recordMetaId}/history
Both require Read on that object (same guard as data access). user is a
UserMetaId GUID or a case-insensitive substring of UserName. Paging uses
take (default 100, max 500) and before (timestamp exclusive) so a growing
log does not skip rows when you load the next page.
The journal stores raw field values. A reference column is a Guid. The
expand on /record-history and the History drawer resolve those ids through
the same /api/documents/resolve-labels path the grids use, and print
Name (guid) — the name a person reads, the id when two rows share a caption
or the target has been deleted (then the Guid stays, unresolved).
Index (TableName, Timestamp) supports the journal; (TableName, RecordMetaId)
supports the drawer.
Traps#
Empty field list writes nothing. Object
isLogged: truewith zero field ticks is valid (the settings page keeps the object on when you clear the last field) but produces no rows until you tick a column again.
Leftover object flag ≠ log-all. Workspace objects that already had object-level
isLogged: truebefore opt-in logging did not automatically get field ticks. Until you configure fields on/record-logging, new saves write nothing even if the object switch looks on.
Overlay beats the model. Turning a table off in Administration does not flip the object's
isLoggedin its model. The designer of a product model stays read-only; the tenant overlay is the switch that actually runs.
Script saves stay out. Code that updates rows through
IDataServiceor document/dictionary managers does not pass throughRecordHistoryService. Only HTTP DataController saves are journaled today.
Purgeable. Retention sweeps can delete old
MetaRecordHistoryrows. Do not treat the journal as the only proof of who changed data for compliance — pair it with author columns and your retention policy.
The Guid in the JSON is the source of truth. The UI label is resolved at read time. A renamed Item group still diffs as the same Guid; a deleted one shows the Guid alone because there is nothing left to name.
Do not confuse with Audit. Field-level business history is
/record-historyandMetaRecordHistory. Platform audit is/auditandMetaAuditLog.
Where next#
- Who wrote this row:
ICurrentUserand the author columns —CreatedByMetaId/ModifiedByMetaIdon every save, independent of opt-in field logging. - The command run journal — a different journal (commands, not field diffs).