SG Ground Truth

Entity types

One per standard entity type: what it is, how it is identified, created and linked. Each rule is the card's own Traps, copied whole.

API

Asset

Only project is required to create an Asset; omit code and the server writes "New Asset ", and two assets in one project may share a code, so key on id and never on code.

  • code is not unique and not required. A client keying an asset by name silently merges or duplicates rows. Match on id, and treat a code lookup as a query that can return more than one row.

  • POST with project alone succeeds, so a request that dropped its payload creates a real asset named New Asset <id> rather than failing. Send code explicitly and check what came back.

  • The 400 for a missing project is code: 103 with source: {} and detail: null. The message is in title; a client reading detail sees nothing.

  • Reading attributes alone shows no links at all: project, shots, sequences and tasks are all under relationships (probe 004).

corpus/findings/entity_types/Asset.md

Attachment

POST /entity/attachments answers 201 on an empty body and returns a row with no file; this_file is editable on create only, so bytes reach a site through the upload dance and never through a create.

  • A create with an empty body answers 201. A request that dropped its payload leaves a real Attachment row with no file, no filename and no link, and it cannot be repaired: this_file is create-only.

  • file_extension and file_size do not fill in. On the probed site, over a 500-row page, file_size was set on 130 rows and file_extension on 85, all created between 2013 and 2019; every row created 2025 or later read null on both, some of them more than a year old.

  • Take the size from the bytes you uploaded and the extension from filename. Probe 014's null columns are the steady state, not a race.

  • processing_status returns thumbnail_pending_us straight after an upload, which is not one of the four values its own valid_values declares, and it reverts to null once transcoding finishes. A client matching against valid_values sees an unknown token, then nothing.

  • Reading attributes alone shows no links: attachment_links, project and local_storage are all returned under relationships (probe 004).

corpus/findings/entity_types/Attachment.md

Cut

A Cut stores an edit, it does not model one: no field is computed or validated, and cut_items is returned sorted by the item's display name rather than by cut_order.

  • The server computes nothing. duration and the two timecode strings keep what was written: a Cut holding 6 items whose last frame is 647 still read duration 168 and timecode_end_text '01:00:07:00', written when it held 3. The extent and every sum are the client's (recipes/007).

  • cut_items is not the running order. It is returned sorted by the item's display name: ['aaa_last', 'sh010', 'sh020', 'sh030', 'sh030_gap', 'sh030_overlap'] against cut_order 1, 2, 3, 4, 5, 6 on the same six rows, as Playlist.versions does. Read the items with POST /entity/cut_items/_search, [["cut", "is", {"type": "Cut", "id": N}]], sort: "cut_order".

  • Deleting a Cut leaves its CutItems behind. DELETE /entity/cuts/<id> answered 204 and the items survived with cut null, reachable only through [["cut", "is", None]]. Delete the items first.

  • The schema is not an exhaustive description of the response. GET /entity/cuts/<id> returns platform_id and platform_revision_id under attributes, and neither appears in /schema/Cut/fields. A client that builds its field list from the schema alone will not ask for them, and one that validates a response against the schema will reject a legal row.

  • fps is the only frame rate on Cut or CutItem, it is null until someone writes it, and a float reads back as the string "24.0" (field_types/float). Nothing on a CutItem points at it.

  • Read only: created_at, created_by, id, image_blur_hash, image_source_entity, open_notes, open_notes_count, updated_at, updated_by.

corpus/findings/entity_types/Cut.md

CutItem

