Manifest files
Describe a canvas in a *.takes.yaml file, complete it in your editor, and check it before it is built.
A canvas can be written down. A manifest is a small *.takes.yaml file that
describes one canvas — which nodes exist, which model each one uses, and what
feeds what — so a graph can live in a repository, be reviewed in a pull
request, and be checked before anything is built.
This is how an agent works on your canvases without guessing. It writes the file, your editor completes it against a published schema, and the server tells it whether the graph is legal.
What it looks like
# yaml-language-server: $schema=https://takes.sh/schema/takes-manifest.v1.json
version: 1
project: takes-site
canvas: pricing-hero
nodes:
brief:
type: text
text: "Hero image for the pricing page. Quiet, warm, no people."
hero:
type: image
model: openai/gpt-image-2
settings:
aspectRatio: "16:9"
quality: high
count: 3
inputs: [brief]
outputs:
hero: ./public/hero/
project and canvas are names, not ids. Everything under nodes is a node
you would otherwise have added from the toolbar, and the name you give it —
brief, hero — is that node's identity: how you point other nodes at it, and
how errors refer back to your own file.
What is not in it
A manifest is what you want, not what happened. Runs, generated images and videos, versions, costs and node positions all belong to the canvas and never appear in the file. Neither does media: an asset is named by a path or a library id, never embedded.
That is what makes it safe to commit. The file is a few dozen lines that read as intent, and re-applying it does not duplicate a canvas or discard its history.
Wiring nodes together
inputs is how edges are written. In the short form you list what feeds a
node and the handle is chosen for you, the same way it is when you drag a
connection on the canvas:
inputs: [brief, logo]
Text goes to the prompt input; an image goes to the reference input of an image node, or the start frame of a video node. When a node has several media inputs and you need to say which, name it:
inputs:
prompt: motion-brief
start-frame: product-still
A video node exposes three outputs rather than one, so a ref can name which of them you mean:
inputs:
start-frame: { from: opening-shot, handle: out-end-frame }
Media you already have
assets names the images and videos a canvas starts from. Each one becomes a
source node, and its name can be used anywhere a node name can:
assets:
logo: ./brand/logo.png
mascot: { id: "ast_9f2k", kind: image }
A path is a local file, and its kind is read from the extension — png, jpg,
jpeg, webp, gif and avif are images, mp4, mov, webm and m4v are
videos. Anything else needs kind spelled out. An id is an asset already in
your Stocks library.
Where results go
outputs says which directory a node's media should be written to:
outputs:
hero: ./public/hero/
Any node except a text note can have one.
Checking a manifest
POST /api/manifests/validate checks a manifest against the same rules that
apply when a canvas is actually built. Sign in with a browser session, or use a
personal token from Settings → MCP as a bearer credential. It is free and
it changes nothing: it only looks up whether the project and canvas names
already exist, never reads or changes canvas content, and generates nothing.
curl -s -X POST https://abuck.app/api/manifests/validate \
-H "Authorization: Bearer $TAKES_TOKEN" \
-H 'content-type: application/yaml' \
--data-binary @canvas.takes.yaml
You get one of three answers:
- Valid — the resolved graph, every edge handle filled in, plus whether a project and canvas of those names already exist.
- The file is wrong — every shape and type problem at once, each with the path in the file it came from. Naming problems, such as an input that points at nothing, are reported once the shape is right.
- The graph is wrong — the first rule the graph breaks, with the same message you would have got had you built it, naming nodes by your keys. A model id that does not exist, a setting the model does not offer, or a loop in the graph is caught here rather than after the write.
If the model catalog cannot be reached, the answer is a 502 rather than a
verdict on your file. Try again.
A model's settings are checked against the live catalog, so the values a manifest may use are exactly the values that model offers today.
Planning a manifest
POST /api/manifests/plan tells you what a file would do before it does it:
which nodes it would add, update or leave alone, and what running the result
would cost. Nothing is written — no project, no canvas, no run — so it is the
thing to check before you apply, and the thing to paste into a review.
curl -s -X POST https://abuck.app/api/manifests/plan \
-H "Authorization: Bearer $TAKES_TOKEN" \
-H 'content-type: application/yaml' \
--data-binary @canvas.takes.yaml
The answer lists every generation the canvas would perform — the node it comes from, its model, and its price — and totals them. That total is the same number a run checks against its own spending limit, and it is a ceiling: a run can come in under it, never over it.
If the price cannot be worked out — a node that names no model, no membership, a model account not connected yet, or a model whose provider publishes no rate — you still get the list of changes, and a short note saying which it was.
Applying a manifest
POST /api/manifests/apply turns the file into the canvas. The project and the
canvas are created if they do not exist yet, every node in the file is matched
to the canvas by its name, and everything your runs produced stays where it is.
Applying the same file twice changes nothing; changing one prompt changes one
node and keeps every earlier take.
curl -s -X POST https://abuck.app/api/manifests/apply \
-H "Authorization: Bearer $TAKES_TOKEN" \
-H 'content-type: application/yaml' \
--data-binary @canvas.takes.yaml
The answer says what happened to each node — added, updated, unchanged, or left
as an orphan because the file no longer names it — and gives you the canvas
URL. Orphans stay on the canvas until you apply with ?prune=1, which removes
them and their connections. A node you made in the browser is never touched,
and a file that breaks a rule writes nothing at all.
Two things to know. An asset named by a local path cannot be applied yet:
upload the file in the browser once and reference it by its library id. And a
video node whose model can generate sound gets generateAudio: false written
for it when the file says nothing, so a server-side run can price it; write
generateAudio: true yourself to ask for sound.
Pulling a canvas into a file
POST /api/canvases/:id/manifest goes the other way: it reads a canvas and
hands you the file that describes it, as an object and as the *.takes.yaml
text to save.
curl -s -X POST https://abuck.app/api/canvases/$CANVAS_ID/manifest \
-H "Authorization: Bearer $TAKES_TOKEN"
What comes back is what the canvas is, not what it has done: node types, prompts, models, settings and wiring, plus any Stocks media it uses, by library id. Takes, versions, costs and node positions stay on the canvas.
A node you added from the toolbar has no name a file can use, so pull gives it
one — text-1, image-2 — and writes it onto the node, so a later apply
recognises it instead of building a second one beside it. That is the whole
point: apply the file you just pulled and nothing changes. Add ?adopt=false
to look without writing anything, remembering that the file you get then
describes new nodes rather than the ones you have.
A video node that never said whether it wants sound comes back asking for it, and the canvas is set to match, because that is what Play would have done with it.
Three things do not come back, and the answer names each one and why. An image or video you pasted onto the canvas is not in your Stocks library, so no file can point at it — upload it to Stocks and place it from there. A node type a manifest has no words for yet, such as a camera, is left out. So is a list that never settled on whether it holds text, images or video. A canvas made only of those says so rather than handing you an empty file.
Completion in your editor
The schema is published at
https://takes.sh/schema/takes-manifest.v1.json. The first line of every
manifest points at it:
# yaml-language-server: $schema=https://takes.sh/schema/takes-manifest.v1.json
VS Code, Zed, Neovim and JetBrains editors read that line and will then
complete node types, handle names and setting keys, and underline a field that
does not belong. In JSON, use a $schema key instead. Until the move to
takes.sh is complete, the same file is served from the current host at
https://abuck.app/schema/takes-manifest.v1.json; point the directive there if
your editor reports that the schema cannot be loaded.
What a server-side run can do today
Everything a manifest can describe runs from the API or an AI client: an image
count above one, several image models compared in one node, a fan-out over a
list, speech and sound effects. Two limits remain. Music is refused until its
provider's price is known to the run engine, and a list filled earlier in the
same run cannot also be read in it — run the node that fills the list first,
then the node that reads it.
See MCP setup for connecting a client, and the node reference for what each node type takes in and produces.