Reference
render_project
Re-render a saved project — the route that needs nothing attached to the conversation.
What it does
- Writes
- Account required
The account route. By default it renders the screenshots the project already holds, so "render my ShotOps project" is one call with no screenshots argument: no upload, no shell, and the image bytes never enter the chat. That makes it the route that works everywhere, ChatGPT on a phone included. The project supplies the structure — frame order, per-locale caption words, styling — so the result reproduces the designer’s exact strip.
You can ask: “Render the German Tidebook panels from the saved project.”
Input
projectstringoptionalat least 1 characterWhich ShotOps project to target: its id (as returned by read_look/save_project), OR its NAME as the user says it — matched exactly, ignoring case and surrounding spaces. Omit = your most recently edited project. A name that matches none or several is refused with the account's projects listed, never resolved by guessing.
screenshotsarrayoptionalat most 60 itemsOPTIONAL. OMIT it (or pass []) to render the screenshots the project ALREADY holds — the files the user or designer put in it in the web app — with no upload and no file transfer of any kind. Supply this FLAT list only to OVERRIDE them with your own raw app screenshots: each an inline base64 PNG, { "ref" }, { "url" }, or (local stdio only) { "path" }, and each with a
name(its original filename). Object entries may also carryvariant: { family?, locale? }; that coordinate overrides the same stored cell, while an entry withoutvariantis the base cell and remains the fallback for every render (the legacy behaviour). Entries are matched to the saved project's shots BY FILENAME (name === the shot's frameName) AND then resolved for each rendered device family + locale, so order does NOT matter and you never pre-sort. A supplied cell wins over the stored one at that coordinate. A shot with neither is reported as missing (it does not error the render). Prefer ref/url for real screenshots so the bytes never transit this conversation.each item
any
The published schema does not constrain this element, on purpose — a malformed entry comes back as a sentence instead of a stacked union error. What the server re-validates against is below.
localestringoptionalApp Store locale, e.g. "de-DE" (default en-US). Labels the render, selects which { "locales": … } screenshot variants render, and, for emit_bundle, selects the fastlane screenshots folder. Does not select caption text — pass the copy for this locale yourself via
style.captions[].text(see the README'scaptions.<locale>.jsonconvention).outputsarrayoptionalat least 1 itemat most 2 itemsThe target DEVICES this project is for — iphone-6-9 (1290×2796) and/or iphone-6-5 (1242×2688), in the order they should be offered. On save_project this is saved ON the project (the same Output list the web app shows) and the first entry becomes the previewed size unless
panelPresetIdsays otherwise; on render_project it OVERRIDES the saved set for this call. Omit it and the project's saved outputs are used unchanged.each item
string
one of
iphone-6-9iphone-6-5previewbooleanoptionalRender at ~25% resolution for a fast styling preview at a quarter the render cost — small enough to always come back inline. Not for final delivery: re-render without
preview(or use emit_bundle directly) once the look is right.outputstringoptionalHow to return rendered bytes: "inline" (base64), "urls" (uploaded, short-lived signed download URLs — use for real/full-resolution renders so bytes never transit this conversation), or omit for auto (inline under ~200KB total, urls above).
one of
inlineurlswaiversarrayoptionalApprovals for readiness findings this run would otherwise be blocked on. Pass back the exact
contract.readiness.findings[]id and digest from apreview: truerun, or from the refusal you just received. Each waiver clears ONE finding; fatal findings are never waivable.each item
object
findingIdstringrequiredThe
contract.readiness.findings[].idthis waiver authorizes. Exactly one.findingDigeststringrequiredThat finding’s
digest. A digest from an earlier run no longer matches and is rejected as stale.policyVersionstringrequiredcontract.readiness.policyVersionas it stood when the finding was raised.waivedBystringrequiredThe account that approved it. Must be the account making this call.
waivedAtstringrequiredISO 8601.
reasonstringoptionalFree text, for the receipt. It authorizes nothing on its own.
idempotencyKeystringoptionalat least 1 characterat most 200 charactersStable retry key for paid Hosted production. Reuse it only for the identical request; a new key deliberately requests a new render. Omit it for deterministic fingerprint retry safety.
What the server actually accepts
The published schema above deliberately accepts a wider shape for screenshots, so a malformed entry comes back as a sentence rather than a stack of union errors. The server re-validates against these shapes and refuses anything else.
screenshotsarrayoptionalat most 60 itemsOPTIONAL. OMIT it (or pass []) to render the screenshots the project ALREADY holds — the files the user or designer put in it in the web app — with no upload and no file transfer of any kind. Supply this FLAT list only to OVERRIDE them with your own raw app screenshots: each an inline base64 PNG, { "ref" }, { "url" }, or (local stdio only) { "path" }, and each with a
name(its original filename). Object entries may also carryvariant: { family?, locale? }; that coordinate overrides the same stored cell, while an entry withoutvariantis the base cell and remains the fallback for every render (the legacy behaviour). Entries are matched to the saved project's shots BY FILENAME (name === the shot's frameName) AND then resolved for each rendered device family + locale, so order does NOT matter and you never pre-sort. A supplied cell wins over the stored one at that coordinate. A shot with neither is reported as missing (it does not error the render). Prefer ref/url for real screenshots so the bytes never transit this conversation.each item
one of 4 shapes
1
stringInline base64-encoded PNG (no "data:" prefix). Capped at 3MB of image data — a real full-resolution screenshot is 3-4MB and will NOT fit. Use request_screenshot_upload and pass { ref } instead; inline is for thumbnails and tests.
2
refA ref returned by request_screenshot_upload — resolved server-side, never re-sent inline.
refstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
variantobjectoptionalWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
familystringoptionalat least 1 characterat most 80 charactersDevice screenshot family (for example "android"). Omit for the project's base family; the two currently advertised iPhone outputs share that base family.
localestringoptionalat least 1 characterat most 80 charactersApp Store locale for these pixels. Omit for the project's base locale.
3
urlAn https URL to a PNG or JPEG — fetched server-side, converted to PNG when needed (no redirects, ~20MB cap).
urlstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
variantobjectoptionalWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
familystringoptionalat least 1 characterat most 80 charactersDevice screenshot family (for example "android"). Omit for the project's base family; the two currently advertised iPhone outputs share that base family.
localestringoptionalat least 1 characterat most 80 charactersApp Store locale for these pixels. Omit for the project's base locale.
4
pathA local filesystem path to a PNG, read straight off disk — ONLY available over the local stdio server (npx shotops-mcp); the hosted server rejects this entry shape.
pathstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
variantobjectoptionalWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
familystringoptionalat least 1 characterat most 80 charactersDevice screenshot family (for example "android"). Omit for the project's base family; the two currently advertised iPhone outputs share that base family.
localestringoptionalat least 1 characterat most 80 charactersApp Store locale for these pixels. Omit for the project's base locale.
Result
okbooleanrequiredcountnumberoptionalpanelPresetIdstringoptionalpanelWidthnumberoptionalpanelHeightnumberoptionalrenderMsnumberoptionaloutputstringoptionalone of
inlineurlslocalestringoptionalnextStepobjectoptionalactionstringrequiredalways "offer_save_project"An offer to put to the user. Never a command, and never consent already given.
toolstringrequiredalways "save_project"The door that would persist this work, if they ask for it.
projectCreatedbooleanrequiredalways falseNo project was created or updated by this call — the same fact as
contract.effects.projectMutation.consentstringrequiredalways "required"Saving needs the user’s own preserve/reuse/keep-editing intent. A client that cannot ask returns this offer unresolved rather than saving.
reasonstringrequired
creditsobjectoptionalCredits this call spent from the account wallet, the balance left afterwards, and when that wallet is next topped up (ISO 8601; absent when no further refill is scheduled). Renders cost per panel; reads, saves, uploads and deletes are free, as is bundling panels that were already rendered.
debitednumberrequiredremainingnumberrequiredrefillAtstringoptional
notestringoptionalFree-text advisories about THIS call, joined into one string. Written when the server did something you did not literally ask for or could not have known: a look/version fallback, an output-mode downgrade, a device this server cannot render, a
style.shotLookalookoverrode, or placement set by hand where astyle.layouttemplate would have produced the same composition. Never an error — the call succeeded. Read it, and act on it next call.layoutobjectoptionalWhat is wrong with this render’s COMPOSITION, measured on the panels it just returned. ABSENT means there is nothing to report — a clean strip, or one whose captions could not be measured. Advisory: the panels are already rendered and nothing here changed a pixel.
findingsarrayrequiredeach item
object
kindstringrequiredWhat overlaps what. Captions must never overlap each other or a device; devices MAY overlap, and are only reported here when the pair does NOT read as composed (no real difference in scale, camera angle or roll).
one of
caption-captioncaption-devicedevice-deviceframesarrayrequired0-based indexes into the returned
panels. For a caption finding the FIRST entry is the frame whose caption is the one to change; a second appears only when the other party sits in a different frame.each item
number
depthPctnumberrequiredHow deep the overlap runs, in whole percent of panel height — or, for
device-device, the percentage of the smaller device the other one covers.anchoredbooleanrequiredtrue ⟹ a caption was placed by hand (an explicit
anchor), so nothing may rewrite it.clearAtScalenumberoptionalThe verified factor that clears this frame’s auto-placed captions — 0.82 means 82% of their current size. Absent when no size above the legibility floor clears it, or when the offending caption is a hand placement.
correctedLookanyoptionalThe look this render used, with every caption shrunk to the largest size verified to clear the overlap. Pass it back as
lookto re-render. Absent when no repair is deterministic — a device-device overlap is a taste call (give the pair a real difference in scale, angle or roll) and a hand-anchored caption is a human placement.notesays which.
contractobjectoptionalThe versioned ShotOps result contract (#662): what ran, where, what it changed, what it cost, what it produced and — on a refusal — a typed failure over a closed code catalog. Structured content is authoritative; the prose beside it is a rendering of this block.
contractVersionstringrequiredThe version of THIS envelope, REPORTED. Compare it against the version you were written for. There is no version negotiation: no tool accepts a requested version, so this is never a refusal — it moves only when a field changes meaning, and additive fields never move it.
statusstringrequiredone of
succeededpartialfailedrefusedacceptedqueuedrunningcancel_requestedcancelledterminalbooleanrequiredfalse ⟹ this operation is still running and will be reported again (#666).
operationobjectrequiredidstringrequiredIdentity for THIS call, unique per invocation. Quote it in a bug report.
toolstringrequiredThe registered tool name that produced this result.
kindstringrequiredone of
renderbundlereadmutateuploaddeletestatusdurableobjectoptional#666 domain handle for addressable long-running work. Absent from synchronous calls.
operationIdstringrequiredpollWithstringoptional
executionobjectrequiredlocationstringrequiredWhere the work RAN.
localis the caller’s own machine over stdio.one of
localhostedinputModesarrayrequiredEvery way this door accepts screenshots and assets.
each item
string
one of
inline_base64stored_refremote_urllocal_pathproject_storeddeliveryModesarrayrequiredEvery way this door can hand a result back.
each item
string
one of
inline_base64signed_urllocal_pathshare_link
effectsobjectrequiredWhat this call DID to the world, independent of what it returned. Every flag is stated on every result, false included: an absent flag would be indistinguishable from an effect nobody thought to declare.
networkFetchbooleanrequiredThis call fetched bytes from a host neither ShotOps nor the caller controls.
uploadbooleanrequiredCaller bytes were uploaded into ShotOps storage.
retainedStoragebooleanrequiredSomething survives this call in ShotOps storage.
projectMutationbooleanrequiredA saved project or its look history changed.
publicationbooleanrequiredSomething became reachable outside the account — a share link.
deletionbooleanrequiredSomething was permanently removed.
costobjectrequiredunitstringrequiredalways "credit"Public cloud credits — the same unit every ShotOps surface quotes.
modelstringrequiredHow this connection pays.
unmeteredis local stdio, which renders on the caller’s own machine;anonymous_allowanceis the unsigned hosted taste.one of
meteredanonymous_allowanceunmeteredestimatednumberoptionalWhat the call was expected to cost, before it ran.
reservednumberoptionalHeld against the wallet for the duration of the call (#666).
releasednumberoptionalGiven back — an unused reservation or a refund after a post-charge failure (#666).
settlednumberoptionalActually taken. Absent when nothing was charged.
balanceAfternumberoptionalThe wallet’s public cloud credit balance once this call settled.
refillAtstringoptionalISO 8601. When the wallet is next topped up; absent when none is scheduled.
progressobjectoptionalPersisted monotonic durable-operation progress. Never inferred from transient render metadata. Relay
panelsCompleted/panelsTotaland the estimate to the waiting user; the item counters are internal bookkeeping and do not match what they asked for.completedintegerrequiredmin 0Internal scheduling items done — panels PLUS the bundle and result items. Do not quote this to a person.
totalintegerrequiredmin 0Internal scheduling items in total. Larger than the panel count.
panelsCompletedintegeroptionalmin 0Panels finished, in the unit the caller asked in. THIS is the number to report.
panelsTotalintegeroptionalmin 0Panels this operation will produce — the count the user asked for.
attemptintegerrequiredmin 0heartbeatAtstring or nulloptionalestimatedRemainingSecondsnumberoptionalmin 0Rough seconds of rendering left, from this operation’s own measured pace once a panel has landed. An estimate, not a deadline — say “about”. Absent when nothing is left to render.
resolvedInputobjectoptional#661 — what the server actually resolved the request to, before any pixel was produced.
snapshotIdstringoptionalpsi_+ the first 32 hex of the fingerprint. Quote it in a bug report.fingerprintstringoptionalsha256 over every resolved fact. Equal fingerprints ⟹ equal production input.
sourcesarrayoptionalOne entry per source cell, resolved or not.
slotis the cell the bytes came FROM — a shot id and coordinate on a saved project, an ordered slot label on a direct render. Never a filename, a ref or a URL.originis absent exactly whenstatusis notresolved: there is nothing the cell came from.each item
object
slotstringrequiredstatusstringrequiredone of
resolvedmissingambiguousoriginstringoptionalone of
inline_base64stored_refremote_urllocal_pathproject_stored
defaultsAppliedarrayoptionalWhat the server chose because the caller said nothing.
each item
string
overridesarrayoptionalWhat the caller said that changed the outcome.
each item
string
readinessobjectoptional#665 — the readiness verdict and any waiver receipt.
statestringoptionalblocked⟹ nothing was delivered.ready_with_findings⟹ delivered, and here is what to know.one of
readyready_with_findingswaivedblockedpolicyVersionstringoptionalThe readiness policy this verdict was computed under. A waiver granted under another one is rejected.
findingsarrayoptionalEvery finding, with
code,waivableanddigest. A waiver names one id AND its digest; there is no wildcard and no code-level waiver.each item
object
idstringrequiredrf_+ 24 hex. Quote it in a waiver.severitystringrequiredone of
infowarnblockmessagestringrequiredtargetobjectrequiredThe exact shot, panel, locale, output or frames this finding is about.
shotIdstringoptionalpanelIndexnumberoptionallocalestringoptionaloutputstringoptionalcaptionLayernumberoptionalframesarrayoptionaleach item
number
evidenceobjectrequiredBounded redacted facts used to derive the finding and its digest.
waiversarrayoptionalThe waivers this call ACCEPTED. Pass these objects back verbatim to reuse them; a rebuilt one is rejected.
each item
object
findingIdstringrequiredfindingDigeststringrequiredpolicyVersionstringrequiredwaivedBystringrequiredwaivedAtstringrequiredcodestringrequiredone of
source_identity_ambiguousoutput_unsupportedoutput_omittedlook_exact_unavailableno_panelspanel_invalid_pngpanel_dimensions_invalidrenderer_failedinput_changed_during_runpanel_source_emptylocale_source_fallbackdevice_source_fallbackcaption_inheritedcaption_locale_fallbacklook_source_fallbackcaption_device_collisioncaption_caption_collisiondevice_device_collisioncaption_legibility_unresolvedscope_filteredlocale_not_live_on_applelook_hold_inactivecaption_text_emptyreasonstringoptional
artifactsarrayoptionaleach item
object
idstringrequiredThe opaque asset id from the custody registry (#663) — or, for
kind: "share", the link’s own token. Never a storage path, and never a signed URL.kindstringrequiredone of
panelbundlescreenshotshareprojectdeliveryobjectrequiredmodestringrequiredone of
inline_base64signed_urllocal_pathshare_linkurlstringoptionalTHE ONE FIELD a signed URL may appear in. Nothing else in this envelope carries one.
expiresAtstringoptionalISO 8601, when the grant above stops working.
bytesnumberoptionalretainedUntilstringoptionalISO 8601, when ShotOps stops keeping the artifact itself.
failureobjectoptionalPresent exactly when
statusisfailedorrefused.codestringrequiredThe closed failure code. Branch on this, never on the sentence.
one of
invalid_inputunsupported_inputproject_not_foundproject_ambiguousno_projectsversion_not_foundresolution_incompletereadiness_requiredreadiness_unmetasset_not_foundasset_in_useauthentication_requiredplan_requiredtrial_choice_requiredquota_exhaustedanonymous_limit_reachedcapability_deniedbilling_unavailablerender_failedstorage_failedpersistence_failedupstream_unavailablepayload_too_largerate_limitedoperation_not_foundidempotency_conflictoperation_unavailableoperation_not_readycontract_version_unsupportedinternal_errorphasestringrequiredHow far the call got.
inputandauthorizationguarantee nothing was rendered, stored, written or charged.one of
inputauthorizationresolutionreservationexecutiondeliverypersistenceretryablebooleanrequiredtrue ⟹ the identical call may succeed later with nothing changed.
nextActionstringrequiredThe one move that resolves this, machine-readable.
one of
nonefix_inputchoose_projectupload_assetsreduce_scopesign_inchoose_planupgrade_planretrywait_and_retrypoll_operationupgrade_clientcontact_supportdetailsobjectoptionalBounded, redacted, code-specific facts (the offending field name, the candidate project ids, the required plan). Never a credential, a signed URL or screenshot bytes.
panelsarrayoptionaleach item
one of 3 shapes
1
string2
refrefstringrequiredurlstringrequiredformat uri
3
positionpositionintegerrequiredmin 1contentIndexintegerrequiredmin 1mimeTypestringrequiredalways "image/png"deliverystringrequiredone of
imageresource_linkrefstringoptional
projectobjectoptionalidstringrequirednamestringrequired
openUrlstringoptionalformat urishotsarrayoptionaleach item
object
frameNamestringrequiredmatchedbooleanrequiredrequestedVariantobjectoptionalWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
familystringoptionalat least 1 characterat most 80 charactersDevice screenshot family (for example "android"). Omit for the project's base family; the two currently advertised iPhone outputs share that base family.
localestringoptionalat least 1 characterat most 80 charactersApp Store locale for these pixels. Omit for the project's base locale.
resolvedAtone of 2 shapesoptionalWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
1
objectWhich screenshot cell this image belongs to. Empty {} is the project's base device family + base locale; a missing cell inherits through ShotOps' pixel fallback rules.
familystringoptionalat least 1 characterat most 80 charactersDevice screenshot family (for example "android"). Omit for the project's base family; the two currently advertised iPhone outputs share that base family.
localestringoptionalat least 1 characterat most 80 charactersApp Store locale for these pixels. Omit for the project's base locale.
2
nullinheritedarrayoptionaleach item
string
one of
familylocalesourcestringoptionalone of
suppliedproject
outputsarrayoptionaleach item
object
deviceIdstringrequiredlabelstringrequiredpanelPresetIdstringrequiredpanelWidthnumberrequiredpanelHeightnumberrequiredcountnumberrequiredpanelsarrayrequiredeach item
one of 3 shapes
1
string2
refrefstringrequiredurlstringrequiredformat uri
3
positionpositionintegerrequiredmin 1contentIndexintegerrequiredmin 1mimeTypestringrequiredalways "image/png"deliverystringrequiredone of
imageresource_linkrefstringoptional
operationIdstringoptionalstatusstringoptionalone of
acceptedqueuedrunningcancel_requestedsucceededfailedcancelledprogressobjectoptionalcompletednumberrequiredtotalnumberrequiredpanelsCompletednumberoptionalPanels finished — the number to report to the user.
panelsTotalnumberoptionalPanels this operation will produce.
attemptnumberrequiredheartbeatAtstring or nulloptionalestimatedRemainingSecondsnumberoptionalAbout how many seconds of rendering are left. An estimate — say “about”.
failureanyoptional
Example
That request maps to this call:
{
"name": "render_project",
"arguments": {
"project": "Tidebook 2.4 launch",
"locale": "de-DE",
"preview": true
}
}One PNG per panel at every device size the project targets, plus a per-shot report saying which shots matched a screenshot and which are missing.
Access and cost
- Needs an account. Not available to an anonymous hosted caller.
- Preview is available to any signed-in account. A full-resolution render is production output and needs an active Pro trial or Pro.
- Hosted pricing is the same as render_strip: 1 cloud credit per preview panel, 4 per full-resolution panel. Local rendering is free.
- A project holds its own screenshots when its owner set its reach to "Your account" in the web app. read_project reports exactly that.
When it refuses
- Shots come back reported as missing.
- That is a report, not an error — the project has a frame with no stored or supplied screenshot. Supply that file, or accept the strip without it.
- You supplied screenshots and they landed on the wrong frames.
- Supplied files are matched to the project’s shots by filename, so the names have to be the originals. Order is irrelevant; the name is everything.
- The reply says production output is not available.
- Nothing rendered, nothing charged. Take the step account_status names, then retry — or render a preview in the meantime.
Where this fits
What the agent is told
The title and description the server publishes on tools/list — this is the copy a model chooses between, reproduced verbatim.
Render a saved project
Re-make the store screenshots for a strip the user already has in ShotOps — the route that needs NOTHING attached to this conversation. By default it renders the screenshots the project ALREADY holds, so "render my ShotOps project" is one call with NO `screenshots` argument: no upload, no PUT, no shell, and the image bytes never enter the chat. That makes it the route that works everywhere, ChatGPT on mobile included. A project holds them whenever its owner set its reach to "Your account" in ShotOps, and read_project reports exactly that as `screenshots.available` — check there rather than assuming the files have to be re-supplied. Supply `screenshots` only to OVERRIDE them with your own raw files: a FLAT list, each with its original filename as `name`, matched to the project's shots BY filename — so ORDER does not matter, you never pre-sort, and a supplied file wins over the stored one. The project supplies the structure — frame order, per-locale caption words (pick one with `locale`), styling — so the result reproduces the designer's exact strip, at every device size the project targets (`outputs` overrides that for this call). Shots with neither a stored nor a supplied screenshot are reported as missing, not an error. Returns per-panel PNGs (`inline` or `urls`). No store credential.