Nothing on a CutItem is unique and code repeats across Cuts, so an id found by a code search may sit on another Cut: check cut before every update or the write lands on the wrong edit.

  • An id does not say which Cut a row is on, and code repeats across Cuts. A search on [["code", "is", "sh010"]] returned items (46, cut 19) and (53, cut 20). A blind PUT /entity/cut_items/53 with no cut key answered 200, left cut at 20, and overwrote the other Cut's item.

  • Deciding update-versus-create on "does it have an id" is the data-loss path: read the candidates filtered on cut, or ask for cut.Cut.id and drop every id that does not match.

  • Sending cut in an update moves the item to that Cut at 200, and Cut.cut_items with {"multi_entity_update_mode": "add"} on the other side does the same, leaving the previous Cut holding [].

  • cut_order is not a sequence the server maintains. It is neither unique nor mandatory nor contiguous, null sorts last in both directions, and rows with an equal cut_order break the tie by id. It is also what a recut changes, so it is not a key: pair items across two edits on code plus its occurrence, scoped to the Cut (recipes/007).

  • Read only: created_at, created_by, id, image_blur_hash, image_source_entity, updated_at, updated_by.

corpus/findings/entity_types/CutItem.md

Delivery

Delivery has two independent Version links, sg_versions and version_sg_deliveries_versions; writing one leaves the other empty, and only the second mirrors Version.sg_deliveries.

  • reply_content returns a developer warning instead of a value. On a Delivery holding one real Reply it read 'Warning: If you see this displayed in the UI, it means the widget is not respecting grid_column = false.' The thread is replies, or POST /entity/replies/_search on [["entity", "is", {"type": "Delivery", "id": N}]] (entity_types/Reply).

  • sg_delivery_type has valid_values: [], so every write is 400 … 'Final' is not a valid list value. Valid list values: ''. An empty vocabulary is a field that can never be set, not a free-text field.

  • A Reply reads back HTML-escaped through one field and not the other: content containing " is returned verbatim by Reply.content and by the name of the Delivery.replies link, and as &quot; by Reply.cached_display_name.

  • Eight fields are read-only, created_at, created_by, delivery_number, id, image_blur_hash, image_source_entity, updated_at, updated_by, and they refuse a write two different ways:

written answer
created_at 400 code 103 API update() Delivery.created_at is editable on create only.
delivery_number 400 code 104 The field is not editable for this user: [Delivery.delivery_number]. Rule: API Admin -- PermissionRule 336: DENY update_field FOR entity_type => Delivery, field_name => delivery_number, field_value =>, so the message depends on the script's role (probe 027)

corpus/findings/entity_types/Delivery.md

HumanUser

sudo_as_login matches login and never email; a create is 401 unless it sends sg_status_list: "dis", and an empty projects is not site-wide access. [partial]

not measured: what an active user costs in seats, since the site refuses to create or promote one; and whether projects or the permission rule set produces the project subset, which needs two users the site does not have

  • Key on login. email is not unique, name is not unique, and code does not exist.

  • A create is 401 and not 400, so a client checking for a 4xx body shape finds code: 110 where every other create failure is code: 103.

  • The only creatable user is a disabled one, and PUT sg_status_list: "act" is refused the same way. A script cannot onboard a person on this site.

  • permission_rule_set and can_impersonate_this_user are read only: what a person may do and who may act as them are granted in the web interface and read over REST.

  • A deleted user is retired, not erased, and its login becomes reusable, so two rows can hold the same login with only one of them live.

corpus/findings/entity_types/HumanUser.md

LocalStorage

A LocalStorage row is site-wide and admin-only to write: an Artist reads every row and is refused on create, update and delete. code must be unique among live rows, and a DELETE frees it for reuse.

  • Writing one is admin-only. Impersonating an Artist on the probed site returned every row on a GET and refused all three writes, each at 400 code 104: Entity of type LocalStorage cannot be created by this user., The field is not editable for this user: [LocalStorage.mac_path]. and Entity of type LocalStorage can not be deleted by this user.

  • A client that offers "point at any folder" as a setup step has to say that an administrator runs it.

  • A row with no root on any platform is a 201, and nothing reports it as incomplete. The path a client then sends is refused as matching no defined storage (recipes/004_register_published_file).

  • The refusal to write is a 400, not a 401 or a 403, so a client branching on the status code reads it as a bad body.

  • A caller with no root to write under does not need a row at all: PublishedFile.path takes the three-call upload instead (recipes/013_publish_file_bytes).

