---
title: "Work on a saved project"
description: "Turn a one-off strip into an editable project you can reopen, refine, version and re-render weeks later, including durable Hosted production."
canonical: "https://shotops.dev/docs/mcp/workflows/saved-project"
last_updated: "2026-09-07"
shotops_mcp_version: "0.9.6"
result_contract_version: "2.0.0"
---
# 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 launch` project 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`](https://shotops.dev/docs/mcp/reference/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.](https://shotops.dev/studio/docs/media/assets/spine-project.svg)

_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`](https://shotops.dev/docs/mcp/reference/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.

> Pass the real filenames for new or replacement files `name` helps ShotOps match a newly supplied file to the intended project shot and lets a local source reload from the user's screenshots folder. Skip it and the record falls back to `screen-1.png`, `screen-2.png`, which may match nothing later. Once saved, however, a source belongs to a stable shot and device/locale cell — a filename is never the persistent source identity. Use the raw captures, never the rendered panels.

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.

> Careful Never retry a refused name as an omitted `project`. The refusal means the named project was not found; rendering the most recent one instead renders a different project than the one that was asked for, silently and successfully.

## Read before you refine

[`read_project`](https://shotops.dev/docs/mcp/reference/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`](https://shotops.dev/docs/mcp/reference/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`](https://shotops.dev/docs/mcp/reference/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" }]
}
```

> Attachments are refs, not bytes [`refine_project`](https://shotops.dev/docs/mcp/reference/refine_project) takes at most ten refs from [`import_screenshot`](https://shotops.dev/docs/mcp/reference/import_screenshot) or [`request_screenshot_upload`](https://shotops.dev/docs/mcp/reference/request_screenshot_upload). Inline base64, a URL or a local path is not accepted here — import it first, then name the ref. A placed screenshot replaces the one it displaces, which stays recoverable until the project is cleaned up.

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`](https://shotops.dev/docs/mcp/reference/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`](https://shotops.dev/docs/mcp/reference/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_look`](https://shotops.dev/docs/mcp/reference/hold_look) sets it, [`release_look`](https://shotops.dev/docs/mcp/reference/release_look) clears 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](https://shotops.dev/docs/mcp/concepts/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](https://shotops.dev/docs/mcp/workflows/localization) covers what that means when German is late, and [Screenshots and assets](https://shotops.dev/docs/mcp/concepts/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.
