Findings — observe: what changed
How the API behaves in this part of a session. Each rule is the entry's own Teaches, copied whole.
025_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. [partial]
not 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.
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.
corpus/findings/025_event_log.md
043_attention
The six attention calls share no convention with the rest of the API: no paging, no fields, links.self spelled /entity/Shot/7668, and a missing record id on activity_stream is a 500.
activity_streamis not the event log. It has its own id space, its own paging keys and its own latency. On the probed site the newest update was id246800dated two days before the run, and three Shots created during the run were absent from their own streams and from the project's after 90 seconds of polling.Read
EventLogEntry(probe 025) for anything a write has to confirm.Page it with
max_id, notpage[]. Both bounds are exclusive,earliest_update_idis the floor reached, and0there means the stream ran out.latest_update_idis site-wide when nomax_idis given, so it does not describe the record you asked about.A record id that is not there answers 500 on
activity_stream, on all four types tried, wherefollowers,followingandthread_contentsall answer a named 404. Check the record first.The follow body's
entityis the CamelCase schema name and nothing else."shots", correct in every path segment and accepted byfollowing?entity=, is a 500 with no clue in it.A partial follow list applies its good half and returns 404. The 404 names only the missing id, so treat the call as non-atomic and re-read
followers.Neither follower list pages or takes
fields. On the probed site one user'sfollowingwas 896 rows in a single body, each row id, type and a link, with no name and no follow date.thread_contentsreturns Note, Attachment and Reply interleaved by time. The Note and Attachment rows name their author undercreated_by, the Reply rows underuser, andentity_fieldswidened the first two and was ignored on the third.
corpus/findings/043_attention.md
049_script_events
A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.
generate_event_log_entrieson the ApiUser gates the whole feed, and its default isFalse. While it is off the write still answers 201, the log stays empty, and any webhook scoped to that change has nothing to fire on. Nothing errors, so a client cannot detect it from the response. Read the flag before concluding the API does not log an operation.A script can read and set its own flag.
PUT /entity/api_users/<own id>with{"generate_event_log_entries": true}answers 200. Turning it on is a one-call fix; it also means a compromised script key can switch its own audit trail off.One create costs one row per populated field plus one
_New. Four keys sent produced fiveShotgun_<Type>_Changerows, because server-set defaults are logged too, and each setsin_create: true. A consumer counting operations must not count events.Events are visible 0.3s to 0.4s after the call. There is no lag to design around at this size.
session_uuidis null on every row this script generated, and set on the rows the web interface generated. It does not group a script's calls into a session.A delete logs two rows and orphans all the others in the same instant.
Shotgun_<Type>_Retirementputsretirement_dateanddisplay_nameinmeta, and every earlier row for that entity drops itsentitylink at once, so the history is reachable only throughmeta.entity_id, which is unfilterable (probe 025). Capture the id before deleting or lose the trail.
corpus/findings/049_script_events.md
066_user_feed
A HumanUser's activity_stream is what the person created, not what they follow: 0 of 9 rows touched the 81 followed records. A feed is a fan-out over their tasks' Shots and Assets.
A HumanUser stream is the rows whose
created_byis that person, plus the creation of the row itself. It is not the Inbox and it is not the follow list: the 77 Tasks and 4 Notes the person follows contributed nothing to it, and impersonating the person did not change it.A Shot's stream holds its Tasks and Versions as
primary_entityrows; a Task's stream holds the Task alone. A feed of "my day" is one call per distinct Shot or Asset behind the person's Tasks, merged oniddescending, withmax_idfor paging (endpoints/get_entity_type_id_activity_stream).readwas false on every row under the script token and undersudo_as_login, so the stream does not expose the Inbox's read state. Keep the lastidseen locally and page withmin_id.What a Note or a Reply writes to any stream is
probe 067.
corpus/findings/066_user_feed.md
067_notes_in_the_stream
A Reply reaches every linked stream in 33 s as create_reply, creates too; a script's Note create and status changes were absent after 430 s. Write as a person.
create_replyis a fourthupdate_type, next tocreate,updateanddelete(probe 043). Itsprimary_entityis the Note, named<subject> - <content>, andmetaholds the Reply's id and itscontent, so a feed can draw the reply without a second call.One update id is written once and fanned out: the same
create_replyrow, id247337, was on the Note, the Version, the Shot, the Task and the Project. A fan-out over several streams must deduplicate onid.A record's stream holds the creates of its children: the Shot's had the Task and the Version, the Task's had the Version. The Version's stream holds no Task. Read the Shot or Asset, not the Task.
Latency for what does show is under 33 s on the probed site, against the 90 s absence
probe 043measured. Poll at 30 s.A Note created by the script user wrote no
createrow on any of six streams, and two status changes by the script wrote noupdate, in 430 s. Attribute changes made in the web application are on the same streams.Autodesk staff on the community forum tie API-made Inbox items to
sudo_as_loginand to the script's "Generate Events" flag. A Note created underscope=sudo_as_loginon the same site was on the Shot's stream and on its own within 60 s. Attribute a write to a person (probe 027).read_by_current_useron the Note read"unread"for the script, a string, not a boolean.
corpus/findings/067_notes_in_the_stream.md