corpus/findings/entity_types/LocalStorage.md

Note

A Note is titled by subject and bodied by content; only project is required to create one, attachments link in that same call, and a bare write to replies destroys the Reply rows.

  • client_note cannot be set over the API, on create or after. Every update answers 400 API update() Note.client_note is editable on create only. A client-facing Note over REST is sg_note_type: "Client", an editable list, and nothing else; it leaves client_note false (probe 069).

  • A bare replies: [] write deletes the Reply rows. Trimming that list the way a client trims any other multi_entity field destroys the replies, with nothing at the id after. Keep it out of a PUT.

  • A bare list written to note_links replaces the set, so appending one link with [{"type": "Shot", "id": N}] drops every other thing the Note was about (field_types/multi_entity).

  • subject is optional, not auto-filled and not unique: a request that dropped its payload creates a real titleless Note, and two Notes in one project may share a subject. Key on id.

  • meta is editable on create only. Seed it in the POST or never: every later PUT answers 400 API update() Note.meta is editable on create only. (field_types/jsonb).

corpus/findings/entity_types/Note.md

Playlist

Playlist.versions reads back sorted by the Version's code, never in the order written; the human order is sg_sort_order on PlaylistVersionConnection, which a write through the field leaves null.

  • A playlist read back is not the playlist a reviewer sees. Sorting by code puts sh010_v10 ahead of sh010_v9 and interleaves shots; take the order from PlaylistVersionConnection.sg_sort_order.

  • Appending with a bare list drops every other Version at 200. The query-string spellings of the update mode do the same (field_types/multi_entity). Send the mode in the body.

  • Unlinking a Version destroys its sg_sort_order: re-adding it makes a new join row with null. Reorder by writing sg_sort_order, never by rewriting the member list.

  • The stock read-only fields are created_at, created_by, external_share_count, id, image_blur_hash, image_source_entity, locked_at, locked_by, media_center_viewed_by_current_user_at, open_notes, open_notes_count, updated_at, updated_by.

  • They 400 with two different messages: API update() Playlist.id is read only. and API update() Playlist.created_at is editable on create only.

corpus/findings/entity_types/Playlist.md

Project

Project is site-wide and has no project field, so a scoping filter 400s on it; name is the identity, the only field both mandatory and unique, and code is a second unique text field.

  • is_template, is_demo and is_template_project are read only over REST, so the flags a picker filters on cannot be set by a script user. archived is editable.

  • start_date, end_date and duration are read only and derived. On the probed site all three are null on every project read, so do not filter or sort a project listing on them.

  • landing_page_url is a path, not a URL: "/detail/Project/N?legacy=true". Prefix the site URL yourself.

  • On the probed site GET /schema/Project/fields returns 42 fields, 15 of them not editable (created_at, created_by, duration, end_date, id, image_blur_hash, image_source_entity, is_demo, is_template, is_template_project, landing_page_url, layout_project, start_date, updated_at, updated_by). The count is site configuration; the names are stock.

corpus/findings/entity_types/Project.md

PublishedFile

Only project is required to create a PublishedFile, and nothing is unique: the same name, version_number and path publish twice at 201, so read the last version before writing the next.

  • path_cache stays null after a REST create even when path resolved to a local storage, while path_cache_storage is set from that resolution. A filter on path_cache misses every row published through the REST API and matches only what a publishing client wrote by hand.

  • A local path has no url key, so value["url"] raises on exactly the shape a publish uses. Read link_type first (field_types/url).

  • There is no notes or open_notes field. A comment about a publish lives on its version or its task, never on the PublishedFile.

  • Not editable on the probed site: id, created_at, created_by, updated_at, updated_by, image_blur_hash, image_source_entity. Everything else, path_cache included, takes a write.

corpus/findings/entity_types/PublishedFile.md

PublishedFileType

