Workflows
Work on a saved project
Turn a one-off strip into an editable project you can reopen, refine, version and re-render weeks later, including durable Hosted production.
A rendered strip is images. A project is the design itself — frame order, per-locale words, styling
and, if you let it, the screenshots too. Once Tidebook 2.4 launch exists as a project, every later
render is one call with nothing attached to the conversation.
Save this as the
Tidebook 2.4 launchproject so I can reopen it, refine it and render it again without attaching the screenshots.
Expected result: ShotOps returns an openUrl; an authenticated save also returns the owned
project id, and a later read_project confirms which screenshot cells are available.
#Save the strip as a project
Call save_project with no project argument and it creates a new one. Ask for
this when you want the strip to survive the conversation. Nothing before this step saved anything: a
render returns images and reports that no project was created, alongside an offer to make one.
{
"name": "Tidebook 2.4 launch",
"screenshots": [
{ "ref": "…01_today…", "name": "01_today.png" },
{ "ref": "…02_forecast…", "name": "02_forecast.png" },
{ "ref": "…03_spots…", "name": "03_spots.png" },
{ "ref": "…04_log…", "name": "04_log.png" },
{ "ref": "…05_alerts…", "name": "05_alerts.png" }
],
"style": { "layout": "bleed", "captions": ["…"] }
}
It is fast and it never renders: it reads the strip structure and screenshot descriptors, never the image bytes.
An agent saves when you ask it to, not at the end of every job. Wanting to keep, share, reuse or keep
editing the work is the request that produces a project; asking for images, full-resolution panels or
a bundle is not, and neither is saying nothing. Pass project only when you name the existing project
the work belongs in — an agent never infers an update target from whichever project was edited most
recently.
#Claim it in the browser
save_project always returns an openUrl. Hand it back.
Signed in, or token-backed. You also get a projectId. The project is owned the moment it is
written.
Unsigned, on your own machine. The server cannot know who the owner is, so it returns
status: "pending_claim" and a claimId rather than inventing a projectId. It stages the record
and the raw source PNGs in private claim storage so the project opens whole, and the first sign-in
through that link — good for seven days — makes that person its owner. An existing project cannot be
updated this way: without the token that owns it, there is nothing to update.
#Open it again later
Every project argument takes the id or the name as the user says it, matched exactly apart from
case and spaces. So "render my Tidebook 2.4 launch project" works without anyone copying a UUID.
Omit project entirely and you get the most recently edited project, which is the right answer only
when the user actually said "my latest". A name that matches nothing, or matches two projects, comes
back refused with that account's projects listed — ask which one they meant and pass its id.
#Read before you refine
read_project is read-only and returns the whole current state:
| Field | What it answers |
|---|---|
project_file | The designer's strip: panels in their order, captionText keyed by locale then panel, locales, baseLocale, styling. |
updatedAt | Remember it. A newer value on the next read means somebody changed something. |
screenshots | Whether the project holds its own pixels, and how completely. |
versions, heldVersion | The saved look history, when the plan includes it. |
layout | Which composition template the strip is on, derived from its values — nothing stores it, so it is never stale. |
It never returns screenshot bytes. Zero-custody means it reports that pixels exist, not what they are.
screenshots.available: true is the field that decides your next call: it means
render_project can render this project with no screenshots argument at all.
Read it rather than inferring from filenames. Filenames help with intake and local reload; they do
not establish whether the project still has its source bytes.
#Refine and re-render
refine_project changes a saved project in words. One call runs one bounded agent
turn: a supported edit commits exactly once and returns the actual diff, while ambiguity, missing
input, an unsupported intent, a conflict or a failure each return their own typed outcome with
nothing partial saved.
{
"project": "Tidebook 2.4 launch",
"instruction": "Move the Alerts panel to the end and shorten its headline."
}
When the change needs new pixels — swap this screen, add the checkout screen — get them into the account first and pass the refs:
{
"project": "Tidebook 2.4 launch",
"instruction": "Use the new checkout capture on the second screen",
"attachments": [{ "ref": "uploads/you/tidebook/checkout.png", "name": "checkout.png" }]
}
Then re-render:
{ "project": "Tidebook 2.4 launch", "output": "urls" }
Supply screenshots only to override what the project holds. A newly supplied file uses its name and
coordinate to find the intended shot, so order does not matter and you must not pre-sort. Once it is
saved, the source is bound to that shot's stable device/locale cell: renaming or duplicating a filename
cannot redirect persisted pixels. The response reports resolvedAt and inherited per shot, so never
assume a successful render used the exact pixels you had in mind. A shot with neither a stored
screenshot nor a supplied file appears as a reported gap. A render can show the incomplete strip while
it is still being assembled; a bundle refuses until the source is supplied.
A full-resolution Hosted re-render is an addressable production operation. Keep the returned id,
poll it with production_operation, and request its result after success rather
than resubmitting the project render. Local re-renders remain synchronous.
Packaging works the same way: emit_bundle({ project, bundleId }) with no screenshots and no panels
zips the project's own pixels.
#Versions, Hold and Follow latest
Every save_look — and every Save this version in the web app — appends a
retained version.
- Follow latest is the default: renders use whatever look was saved most recently. A designer who keeps experimenting keeps changing what agents render.
- Hold pins one version as the project default.
hold_looksets it,release_lookclears it. Pin before a release so a pipeline keeps shipping the approved design while the designer carries on.
What a version contains, and what Free can read of the history, is in Looks and versions.
#The screenshot cells a project keeps
A project's screenshots are not a flat list. They are a grid: one cell per shot × device family ×
locale. read_project reports it in screenshots.cells[], one row per cell, each naming the
variant requested, the resolvedAt cell actually used, and which axes were inherited.
Whether a project holds pixels at all depends on how it was saved:
| Saved from | What the record keeps |
|---|---|
Account-scoped refs (request_screenshot_upload, import_screenshot) | A manifest pointing at those uploads. The project reopens with the screens already in it, on any device the owner signs in on. |
Inline base64, or { url } | No pointer at all. The project opens empty and asks the user to re-load their own files. |
Local { path }, token-backed | The raw PNGs are uploaded as account refs after the project is created. |
Local { path }, unsigned | The same PNGs are staged privately and promoted when the claim is owned. |
A cell with no screenshot of its own inherits, and pixels inherit along a deliberately strict path: device family dominates, and the chain never wanders sideways into another family's locale cell. Ship every locale covers what that means when German is late, and Screenshots and assets covers what deleting a ref does to a project that still points at it.
If an older project has the same filename on multiple shots, ShotOps leaves that legacy source
unassigned rather than guessing. The result contract reports an ambiguous source and the remedy;
preview can show the gap, but store-ready output refuses it until the source is repaired.