event log
meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself.
Partly measured. Whether event_type or meta can be set in the create body is untried: testing it costs another permanent row. Every event read was generated by another user; probe 049 covers the script's own.
Q What can a client do with EventLogEntry over REST?
Endpoint POST /entity/event_log_entries/_search ; POST /entity/event_log_entries ; DELETE /entity/event_log_entries/<id> ; GET /schema/EventLogEntry/fields
Docs claim Silent on the log's contents. The REST docs address it as an ordinary entity type and
describe neither meta nor which operations a script user is permitted.
Actual
POST /entity/event_log_entries/_search
{"filters":[["entity","is",{"type":"Shot","id":<id>}],["attribute_name","is","sg_status_list"],
["event_type","is","Shotgun_Shot_Change"]],"fields":["meta","created_at"],"sort":"-id"} -> 200
{"attributes":{"meta":{"type":"attribute_change","attribute_name":"sg_status_list",
"entity_type":"Shot","entity_id":<id>,"in_create":true,"field_data_type":"status_list",
"old_value":"wtg","new_value":"ip","platform_id":null},"created_at":"2026-01-21T19:47:33Z"},
"relationships":{"entity":{"data":{"id":<id>,"name":"sh010","type":"Shot"}}},"id":<id>}
GET /entity/shots/<id>?fields=sg_status_list -> {"sg_status_list":"ip"} new_value, still live
[["meta","is",null]] -> 400 code 103
"API summarize() EventLogEntry.meta's 'serializable' data type cannot be used in a filter."
sort=meta, sort=audit_trail, sort=zzz_not_a_field -> 200, all three ordered by ascending id
POST /entity/event_log_entries {} -> 400 code 103 "API create() missing 'project' attribute: {}"
POST /entity/event_log_entries {"project":{"type":"Project","id":<id>}} -> 201
{"description":"New Event","created_at":"2026-09-02 18:39:48 UTC"}
read back: event_type null, attribute_name null, meta null, user null, entity null
PUT {"meta":{...}} -> 400 code 104
"The field is not editable for this user: [EventLogEntry.meta]. Rule: API Admin --
PermissionRule 306: DENY update_field FOR entity_type => EventLogEntry, field_name => meta"
DELETE /entity/event_log_entries/<id> -> 400 code 104
"Entity of type EventLogEntry can not be deleted by this user. Rule: API Admin --
PermissionRule 297: DENY retire_entity FOR entity_type => EventLogEntry"
disjoint 1001-id windows, newest first:
[head-1000, head] 714 of 1001 ids 71.3% dense
[head-11000, head-10000] 1001 of 1001 ids 100.0% dense
[head-101000, head-100000] 1001 of 1001 ids 100.0% dense
[head-1001000, head-1000000] 1001 of 1001 ids 100.0% dense
Teaches
The type has 16 fields, all of them server-written. id and created_at are the only two that order it,
and meta is the only one that says what changed.
| field | data type | read | filter |
|---|---|---|---|
event_type |
text |
yes | all 8 text relations |
attribute_name |
text |
yes | all 8 text relations |
description |
text |
yes, a rendered English sentence | all 8 text relations |
meta |
serializable |
yes, decoded | none: 400 cannot be used in a filter |
audit_trail |
jsonb |
never returned, even when named in fields |
accepted and ignored (field_types/jsonb) |
entity, project, user, image_source_entity |
entity |
under relationships |
is, type_is, in, dotted paths |
created_at |
date_time |
yes | all 15 date_time relations |
id |
number |
yes | is, greater_than, less_than, between, in |
session_uuid |
uuid |
yes | is, is_not, in, not_in |
cached_display_name, image, filmstrip_image, image_blur_hash |
text, image |
yes | is, is_not |
The four production uses, measured:
| use | works | how |
|---|---|---|
| history: read a previous value | yes | meta.old_value and meta.new_value, narrowed on entity + event_type + attribute_name, sort: "-id" |
| ledger: write an entry | create yes, and it is permanent | project is the only requirement; event_type and meta are unwritable and DELETE is refused |
| change feed: consume in id order | no, not from the head | ids at the head are 71.3% dense and settle to 100%, so a max-id cursor skips events |
| lock | no | the read half works; the write half cannot be released, so each acquisition leaks a permanent row |
metaholds the answer and refuses every query.old_valueandnew_valueexist only wheremeta.typeisattribute_change;new_entity,entity_retirementandentity_revivalholdentity_idandentity_typeand no values at all, and a preference change has nometa.typeand the keysold,new,pref.- Since
serializableis unfilterable and unsortable (field_types/serializable), select rows byentity,event_typeandattribute_name, order by-id, and inspectmetaclient-side. - To restore a previous status: take the newest matching entry, check
meta.new_valueequals the value the entity holds now, then writemeta.old_value. A mismatch means something changed since, and the entry is stale. - A created entry cannot be deleted, so never write one to a real site.
POSTwithprojectalone answers 201, inventsdescription: "New Event", and leavesevent_type,attribute_name,meta,userandentitynull. - Every one of those is then refused on
PUTby a per-field permission rule, andDELETEis refused byPermissionRule 297. Both refusals name a role and a rule number,API Admin -- PermissionRule 297, so a script user in a different role may be permitted more; check the error before concluding the API forbids it everywhere. - This probe spent its one create on the minimal body and stopped, so whether
event_typeormetacan be set in the create body is unmeasured: testing it costs another permanent row. One row from this probe survives in the sandbox project of the probed site. A ledger built here is append-only with no way to correct or retract an entry. - Ids are reserved ahead of use and committed late. On the probed site the newest 500 rows spanned 738 ids with 9 gaps, the largest 33 wide, while every 1001-id window at depth 10000 or more was 100% dense. Gaps close, so they are held blocks and not deletions.
- A cursor that stores
max(id)and asks forid greater_than <that>loses whatever later lands in the gaps it passed. Track a low-water mark instead: re-scan a window behind the head, or drive the feed fromcreated_atand deduplicate onid. entitygoes null when its target is deleted;metaremembers. On the probed site 12889 of 17778Shotgun_Shot_Changerows haveentitynull, and each names ameta.entity_idwhose Shot now 404s. Filtering onentityreturns only live targets, so the history of a deleted entity is reachable byevent_typeandcreated_atalone.- Every event this probe read was generated by somebody else. On the probed site the script's
generate_event_log_entrieswas False for the whole window this probe ran in, so none of its own writes were logged and the reads above measure other users' events. Probe 049 measures what a script's own write puts in the log, and the flag that decides whether it appears at all. - Narrowing works on everything but
meta. On the probed site the unfiltered log holds 2462044 rows, one project 22811, one Shot 2.event_typetakesstarts_withandin,created_attakesin_lastandbetween, andentitytakes a{type, id}hash,type_is, and a dotted path such asentity.Shot.code. attribute_namealone is site-wide across every entity type, so pair it withevent_typeorentity.- A sort on
meta, onaudit_trailor on a name the type does not have is accepted and ignored, falling back to ascendingid, so a client cannot tell an ignored sort from a satisfied one.
Python equivalent
# probe 025: what a Shot's status was before the current one
prev = sg.find_one(
"EventLogEntry",
[["entity", "is", {"type": "Shot", "id": shot_id}],
["event_type", "is", "Shotgun_Shot_Change"],
["attribute_name", "is", "sg_status_list"]],
["meta"], order=[{"field_name": "id", "direction": "desc"}])
now = sg.find_one("Shot", [["id", "is", shot_id]], ["sg_status_list"])["sg_status_list"]
old = prev["meta"]["old_value"] if prev and prev["meta"]["new_value"] == now else None