SG Ground Truth

Findings — observe: what changed

How the API behaves in this part of a session. Each rule is the entry's own Teaches, copied whole.

API

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
  • meta holds the answer and refuses every query. old_value and new_value exist only where meta.type is attribute_change; new_entity, entity_retirement and entity_revival hold entity_id and entity_type and no values at all, and a preference change has no meta.type and the keys old, new, pref.

  • Since serializable is unfilterable and unsortable (field_types/serializable), select rows by entity, event_type and attribute_name, order by -id, and inspect meta client-side.

  • To restore a previous status: take the newest matching entry, check meta.new_value equals the value the entity holds now, then write meta.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. POST with project alone answers 201, invents description: "New Event", and leaves event_type, attribute_name, meta, user and entity null.

  • Every one of those is then refused on PUT by a per-field permission rule, and DELETE is refused by PermissionRule 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_type or meta can 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 for id 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 from created_at and deduplicate on id.

  • entity goes null when its target is deleted; meta remembers. On the probed site 12889 of 17778 Shotgun_Shot_Change rows have entity null, and each names a meta.entity_id whose Shot now 404s. Filtering on entity returns only live targets, so the history of a deleted entity is reachable by event_type and created_at alone.

  • Every event this probe read was generated by somebody else. On the probed site the script's generate_event_log_entries was 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_type takes starts_with and in, created_at takes in_last and between, and entity takes a {type, id} hash, type_is, and a dotted path such as entity.Shot.code.

  • attribute_name alone is site-wide across every entity type, so pair it with event_type or entity.

  • A sort on meta, on audit_trail or on a name the type does not have is accepted and ignored, falling back to ascending id, 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_stream is 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 id 246800 dated 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, not page[]. Both bounds are exclusive, earliest_update_id is the floor reached, and 0 there means the stream ran out. latest_update_id is site-wide when no max_id is 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, where followers, following and thread_contents all answer a named 404. Check the record first.

  • The follow body's entity is the CamelCase schema name and nothing else. "shots", correct in every path segment and accepted by following?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's following was 896 rows in a single body, each row id, type and a link, with no name and no follow date.

  • thread_contents returns Note, Attachment and Reply interleaved by time. The Note and Attachment rows name their author under created_by, the Reply rows under user, and entity_fields widened 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_entries on the ApiUser gates the whole feed, and its default is False. 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 five Shotgun_<Type>_Change rows, because server-set defaults are logged too, and each sets in_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_uuid is 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>_Retirement puts retirement_date and display_name in meta, and every earlier row for that entity drops its entity link at once, so the history is reachable only through meta.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_by is 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_entity rows; 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 on id descending, with max_id for paging (endpoints/get_entity_type_id_activity_stream).

  • read was false on every row under the script token and under sudo_as_login, so the stream does not expose the Inbox's read state. Keep the last id seen locally and page with min_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_reply is a fourth update_type, next to create, update and delete (probe 043). Its primary_entity is the Note, named <subject> - <content>, and meta holds the Reply's id and its content, so a feed can draw the reply without a second call.

  • One update id is written once and fanned out: the same create_reply row, id 247337, was on the Note, the Version, the Shot, the Task and the Project. A fan-out over several streams must deduplicate on id.

  • 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 043 measured. Poll at 30 s.

  • A Note created by the script user wrote no create row on any of six streams, and two status changes by the script wrote no update, 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_login and to the script's "Generate Events" flag. A Note created under scope=sudo_as_login on 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_user on the Note read "unread" for the script, a string, not a boolean.

corpus/findings/067_notes_in_the_stream.md

Every entry on this site is the output of a probe in probes/. The corpus is generated by running those probes against a live Flow Production Tracking site, not written from memory.

Not affiliated with or endorsed by Autodesk. Flow Production Tracking is their product; this is an independent record of how its REST API answers.