PublishedFileType is site-wide with no project field, so a publish that creates one on an unknown extension adds it to every project; code is the identity and the only unique field.

  • The scope is the site. A publish that creates the type on an unknown extension pollutes every project, and no filter narrows this endpoint. Gate creation behind an allowlist, or resolve to an existing row.

  • ?project_id=N returns 200 and changes nothing. A client that reads it as scoping will report every type on the site as belonging to whichever project it asked about.

  • Matching by code is case-insensitive on read. Two rows differing only in case are two types to the API and one to a filter.

  • PublishedFile.published_file_type takes a {type, id} hash; a bare id is 400. Filter by the dotted path published_file_type.PublishedFileType.code when the id is not already in hand.

corpus/findings/entity_types/PublishedFileType.md

Reply

Reply is site-wide with no project field, and entity accepts almost every type on the site, not only Note; send entity on create, because a Reply whose entity is null cannot be deleted.

  • A Reply whose entity is null cannot be deleted. DELETE /entity/replies/<id> answers 400 code 104 Delete failed for [Reply with id=N]: undefined method 'reflect_on_association' for class NilClass. Assign any entity with a PUT and the same delete answers 204. A create that omitted entity is therefore permanent litter until it is repaired, so send entity in the create body.

  • content is flagged mandatory and an empty body still answers 201, the same inversion probe 012 found on Version. The flag is not the contract.

  • A filter for a type that is not in Reply.entity's valid_types 400s with API read() invalid/missing entity hash string 'type', even when live rows point at it. On the probed site the oldest replies hang off Ticket, a type absent from /schema, and the 400's own Valid entity types list names Ticket while the filter is refused.

  • [["entity", "type_is", "Ticket"]] returns those rows at 200; use type_is for a type-level cut and reserve the hash for a type you read out of valid_types.

  • entity is editable after the fact, with no type check against the original. A PUT moves a Reply from a Note to a Version at 200 and the Note's thread silently loses a row.

corpus/findings/entity_types/Reply.md

Sequence

A Sequence needs project, not code, and project alone names it New Sequence <id>; shots is the reverse of Shot.sg_sequence, one link, so a Shot sits in exactly one Sequence.

  • code looks mandatory and is not, and project looks optional and is not. {"project": …} alone returns 201 with code set to New Sequence <id>, so a client that omits code on a retry does not fail, it litters. {"code": …} alone 400s with missing 'project' attribute.

  • A Shot belongs to exactly one Sequence. Adding a Shot already linked elsewhere to Sequence.shots returns 200 and silently empties the first Sequence's shots; there is no reparenting error.

  • code is not unique and no filter makes it so. Two rows in one project answered to the same code. cached_display_name accepts a PUT at 200 and stores nothing: it re-reads as code.

  • DELETE /entity/sequences/<id> returns 204 and the row leaves both GET and _search, but it is a retire: POST /entity/sequences/<id>?revive=true returns 200 and the id is live again. Without the query parameter that same POST is 400 Request Parameters invalid. with source: {"revive": ["revive is missing"]}.

corpus/findings/entity_types/Sequence.md

Shot

A Shot needs only project on create, and code is flagged mandatory, is optional and is not unique: an omitted one becomes "New Shot " and a re-run duplicates rows. Send code, key on id.

  • The mandatory flags invert. Omit code and the row is created as New Shot <id>, which reads as a real shot in any picker and is findable only by that string. Always send code.

  • Nothing enforces uniqueness: unique is false and a repeated create returns 201, so a re-run of an ingest doubles the rows. Search ["code", "is", ...] plus the project filter before creating.

  • The slug is not the scope. /entity/shots unfiltered reaches every project, and the singular and capitalised spellings resolve to the same collection, so a typo'd slug fails loudly while a missing project filter does not.

  • step_<n> passes every schema visibility test and reads null on every row, and a write answers 400 API update() Shot.step_0 is read only. (field_types/pivot_column). For the same rollup, query Task filtered on entity and step.

corpus/findings/entity_types/Shot.md

Step

