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.
Asset
Only project is required to create an Asset; omit code and the server writes "New Asset
codeis not unique and not required. A client keying an asset by name silently merges or duplicates rows. Match onid, and treat acodelookup as a query that can return more than one row.POSTwithprojectalone succeeds, so a request that dropped its payload creates a real asset namedNew Asset <id>rather than failing. Sendcodeexplicitly and check what came back.The 400 for a missing project is
code: 103withsource: {}anddetail: null. The message is intitle; a client readingdetailsees nothing.Reading
attributesalone shows no links at all:project,shots,sequencesandtasksare all underrelationships(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_fileis create-only.file_extensionandfile_sizedo not fill in. On the probed site, over a 500-row page,file_sizewas set on 130 rows andfile_extensionon 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_statusreturnsthumbnail_pending_usstraight after an upload, which is not one of the four values its ownvalid_valuesdeclares, and it reverts tonullonce transcoding finishes. A client matching againstvalid_valuessees an unknown token, then nothing.Reading
attributesalone shows no links:attachment_links,projectandlocal_storageare all returned underrelationships(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.
durationand the two timecode strings keep what was written: a Cut holding 6 items whose last frame is 647 still readduration168 andtimecode_end_text'01:00:07:00', written when it held 3. The extent and every sum are the client's (recipes/007).cut_itemsis not the running order. It is returned sorted by the item's display name:['aaa_last', 'sh010', 'sh020', 'sh030', 'sh030_gap', 'sh030_overlap']againstcut_order1, 2, 3, 4, 5, 6on the same six rows, asPlaylist.versionsdoes. Read the items withPOST /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 withcutnull, reachable only through[["cut", "is", None]]. Delete the items first.The schema is not an exhaustive description of the response.
GET /entity/cuts/<id>returnsplatform_idandplatform_revision_idunderattributes, 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.fpsis the only frame rate on Cut or CutItem, it isnulluntil someone writes it, and afloatreads 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
coderepeats across Cuts. A search on[["code", "is", "sh010"]]returned items(46, cut 19)and(53, cut 20). A blindPUT /entity/cut_items/53with nocutkey answered 200, leftcutat 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 forcut.Cut.idand drop every id that does not match.Sending
cutin an update moves the item to that Cut at 200, andCut.cut_itemswith{"multi_entity_update_mode": "add"}on the other side does the same, leaving the previous Cut holding[].cut_orderis not a sequence the server maintains. It is neither unique nor mandatory nor contiguous,nullsorts last in both directions, and rows with an equalcut_orderbreak the tie byid. It is also what a recut changes, so it is not a key: pair items across two edits oncodeplus 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_contentreturns 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 isreplies, orPOST /entity/replies/_searchon[["entity", "is", {"type": "Delivery", "id": N}]](entity_types/Reply).sg_delivery_typehasvalid_values: [], so every write is400 … '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:
contentcontaining"is returned verbatim byReply.contentand by thenameof theDelivery.replieslink, and as"byReply.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.emailis not unique,nameis not unique, andcodedoes not exist.A create is 401 and not 400, so a client checking for a 4xx body shape finds
code: 110where every other create failure iscode: 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_setandcan_impersonate_this_userare 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
loginbecomes 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
Artiston the probed site returned every row on aGETand 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].andEntity 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.pathtakes 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_notecannot be set over the API, on create or after. Every update answers400 API update() Note.client_note is editable on create only.A client-facing Note over REST issg_note_type: "Client", an editablelist, and nothing else; it leavesclient_notefalse (probe 069).A bare
replies: []write deletes the Reply rows. Trimming that list the way a client trims any othermulti_entityfield destroys the replies, with nothing at the id after. Keep it out of aPUT.A bare list written to
note_linksreplaces the set, so appending one link with[{"type": "Shot", "id": N}]drops every other thing the Note was about (field_types/multi_entity).subjectis 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 onid.metais editable on create only. Seed it in thePOSTor never: every laterPUTanswers400 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
codeputssh010_v10ahead ofsh010_v9and interleaves shots; take the order fromPlaylistVersionConnection.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 withnull. Reorder by writingsg_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.andAPI 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_demoandis_template_projectare read only over REST, so the flags a picker filters on cannot be set by a script user.archivedis editable.start_date,end_dateanddurationare 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_urlis a path, not a URL:"/detail/Project/N?legacy=true". Prefix the site URL yourself.On the probed site
GET /schema/Project/fieldsreturns 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_cachestaysnullafter a REST create even whenpathresolved to a local storage, whilepath_cache_storageis set from that resolution. A filter onpath_cachemisses every row published through the REST API and matches only what a publishing client wrote by hand.A
localpath has nourlkey, sovalue["url"]raises on exactly the shape a publish uses. Readlink_typefirst (field_types/url).There is no
notesoropen_notesfield. A comment about a publish lives on itsversionor itstask, 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_cacheincluded, 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=Nreturns 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
codeis case-insensitive on read. Two rows differing only in case are two types to the API and one to a filter.PublishedFile.published_file_typetakes a{type, id}hash; a bare id is 400. Filter by the dotted pathpublished_file_type.PublishedFileType.codewhen 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
entityis null cannot be deleted.DELETE /entity/replies/<id>answers 400 code 104Delete failed for [Reply with id=N]: undefined method 'reflect_on_association' for class NilClass. Assign anyentitywith aPUTand the same delete answers 204. A create that omittedentityis therefore permanent litter until it is repaired, so sendentityin the create body.contentis flaggedmandatoryand 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'svalid_types400s withAPI read() invalid/missing entity hash string 'type', even when live rows point at it. On the probed site the oldest replies hang offTicket, a type absent from/schema, and the 400's ownValid entity typeslist namesTicketwhile the filter is refused.[["entity", "type_is", "Ticket"]]returns those rows at 200; usetype_isfor a type-level cut and reserve the hash for a type you read out ofvalid_types.entityis editable after the fact, with no type check against the original. APUTmoves 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.
codelooks mandatory and is not, andprojectlooks optional and is not.{"project": …}alone returns 201 withcodeset toNew Sequence <id>, so a client that omitscodeon a retry does not fail, it litters.{"code": …}alone 400s withmissing 'project' attribute.A Shot belongs to exactly one Sequence. Adding a Shot already linked elsewhere to
Sequence.shotsreturns 200 and silently empties the first Sequence'sshots; there is no reparenting error.codeis not unique and no filter makes it so. Two rows in one project answered to the samecode.cached_display_nameaccepts aPUTat 200 and stores nothing: it re-reads ascode.DELETE /entity/sequences/<id>returns 204 and the row leaves bothGETand_search, but it is a retire:POST /entity/sequences/<id>?revive=truereturns 200 and the id is live again. Without the query parameter that samePOSTis 400Request Parameters invalid.withsource: {"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 code, key on id.
The mandatory flags invert. Omit
codeand the row is created asNew Shot <id>, which reads as a real shot in any picker and is findable only by that string. Always sendcode.Nothing enforces uniqueness:
uniqueis 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/shotsunfiltered 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 400API update() Shot.step_0 is read only.(field_types/pivot_column). For the same rollup, query Task filtered onentityandstep.
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_typegroups Steps; it does not constrainTask.step. On the probed site 6 Tasks hold a Step declared for another type: 4LevelSteps and 2ShotSteps 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 whoseentityis null. Of 205 apparent cross-type Tasks on the probed site, 199 have noentityat all. Add["entity", "is_not", null].cached_display_nameis null on every Step on the probed site, though it iseditable: trueand reads as an ordinarytextfield. Displaycode.Task.steptakes a{type, id}hash. A bare id returns 400API 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.codeandnamedo not exist on Task and both 400, so a generic "read thecode" client fails on this type alone.contentismandatoryin the schema and still nullable over REST:nulland""both return 200 and read backnull, leavingcached_display_nameas"-".valid_typesonentitydoes not bind, matchingfield_types/entity.{"type": "Task", "id": N}was accepted at 200 and read back as a Task.Never PUT two of
start_date,due_date,durationexpecting both to stand: the third is recomputed, and on a dependent Task a date write also setspinnedand can raisedependency_violation.time_logs_sum,time_vs_estandtime_percent_of_estare read only. SumTimeLog.durationto predict them;Task.coloris 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.
datenever 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 forgetsdatesilently lands on today.entityis not restricted to Task despitevalid_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_sumis read-only and 400s on a write (entity_types/Task). Change the sum by creating, editing or deleting a TimeLog.created_byandcreated_atareeditable on create only: aPUTto either is 400API update() TimeLog.created_by is editable on create only.userstays 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 code is not unique, so key on id.
The schema's
mandatoryandeditableflags are not the contract.projectis required and unflagged,codeis flagged and generated, andimagereadseditable: truewhile refusing every write (field_types/image).The 201
relationshipsblock 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 onepivot_column(field_types/pivot_column).There is no
attachmentsfield. A file uploaded with no field in the path is found throughAttachment.attachment_links, never from the Version (probe 014).useris the Artist field and defaults to the caller, so a script's Versions are authored by the script: 106 of 200 recent rows haduserequal to their owncreated_byApiUser. It is editable, unlikecreated_by:create created_byuser(Artist)script, usernot sentthe ApiUser the ApiUser script, usersentthe 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
userneeds no impersonation permission; the scope also fixescreated_by.
corpus/findings/entity_types/Version.md