Recipes
A verified call and its real response, addressed by the task. The heading is the intent and the rules are the recipe's own Notes. The code is in the entry.
001_publish_version_with_media
Publish a generated image to Flow PT as a Version, with provenance and the workflow attached
projectis not schema-mandatory on Version but omitting it returns 400 (probe 012).entitymust be a{type, id}hash; a bare shot id 400s (probe 012,field_types/entity).sg_status_listaccepts only a code in the field'svalid_values, andhidden_valuesis not enforced, so subtract it per project yourself (field_types/status_list).upload_datamust be present in the complete call even though it is empty (probe 013).The complete call takes
Content-Type: application/json; the vendor type 415s there (probe 014).The field in the path picks the upload type:
/image/a Thumbnail, any other field an Attachment, no field at all a generic Attachment onattachment_links(probes 013, 014).Reading
imagestraight back gives a placeholder under/images/status/transient/until the transcode lands, so test that prefix rather than truthiness (probe 013,field_types/image).To find the attachments again use
POST /entity/attachments/_searchwithContent-Type: application/vnd+shotgun.api3_array+jsonand an entity hash filter (probe 014).
corpus/recipes/001_publish_version_with_media.md
002_batch
Apply many creates, updates and deletes in one atomic call, and match the results back to the requests
A batch cannot use an id it creates. Every way of pointing request 1 at request 0's row was rejected, and the failure is the whole batch, so nothing at all lands.
entityvalue sentresult {"type": "Shot", "id": "$0"}400 Invalid field value, update failed [5 - Update failed for [Version.entity]: Value is not legal.]{"type": "Shot", "id": -1}400, the same {"type": "Shot", "id": "0"}400, the same {"type": "Shot", "id": "u1"}, request 0 sent with"uuid": "u1"400, the same {"type": "Shot", "uuid": "u1"}, request 0 sent with"uuid": "u1"400 Invalid field value, update failed [5 - Update failed for [Version.entity]: Invalid statement.]The
uuida delete row returns is generated per request and is not an input. Build a dependent graph as one batch per level: create the parents, read their ids out of the response, substitute, then send the children. Steps 1 and 2 above are that sequence.Results are in request order, one row per request, interleaved by neither id nor type. A batch of
[update 29926, create, update 29927, create, update Shot 7557]answered in exactly that order, sozip(requests, response["data"])is correct and no key matching is needed.Two creates sending the same
codecame back as two rows distinguished only by position and by the new ids, 29930 and 29931.One failing request rolls back every other one. Each round below sent a good create, one bad request, and an update of an existing row whose
descriptionreadbefore:the bad request status after it updaterecord_id999999999404 Entity of type [Version] with id=999999999 does not exist.0 rows created, descriptionstillbeforedeleterecord_id999999999404, the same 0 rows created, descriptionstillbeforecreatewithsg_not_a_field400 Invalid field value, update failed [2 - Invalid field name: field [Version.sg_not_a_field] does not exist or user does not have access permission.]0 rows created, descriptionstillbeforecreatewithsg_status_list: not_a_status400 Invalid field value, update failed [5 - Update failed for [Version.sg_status_list]: 'not_a_status' is not a valid status. Valid statuses: 'na', 'rev', 'vwd', 'apr', 'custom', 'fin', 'ip', 'clsd', 'cmpt', 'cfrm', 'pndad', 'pndl', 'pndvs', 'part', 'pass', 'pndng'.]0 rows created, descriptionstillbeforeThe rollback is the reason to use the endpoint. A timeout is not covered by it: see the size note.
A batch create skips the validation a single create applies, and the row it makes is unreadable.
POST /entity/versionswith noprojectis 400API create() missing 'project' attribute: {"code" => "v001"}. The same create inside a batch answered 200 withid29932 and a create row holding noprojectrelationship.GET /entity/versions/29932then answered 404Version: 29932 not found, and a site-widePOST /entity/versions/_searchon itscodereturned 0 rows.DELETE /entity/versions/29932answered 204, so the row exists and only the id from the create response can reach it.Validate a batch payload yourself; a 200 is not proof the row is addressable. A link to an id that does not exist is rejected on both paths, 400
Update failed for [Version.entity]: Value is not legal.Size. No cap was found. A
requestsarray of 5001 was validated in full, answering onedata hash containing field/value pairs is required for the given requestper element.On the probed site a committing batch of 200 answered in 11.7s, 500 in 31.0s and 1001 in 47.7s on one run and not at all on another, where the client gave up at its own 60s read timeout. All 1001 rows had committed anyway.
A read timeout tells you nothing about what landed, and there is no request id to ask about, so keep a batch inside the response window, around 200 requests, and make each chunk re-runnable by reading back on
codebefore resending.The contract, one 400 at a time. Every rejection below names what it wanted.
sent result Content-Type: application/vnd+shotgun.api3_array+json415 Unsupported Content-Type 'application/vnd+shotgun.api3_array+json',{"content_type": "Content-Type must be one of: 'application/json'."}a top-level array 400 Invalid JSON body. Expected Hash but received Array.{"entity": "Version"}400 Request Parameters invalid.{"requests": ["requests is missing"]}{"requests": []}200 {"data": []}a request with no entity400 {"requests": {"0": {"entity": ["entity is missing"]}}}a createwith nodata400 {"data": ["data hash containing field/value pairs is required for the given request"]}"request_type": "read"400 {"requests": {"0": {"request_type": ["request_type must be one of: create, update, delete"]}}}"entity": "versions", the URL slug400 Invalid entity type: entity type [] does not exist.deletewith norecord_id, or withentity_id404 Entity of type [Version] with id=0 does not exist.deletein a batch and theDELETEverb do the same thing and report it differently.body after it batch delete200 {"request_type": "delete", "type": "Version", "id": N, "uuid": "...", "did_delete": true}GETthat id 404sDELETE /entity/versions/N204, 0 bytes GETthat id 404sDeleting an already deleted id inside a batch is 404
Entity of type [Version] with id=N does not exist.and takes the rest of the batch down with it, so a delete pass is not idempotent.Response shape differs by
request_type: a create row is the thin create subset, an update row is the whole record wrapped withlinksandstatus, a delete row is flat (probe 024 for the field-level table).?fieldson/entity/_batchis accepted and ignored, as on every other write (probe 024), and no row resolves a dotted path, so re-read for those.
corpus/recipes/002_batch.md
003_query_fields_and_pages
Resolve a query field's value, and run the rows a saved Page shows
The field is not a shortcut
The four flavours
The tree runs nowhere as stored
Tokens
Relations whose value is a list
Reading the page
columnsare schema field names in display order and go straight into?fields. All six on the page above were returned. On the probed site another Shot page lists the pivot columnsstep_35andstep_106, which are real fields in/schema/Shot/fieldsand were returned like any other.idis a legal column and is not a field. On an EventLogEntry page listing it,?fields=id,useranswered 200 withattributes: {}and the id under the row's ownidkey. Dropidfrom the list and readrow["id"].A column absent from
/schema/<Type>/fieldsis dropped at 200 with no error (probe 004), so check the list against the schema to know which columns you lost (probe 023).sortsandgroupingare lists of{column, direction}.?sorttakes one field, so the second and later sort keys and the grouping have to be applied client-side.
The stored project can be a project that is gone
Once translated, the field is still unusable as a field
The URL slug
corpus/recipes/003_query_fields_and_pages.md
004_register_published_file
Register the next PublishedFile without overwriting the last one, and write a path the server resolves for every platform
The version query is the whole guard, and it is a read-then-write race. No field on PublishedFile is unique and no combination is enforced, so the identical body posted twice returns two 201s and there is no conflict error to catch (
entity_types/PublishedFile). Two clients that readnext_versionat the same moment both publish version 4.The API offers nothing to close this: no unique constraint to create, no conditional write, no returned row to lose the race against.
What a client can do is narrow the query to the same context it publishes into (
nameplusproject, plusentityortaskif the stream is scoped to one), re-run it immediately before the create, and treat the answer as advisory.Production code pairs it with a filesystem probe of the publish directory and a retry cap because either source alone goes stale; that belongs in the client, and the API cannot confirm or deny what the retry found.
A caller with no storage root has a second route. The same field takes the three-call upload, which puts the bytes on the site and names no LocalStorage at all (
recipes/013_publish_file_bytes). Everything below still applies to thelocalshape.Each accepted path write mints an Attachment, on the create and again on every corrective
PUT. The id is inside thepathobject. Nothing removes the previous one, so a publish loop that rewrites paths accumulates Attachment rows silently. Delete byDELETE /entity/attachments/<id>, which answered 204.Creating a PublishedFileType for an unknown extension adds it to every project on the site. PublishedFileType has no
projectfield and no filter narrows it (entity_types/PublishedFileType). Resolve against the full listing with a case-normalised compare, and create only from an allowlist. The create call is shown in step 3 and was not run for this reason.The 201 body already holds the resolved
path, so a publish needs no read-back to log the paths it wrote. This is the one place a create returns more than it was sent;?fieldson a write is still ignored (probe 024).path_cacheis null after a REST create even though the path resolved. A filter onpath_cachemisses every row published this way (entity_types/PublishedFile).sg_status_listtakes a raw code from the field'svalid_valuesminus the project'shidden_values(probe 009,field_types/status_list). On the probed site the set is['wtg', 'ip', 'cmpt'].Reading the path back later:
GET /entity/published_files/<id>?fields=pathreturns thelocalshape, which has nourlkey, sovalue["url"]raises on exactly the shape a publish writes. Testlink_typefirst (field_types/url, probe 021).
corpus/recipes/004_register_published_file.md
005_propagate_status
Roll a status up from a parent's Tasks and Versions onto the parent, without racing a concurrent write
The trigger is not the rule. A run started by one Task changing answers "do all siblings satisfy the condition now", so the sibling set is re-queried in full and the triggering row's own status is used for nothing but the guard in step 4.
Two child types are two calls.
_searchis per entity type, so a rule over Tasks and Versions queries/entity/tasks/_searchand/entity/versions/_searchwith the sameentityfilter. One call per child type per parent, not one per row.Many parents in one call.
["entity", "in", [{"type": "Shot", "id": a}, {"type": "Shot", "id": b}]]is accepted at 200, as is["entity.Shot.id", "in", [a, b]]. Ask forentityinfieldsand group the rows byrelationships.entity.data.idyourself, then pair that with the batch write in step 5.A sibling can hold a status outside
usable. REST does not enforcehidden_values(field_types/status_list), so a code the project hides writes and reads back fine. On the probed sitehldis hidden on Task in this project andPUT {"sg_status_list": "hld"}answered 200 and read backhld.The two spellings of the rule then disagree over the same siblings
['fin', 'fin', 'hld']:rule result parent all(s in done)Falseip, correctnot any(s in blocking)Truefin, wrong:blockingwas built fromusable, which excludeshldBuild the "every status except these" set from the schema for the operator-facing list, and decide with
in doneso an unknown or hidden code blocks instead of passing.hidden_valuescan name codes that are not invalid_values. On the probed site the project hides['blk', 'hld', 'na', 'rdy', 'rev']on Task whilevalid_valuesholds noblkand nordy;PUT {"sg_status_list": "blk"}is 400. Subtracting one list from the other is still correct, and the difference is not the set of writable codes.Display labels fail two different ways.
PUT {"sg_status_list": "Final"}is a 400 that names the legal set, and the same string in a filter is a silent 0 rows:400 {"status": 400, "code": 104, "source": null, "detail": null, "meta": null, "title": "Update failed for [Task.sg_status_list]: 'Final' is not a valid status. Valid statuses: 'wtg', 'ip', 'fin', 'apr', 'dis', 'na', 'hld', 'rev', 'omt', 'ready'."}The 400 enumerates site-wide
valid_values, hidden codes included. Round-trip throughdisplay_valuesfor anything an operator reads and send the code everywhere else.The read-then-write race has no server-side guard. The step 4 comparison narrows the window; it does not close it, and there is no conditional write to close it with:
sent on PUT /entity/tasks/{id}result If-Match: "zzstale"200, applied If-Unmodified-Since: Mon, 01 Jan 1990 00:00:00 GMT200, applied If-None-Match: *200, applied updated_atechoed back in the body400 API update() Task.updated_at is editable on create only.A
GETdoes return a weakETag(W/"6829a03d..."), and no verb honours it. Two propagations racing over one parent both write; the last one wins, and the loser leaves no trace. Serialise the runs per parent on your side if the answer has to be exact.Batch is worth it for the write half only. The decision is reads, which
_batchdoes not do. On the probed site two parent updates answered in 474ms as one batch against 857ms as twoPUTs, one failing row rolls the whole call back, and the rows come back in request order. Recipe 002 has the contract, the size limits and the rollback matrix.A batch update row returns the whole record including the new
sg_status_list, and it is still not the confirmation:?fieldsis ignored on every write (probe 024) and a write can be a 200 no-op (probe 028). Step 6 is the confirmation.
corpus/recipes/005_propagate_status.md
006_media_round_trip
Take media off one Version and put the same bytes on another, which is what every sync, transfer and hand-off does
No server-side copy, and no reference to reuse. The only value
sg_uploaded_movieaccepts is an object holding aurl(field_types/url). Wrapping the source's presigned url in{"url": …, "name": …}answers 200 and stores alink_type: weblink that dies with the signature, with no transcode and no thumbnail. Moving the bytes is the only transfer that survives.The signature expires, and re-reading the field is the fix. The window is
X-Amz-Expiresseconds fromX-Amz-Date, and the number is not a constant: two reads one second apart returned 847 and 900.Both reads returned different strings for the same Attachment, so a client that outlives its url re-reads the field and starts the transfer again rather than retrying the string. A string held 706 seconds past expiry 403s
AccessDenied(field_types/image). Persist the Attachment id or the Version id; never the url.HEAD403s with anapplication/xmlbody. The signature coversGETalone, so size and type come from theGETresponse or fromGET /entity/attachments/{id}.Where the extension lives depends on the field.
field source of the filename sg_uploaded_movieand the three derivedurlfieldsname, andresponse-content-dispositionagrees with itimage,filmstrip_imageresponse-content-dispositiononly: animagefield is a bare string with nonameParse the query parameter in both cases and the same code handles all six. Uploading with the wrong extension is accepted, so nothing downstream corrects it.
Clearing is per field, and two readings never clear.
after sg_uploaded_movie_mp4image,filmstrip_image_frame_rate_transcoding_statusPUT {"sg_uploaded_movie": null}null old file old file '25.0'1 PUTall six nullnull null null '25.0'1 the new upload, before the transcode new file null /images/status/transient/'25.0'0 the new upload, after the transcode new file new file new file '25.0'1 _frame_rateand_transcoding_statusare afloatand anumber, noturlfields, and neithernullnor the upload resets them. Between the clear and the transcode landing they describe a file the Version no longer holds, exactly as a replacement does (probe 022)._frame_ratereads back as a JSON string (field_types/float).sg_uploaded_movie_transcoding_statuswas 0 in flight, 1 after the transcode landed, and 2 for a 16x16 png the transcoder refused, which left every derived field null. Treat 1 as "a transcode finished", never as "this media is transcoded": 1 was the reading throughout the clear, when the Version held no media at all.The target does not end up with the source's rendition set. Uploading the source's mp4 produced a second transcode of an already transcoded file (
<hash3>_<hash>_bunny.mp4), a thumbnail and a filmstrip, and leftsg_uploaded_movie_imagenull, which the source had. Compare Versions on the file you sent, not on which fields are filled.Attachments accumulate and the clear does not touch them. One file synced twice left 5 Attachments on the target: the seed, both uploads and both transcodes.
PUT … nullunlinks nothing;DELETE /entity/attachments/{id}does, and only the rows you made.The download's
Content-Typeis not the media's. Reading the round-tripped file back servedbinary/octet-streamwhile the field readsvideo/mp4. Trust the field'scontent_type.Wrap the download so the temp file is removed even when the upload raises. A failed sync that keeps its scratch file fills the disk of whatever runs the job.
corpus/recipes/006_media_round_trip.md
007_build_and_reconcile_a_cut
Write a Cut and its CutItems from an edit, read the timeline back, and reconcile a second edit against the Cut already there
The four stages exist because a batch cannot use an id it creates (
recipes/002). A CutItem needs a Cut id, a Shot id and a Version id at once, and a Version needs a Shot id, so the graph is four levels deep and each level is its own call. Within a level, batch, and chunk at around 200 requests.An id alone does not say which Cut a row is on.
coderepeats across Cuts:[["code", "is", "reel1_sh010"]]returned items(46, cut 19)and(53, cut 20). A blindPUT /entity/cut_items/53with nocutkey answered 200, leftcutat 20, and overwrote that Cut's metadata.Before updating, confirm the row's Cut: filter on
cutwhen reading, or ask forcut.Cut.idinfieldsand drop every id that does not match. A dotted read through this singleentityfield works, unlike one through a multi_entity field (probe 016).Sending
cutin an update moves the item.PUTwith{"cut": {"type": "Cut", "id": other}}answers 200 and the item leaves its old Cut. So does the other side:PUT /entity/cuts/<a>with{"cut_items": {"multi_entity_update_mode": "add", "value": [...]}}answered 200 and left the item's former Cut holding[].CutItem.cutis single-valued, so anaddon the parent is a re-parent, not an addition.Cut.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.Read the items with
POST /entity/cut_items/_search,[["cut", "is", {"type": "Cut", "id": N}]],sort: "cut_order". Anullcut_ordersorts last in both directions.No frame rate is reachable from a CutItem.
Cut.fpsis the only rate on either type, it isnulluntil someone writes it, and no CutItem field points at the Cut's value. ReadCut.fpsonce and pass it down;floatreads back as a string, sofloat()it (field_types/float).Drop frame is expressible only inside the
textfields, which validate nothing, so the client owns that flag too.Deleting a Cut does not delete its CutItems.
DELETE /entity/cuts/<id>answered 204 and the item survived withcutnull, reachable only through[["project", "is", ...], ["cut", "is", None]]. Delete the items first. A delete inside a batch is not idempotent: a second delete of the same id 404s and takes the whole batch with it (recipes/002).Nothing about a Cut is unique either. Three Cuts created with the same
codeall answered 201.revision_numberis a plain number the client maintains, and the display name the server builds from it iscodeplusv%03d:reel1 v001,reel1 v002, and barereel1whenrevision_numberisnull. "The current cut" issort: "-revision_number"over acodefilter.Cut.entityaccepts['Sequence', 'Scene', 'Episode', 'Reel']andCut.versionaVersion, whose reverseVersion.cutsfills in on the same write.
corpus/recipes/007_build_and_reconcile_a_cut.md
008_delivery_progress
Keep a Delivery honest about what a long transfer is doing, including when it is cancelled and when it crashes
Write the pair from a
finally, not from the success path. An uncaught exception between two progress writes leaves a Delivery readingipand a line describing work that stopped an hour ago, and nothing in the API times a row out.A
200on the write is not proof of the value. Re-read the row (probe 028);say()above returns the re-read, not the response to thePUT.Always send
entityon the Reply. A Reply created without one cannot be deleted:DELETE /entity/replies/<id>answers 400 code 104undefined method 'reflect_on_association' for class NilClass(entity_types/Reply). A failure reporter that drops the link leaves permanent litter on exactly the runs that already went wrong.Reply.entitynames 113 of the 114 types in/schema,Deliveryamong them, andDelivery.repliesis one of only two fields anywhere withvalid_types: ['Reply'](entity_types/Reply). A Reply on a Delivery is ordinary, and it is readable back both fromDelivery.repliesand fromPOST /entity/replies/_searchon[["entity", "is", {"type": "Delivery", "id": N}]].Delivery.reply_contentis not the thread. It read'Warning: If you see this displayed in the UI, it means the widget is not respecting grid_column = false.'on a Delivery holding one real Reply. Readreplies.Reply.cached_display_namecomes back HTML-escaped wherecontentdoes not: a traceback containing"reads back with"incached_display_nameand with"incontentand in thenameof theDelivery.replieslink.Writing
""todescriptionstoresnull, so an empty progress line erases the previous one rather than blanking it (field_types/text). Send a real line every time.sg_delivery_typehasvalid_values: []on the probed site, so every write to it is400 … 'Final' is not a valid list value. Valid list values: ''.An empty vocabulary is a field that cannot be set, not a free-text field.Uploading with no field in the path stores the file as an Attachment on
Delivery.attachments(probes 013, 014). Delete the Attachment rows, not just the link, when a run is rolled back.
corpus/recipes/008_delivery_progress.md
009_multi_entity_safely
Add to and remove from a multi_entity field without destroying the links you did not mean to touch
The removal direction is the dangerous one. An append that goes wrong loses one link; a removal that skips the other-parents check breaks a relationship something else still needs, and a child that left one parent is not a child nothing claims.
Remove from the parent, then ask
[[<field>, "is", <child hash>], ["id", "is_not", <parent id>]]on the parent type, and strip the child only on an empty answer.On the probed site the same query over an existing project answered
200, [332, 4473, ... 4491]for one Shot, 20 Notes claiming it, 19 once the one it left is excluded.Use
iswith one entity hash for that query. A bare id is400 API read() invalid/missing entity hash: 954,iswith a list is400 'is' 'relation' expects a 1-element array, andinmeans "links any of", which on some fields returns the rows that link nothing when a member is unresolvable (field_types/multi_entity).One
_searchanswers for one child; batch it by askingin [child, child, ...]and grouping the returned parents yourself.The query-string trap.
?multi_entity_update_mode=addand?options[multi_entity_update_modes][<field>]=addboth answer 200 having replaced the whole list. The loss is a success response, so the mode is only ever correct in the body (field_types/multi_entity, probe 028).The lost-update race. Read-then-PUT is not an append. A bare list replaces, the window between the read and the write is open, and no conditional write closes it:
If-Match,If-Unmodified-SinceandIf-None-Matchare ignored at 200 andupdated_atechoed back is400 editable on create only(probe 024). The wrapper is not a narrower window, it is no window.Verify by re-reading.
?fieldsis ignored on every write (probe 024) and a 200 proves nothing about amulti_entityfield, since the query-string form returns one after replacing (probe 028).Compare the set you wanted against a fresh
GET /entity/<slug>/<id>?fields=<field>. A dotted path is not a shortcut:?fields=versions.Version.codeanswered 200 withattributesandrelationshipsboth empty (probe 016).Order is not stored.
Playlist.versionsreads back sorted by the target'scode, whatever order was written, and the human order issg_sort_orderon thePlaylistVersionConnectionjoin row, which a write through the field leaves null.removethenaddthe same member replaces the join row and the order with it, so reorder by writingsg_sort_order, never by rewriting the member list (entity_types/Playlist).Know the field before sending a bare list.
PUT {"replies": []}on a Note deletes the Reply rows outright, and the ids answer 404 afterwards (entity_types/Note). A bare list is a replace on most fields and a delete on some, and nothing in the response distinguishes them.A multi_entity field reads back as a list. Unset is
[], never null and never an absent key.relationships.<field>.datawas a list on every read taken here: 100 rows ofNote.note_linksand 100 ofVersion.playlistsandVersion.tasksfrom_search, 20 of those re-read singly byGET, and 72 sandbox reads split across 0, 1 and 2 members overGET,_searchunder both filter Content-Types, andGET .../relationships/<field>.One implementation reported by the survey defends against the field coming back as a single mapping instead; that did not reproduce, so the defensive read below is recorded unverified, on the survey's word rather than on a measurement here.
The one field that does return a mapping is a single
entityfield,{"data": {"id", "name", "type"}}, which is what a caller reading the wrong field name gets.d = row["relationships"][field]["data"] or [] d = [d] if isinstance(d, dict) else d
corpus/recipes/009_multi_entity_safely.md
010_status_picker
List the statuses a project actually offers, each with the label, colour and icon needed to draw it
Two calls, not one per status
Icon.url is empty unless image_data is asked for beside it
The three renderings
Rediscover the sprite; never hardcode it
Fall back to bg_color
The picker is not a validator
The other pieces
Read the schema per entity type. Codes do not transfer: on the probed site Version has 16 and Task 10, overlapping on five, and
HumanUserhasactanddis, which no other type offers.display_valuesis a map from code to label and a key can be missing, so fall back to the code rather than dropping the option.valid_valuesorder is the order to show. It is not alphabetical by label, and there is no substring operator on astatus_listfield, so a type-ahead filters the list client-side (field_types/status_list).icon_typehas two values and no more. Over all 98Iconrows on the probed site:permanent_status/image_map94,custom_status/html3,custom_status/image1. Page 2 of the same listing returned 0 rows, so that census covers the whole table (probe 006).Probe 010 left the question open; the answer is site configuration, and a site with more custom statuses will hold more
custom_statusrows, not a thirdicon_type.
corpus/recipes/010_status_picker.md
011_audit_webhook_subscriptions
Inventory every webhook subscription on a site, and see which have ever delivered
The listing is the whole site. A script with API access reads every other integration's consumer url, its
entity_typesand its project scoping. Treat the output as sensitive.The secret token is never returned.
is_token_setis the only readable fact about it.num_deliveries: 0on anactivehook means it has never delivered in its lifetime, not that it is idle. That is the one field that separates a working subscription from a subscription that was accepted and never fired, which the create contract cannot tell you (045_webhooks).A hook may name
entity_typesorevent_type, never both, so read whichever is populated (050_webhook_subscriptions).statusis one ofactive,unstable,failedordisabled. Onlydisabledis set by a caller; the other three are the site's own assessment of the endpoint's health.
corpus/recipes/011_audit_webhook_subscriptions.md
012_sign_in_as_a_person
Reach the REST API as a person, with no script key and no password, by having them approve a login in their browser
useris aHumanUser, so every row this bearer writes has the person ascreated_byand, on a Version, asuser, which the web UI shows as Artist. A script key gets the same result only throughsudo_as_login, and that needs an administrator to grantcan_impersonate_this_userper person (027_auth_permissions). This needs nobody.The person's permission level applies, not a script's. Rows collapse to what they may see (
027_auth_permissions); on the probed site the approver was in theAdminset and saw everything.expires_inis 600, the same as a script token, andgrant_type=refresh_tokenon the returnedrefresh_tokenanswers 200 with another 600. Re-minting from the session token costs the same one call and needs no refresh bookkeeping.The session token is the credential to keep, not the bearer. How long the site keeps it alive is the site's
User Session Expirypreference (052_app_session_launcher), and is not returned anywhere.machineIdis not checked when polling, so it is a label for the person, not a binding.
corpus/recipes/012_sign_in_as_a_person.md
013_publish_file_bytes
Publish a file's bytes onto a PublishedFile when the caller has no LocalStorage root to write under
The url is not the file. It is minted per read and signed for 900 seconds, so persist
path["id"], the Attachment, and re-read the field when the bytes are wanted (field_types/url). Two reads of the same unchanged row return two different strings.local_path_macis absent, not null. A reader doing(path or {}).get("local_path_mac")returnsNonefor every row published this way and reports it as a row with no path. Branch onlink_typefirst: anuploadorwebvalue hasurl, alocalvalue has the three platform paths and nourl(field_types/url).Deleting the PublishedFile leaves the Attachment.
DELETE /entity/published_files/<id>answered 204 andGET /entity/attachments/<id>still answered 200 with the filename. The bytes stay on the site until the Attachment is deleted by id, which is the same accumulation alocalpath write causes on every rewrite (recipes/004_register_published_file).file_sizeon that Attachment readsnulleven after the bytes landed, so it distinguishes nothing (endpoints/put_links_upload).Step three answers 201 whether or not step two ever ran, and its body is a single space rather than JSON (
endpoints/post_links_complete_upload). Fetch the url to prove the bytes exist.A
file://url is a third shape, and it moves nothing.{"url": "file:///…", "name": …}on the same field answers 201 on create and 200 on aPUT, atlink_typeweb, and stores the string only: no bytes reach the site and any reader without that mount sees a dead link. A space in the url is refused, so percent-encode before sending (field_types/url).The three-call flow needs no LocalStorage row to exist on the site at all, and no
published_file_type. What it costs is that the file is one object: a sequence has to be archived first, and the archive is what a consumer downloads.
corpus/recipes/013_publish_file_bytes.md
014_notes_about
Find the Notes about a Shot, Asset or Version by the name of the thing, and read what each Note is linked to
cached_display_nameresolves for every one of the 28valid_types.codeis 400 onBookingandnameon every type butDepartment, with the samedoesn't exist.string for a wrong type and a wrong field.?fields=note_links.Shot.codeanswers 200 with the key absent fromattributes(probe 016). The links are readable only throughnote_linksitself, as{id, name, type}triples.["note_links", "contains", NAME]without a type is 400'multi_entity' data type doesn't support 'contains' 'relation'. There is no search across all types in one filter by name; resolve ids first and use["note_links", "in", [{"type": ..., "id": ...}, ...]].A Note about a Task is in
tasks, notnote_links. Search it ontasks.Task.content.Two hops resolve:
note_links.Shot.sg_sequence.Sequence.codenarrows to a sequence.
corpus/recipes/014_notes_about.md