Step is site-wide with no project field, partitioned only by entity_type; list the Steps for a Shot with entity_type is "Shot", and treat neither code nor short_name as unique.

  • entity_type groups Steps; it does not constrain Task.step. On the probed site 6 Tasks hold a Step declared for another type: 4 Level Steps and 2 Shot Steps on Asset Tasks. Check the pair yourself before trusting a Step to describe the entity it is attached to.

  • ["entity", "type_is_not", "Shot"] also matches a Task whose entity is null. Of 205 apparent cross-type Tasks on the probed site, 199 have no entity at all. Add ["entity", "is_not", null].

  • cached_display_name is null on every Step on the probed site, though it is editable: true and reads as an ordinary text field. Display code.

  • Task.step takes a {type, id} hash. A bare id returns 400 API summarize() Task.step expected [Hash, ActiveSupport::HashWithIndifferentAccess, ... NilClass] data type(s) but got Integer: 2 (probe 012).

corpus/findings/entity_types/Step.md

Task

A Task is named by content, never code; a create needs only project; start_date, due_date and duration are one triple the server recomputes on every write.

  • Identity is content. code and name do not exist on Task and both 400, so a generic "read the code" client fails on this type alone.

  • content is mandatory in the schema and still nullable over REST: null and "" both return 200 and read back null, leaving cached_display_name as "-".

  • valid_types on entity does not bind, matching field_types/entity. {"type": "Task", "id": N} was accepted at 200 and read back as a Task.

  • Never PUT two of start_date, due_date, duration expecting both to stand: the third is recomputed, and on a dependent Task a date write also sets pinned and can raise dependency_violation.

  • time_logs_sum, time_vs_est and time_percent_of_est are read only. Sum TimeLog.duration to predict them; Task.color is not a colour either (field_types/color).

corpus/findings/entity_types/Task.md

TimeLog

A TimeLog create requires only project; date defaults to the server's today instead of failing, entity takes any type despite valid_types ['Task'], and a script may log for any HumanUser.

  • date never fails a create. Omit it and the row gets the server's current date, in the site's timezone, not the day the work happened. A backfill that forgets date silently lands on today.

  • entity is not restricted to Task despite valid_types: ['Task']. A tool that logs against a Shot gets 201 and a row no Task rollup will ever count. Check the type client-side.

  • Task.time_logs_sum is read-only and 400s on a write (entity_types/Task). Change the sum by creating, editing or deleting a TimeLog.

  • created_by and created_at are editable on create only: a PUT to either is 400 API update() TimeLog.created_by is editable on create only. user stays editable for the row's life.

corpus/findings/entity_types/TimeLog.md

Version

The schema inverts the create contract: project is required and code is not, generated as "New Version " when omitted. code is not unique, so key on id.

  • The schema's mandatory and editable flags are not the contract. project is required and unflagged, code is flagged and generated, and image reads editable: true while refusing every write (field_types/image).

  • The 201 relationships block lists all 20 link slots, including ones never set, so its keys are not a record of the input (probe 012). Read the row back to confirm a link.

  • Not editable on the probed site: id, created_at, created_by, updated_at, updated_by, image_blur_hash, image_source_entity, open_notes, open_notes_count, otio_playable, viewed_by_current_user_at, and one pivot_column (field_types/pivot_column).

  • There is no attachments field. A file uploaded with no field in the path is found through Attachment.attachment_links, never from the Version (probe 014).

  • user is the Artist field and defaults to the caller, so a script's Versions are authored by the script: 106 of 200 recent rows had user equal to their own created_by ApiUser. It is editable, unlike created_by:

    create created_by user (Artist)
    script, user not sent the ApiUser the ApiUser
    script, user sent the ApiUser the HumanUser sent
    scope=sudo_as_login:<login> (probe 027) the HumanUser the HumanUser

    A tool publishing for someone wants one of the last two. Sending user needs no impersonation permission; the scope also fixes created_by.

corpus/findings/entity_types/Version.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.