Workflows

Work on a saved project

Turn a one-off strip into an editable project you can reopen, edit, version and re-render weeks later, including durable Hosted production.

View as Markdown

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 launch project so I can reopen it, edit 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.

Stage three: the weather app strip saved as an editable project with a look, captions and two locales.
The same five panels, saved as an editable project with its look, its captions and its two locales.

#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.

Tidebook already ships on the App Store, so nothing about its identity needs typing. Say which app it is and search_app_listing finds it:

The app is Tidebook, the tide forecast one. Use its real App Store details.

Each result carries the app's exact name, its seller, its icon, its bundle ID and its numeric app ID, plus the App Store Connect languages and the iPhone devices ShotOps would apply. Confirm which result is yours — the icon and the seller are what prove it — then pass that result's appId, or its storeUrl, as appListing on the save:

{
  "appListing": "1234567890",
  "screenshots": ["…"]
}

The new project takes the app's name, bundle ID, language list and iPhone device set. Anything you state yourself wins: a projectName, an outputs list or a locale is applied exactly as given and the app is still linked. Passing appListing on an existing project re-links that project and rewrites nothing else — the language list, the frames and every caption stay as they were. From then on read_project reports the app as linkedApp.

Both calls read Apple's public listing data, the same page anyone can open. No Apple or store credential is involved, and finding a listing proves nothing about who owns the app — the bundle ID stays editable, and the authenticated release path verifies ownership separately. An app released in one country only is invisible to a search of any other, so pass country when yours does not come back.

#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 edit

read_project is read-only and returns the whole current state:

FieldWhat it answers
project_fileThe designer's strip: panels in their order, captionText keyed by locale then panel, locales, baseLocale, styling.
updatedAtRemember it. A newer value on the next read means somebody changed something.
screenshotsWhether the project holds its own pixels, and how completely.
versions, heldVersionThe saved look history, when the plan includes it.
layoutWhich 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 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.

#Edit and re-render

edit_project applies a concrete change and saves it: the caption words for a frame in a locale, a new frame order, a layout or preset for every frame, caption style, background, or a device — added, removed, duplicated, given a new screenshot, hidden, layered, posed or placed. Frames and devices are numbered from 1, the way read_project lists them.

{
  "project": "Tidebook 2.4 launch",
  "order": [1, 2, 4, 3],
  "captions": [{ "frame": 3, "text": "Alerts before the swell" }]
}

A caption in a locale the project does not have yet adds that locale. A value the engine cannot use is refused with the exact field and what it accepts, and nothing is saved. The edit is written against the version it read, so a Studio save made in the meantime is never overwritten; read the project again and reapply.

#Place devices across frames

Ask for one continuous scene, such as "put the forecast phone large across frames 1 and 2", and the agent can place it by calculation instead of rendering and guessing.

The frames sit side by side as one strip. Its origin is the top-left corner, x runs right, y runs down, and the unit is pixels at the project's base output panel size. For iPhone 6.9 that is 1320 × 2868 per frame, so frame 2 spans x 1320 to 2640. A placement names the top-left of the device box and either its width or its height; the device keeps its rendered proportions:

{
  "project": "Tidebook 2.4 launch",
  "devices": [
    { "id": "shot-2", "placement": { "coordinateSpace": "strip", "unit": "px", "x": 700, "y": 900, "height": 1800 } },
    { "id": "shot-1", "z": 1 },
    { "action": "remove", "frame": 3, "device": 2 }
  ]
}
  • A device belongs to the frame under its centre. It paints into the neighbouring frame when it overlaps the seam, unless its clipToFrame is on, and the exported panels placed side by side reproduce the strip exactly.
  • A higher z paints on top. Captions always paint above every device. hidden keeps a device in the project but out of the render and the export.
  • hOffset, vOffset and deviceSize still place a device inside its own frame. describe_look lists the exact relation between them and strip pixels under editProject.strip.

Every edit returns geometry: each device's box in strip pixels, the frames it reaches, its layer, and each overlap with both boxes, the intersection, and its size. read_project returns the same block when you pass geometry: true.

Then re-render:

{ "project": "Tidebook 2.4 launch", "output": "panels", "delivery": "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: render({ project, output: "bundle", bundleId }) with no screenshots zips the project's own pixels.

#Versions

Every save_look — and every Save this version in the web app — appends a retained version and restyles the project to it, so the next render uses it. A designer who keeps saving keeps changing what agents render; to go back to an approved design, restore that version from the web app's version history.

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 fromWhat 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-backedThe raw PNGs are uploaded as account refs after the project is created.
Local { path }, unsignedThe 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.