# Writing your own recipe When to read this: no catalog recipe makes what the user wants with its inputs alone, or the user wants a reusable design that takes different texts and photos each time. It covers when to write a document, the check and render loop, the document skeleton and common errors. The topic guides below hold the rest of the format. ## When to author a recipe Every request starts the same way: find the recipe nearest to what the user wants (`tweenframe recipes list`). If its inputs give the user's result, render it by id as [recipes.md](/docs/agent/recipes.md) describes. If not (another layout, more or fewer blocks, other logic), take its **recipe document** and change it: `tweenframe recipes show --document > work/recipe.json`. When no catalog recipe is near, start from the closest example below instead. A recipe rendered by code has no document (`recipe_has_no_document`). A recipe document is the format the catalog recipes are written in: a design with named inputs (`params`), scene prototypes in the engine's vocabulary and a short program (`emit`) that turns inputs into clips, or into one still. You send it with every check and render; nothing is stored. A document says what it produces: `"produces": "timeline"` (the default) is motion, rendered as an animated format or as a png/jpeg frame at a `time`; `"produces": "scene"` is a still, printed as png, jpeg, svg or pdf. A vector file for print needs a scene recipe; see [A still: a scene recipe](#a-still-a-scene-recipe). On a site whose formats are stills only (`tweenframe recipes render --help` lists them), start from a scene recipe such as [place-card.json](/docs/agent/examples/place-card.json): a timeline document renders there as one frame, before its entrances. Everything the user will change between renders (texts, colors, photos, lists of items, timings) is an input; the document itself never holds the user's own copy. ## Workflow 1. Save the document you start from as `work/recipe.json`: the nearest catalog recipe's (`tweenframe recipes show --document > work/recipe.json`) or the closest example (see [examples.md](/docs/agent/examples.md); `tweenframe docs example-place-card > work/recipe.json`). 2. Check the document alone: `tweenframe recipes check --document work/recipe.json`. The answer is the recipe's public view, as `recipes show` gives it for a catalog recipe: `input_schema`, `asset_paths`, `inputs_summary`, the default `output` and the `engine` version it needs. Read the schema back: it is what a caller of your recipe sees. 3. Check it with inputs: `tweenframe recipes check --document work/recipe.json --input work/inputs.json`. Only now is the document expanded, so most mistakes in prototypes (a misspelled node field, a binding that does not resolve, an invalid layout) show up here. `timeline` summarizes the result of a timeline recipe (`clips`, `overlays`, `size`); `scene` gives the canvas size of a scene recipe. Check with the fullest inputs and with the smallest ones (optional inputs left out): a node that only exists for some inputs is only checked with them. The answer also lists `warnings`: `missing_glyphs` names a text node (`path`, `input_path`), its `font` and the `chars` that font cannot draw; `layout_overflow` names a frame (`path`, `node_id`) whose children need more room (`required`) than it has (`available`) along `axis`. The render still runs, but those characters come out as empty boxes or nothing, and what overflows may be cut; change the font or the text, let a child fill or shrink, or give the frame more room. 4. Render and look at the result as [output.md](/docs/agent/output.md#render-and-look) describes, with the same `--document` and `--input` as the check. Output options are flags as for any render; the document's `default_output` holds the defaults. 5. Fix what you saw, then check and render again. Never resend an unchanged request after an error. Local images go in as `file:///absolute/path` values: in the inputs at the `asset_paths` the check answers, or anywhere in the document (a logo that is part of the design). The CLI uploads each distinct file once. The catalog's demo photos (`/template/assets/…` paths from any recipe's examples) work in your inputs too, which is handy for a first draft. Errors in the document are `invalid_recipe_document` (HTTP 422): ```json { "error": { "code": "invalid_recipe_document", "message": "Recipe document failed expansion: Extra inputs are not permitted at prototypes.card.payload.scene.children.0.children.0.font_szie", "details": { "path": ["document", "prototypes", "card", "payload", "scene", "children", 0, "children", 0, "font_szie"], "errors": [ { "node_id": "headline", "emit": 0, "path": ["prototypes", "card", "payload", "scene", "children", 0, "children", 0, "font_szie"], "message": "Extra inputs are not permitted", "type": "extra_forbidden" } ] } } } ``` - `details.path` starts with `"document"` and continues inside your document: keys and list indexes, here the `font_szie` key of the first child of the first root node of prototype `card`. It always points into what you wrote (a prototype, a param, an emit step), never into the expanded result. - `details.errors` lists each problem with its own `path`, `message` and `type`; `node_id` names the scene node, `emit` the index of the emit step that was running and `item` the list index when it ran inside a `repeat`. The same mistake repeated for several list items is reported once, with `count`. - An input that breaks your `params` declarations (unknown field, wrong type, out of range) is `invalid_recipe_inputs` with `details.path` into the inputs (`["features", 0, "colour"]`), as for a catalog recipe. A valid input that a document field cannot take (a text param bound to a `duration`) is `invalid_recipe_document`: `details.path` is the document field and each entry of `details.errors` adds the `input_path` of the value. ## Document skeleton A complete document: a headline that shrinks to fit, an optional subline and a button. ```json { "document_type": "recipe", "schema_version": 1, "recipe_id": "my/sale-card", "recipe_version": 1, "title": "Sale card", "description": "A headline, an optional subline and a button on a solid color.", "canvas": { "size": { "width": 800, "aspect": "16:9" }, "fps": 20, "background": "${params.background}" }, "default_output": { "format": "gif", "width": 800 }, "params": { "headline": { "type": "text", "required": true, "example": "Summer sale", "description": "Main line, shrinks to fit the card." }, "subline": { "type": "text", "description": "Optional line under the headline; leave it out to hide it." }, "cta": { "type": "text", "default": "Shop now", "description": "Button label; empty string hides the button." }, "background": { "type": "color", "default": "#14213d" }, "accent": { "type": "color", "default": "#fca311" } }, "prototypes": { "card": { "duration": 3000, "payload": { "kind": "scene", "content_time": "once_hold", "scene": { "size": "${canvas.size}", "children": [ { "type": "frame", "id": "content", "size": [800, 450], "sizing": { "width": "fill", "height": "fill" }, "layout": { "direction": "column", "justify": "center", "align": "start", "gap": 20, "padding": [48, 64, 48, 64] }, "children": [ { "type": "text", "id": "headline", "text": "${params.headline}", "font": "Anton", "fill": "#ffffff", "fit": "box", "max_size": 120, "align": "left", "size": [672, 150], "sizing": { "width": "fill" }, "animation": { "in": { "name": "slide-in", "options": { "dir": "up", "fade": true } } } }, { "type": "text", "id": "subline", "when": "${params.subline}", "text": "${params.subline}", "font": "Roboto", "font_size": 28, "fill": "#e5e5e5", "fit": "width", "align": "left", "size": [672, 40], "sizing": { "width": "fill", "height": "hug" } }, { "type": "frame", "id": "button", "when": "${params.cta}", "size": [10, 10], "sizing": { "width": "hug", "height": "hug" }, "fill": "${params.accent}", "corner_radii": [28, 28, 28, 28], "layout": { "direction": "row", "padding": [14, 32, 14, 32] }, "children": [ { "type": "text", "id": "button-label", "text": "${params.cta}", "font": "Roboto Bold", "font_size": 24, "fill": "#14213d", "fit": "none", "wrap": false, "size": [10, 10], "sizing": { "width": "hug", "height": "hug" } } ] } ] } ] } } } }, "emit": [ { "clip": { "id": "card", "prototype": "card" } } ] } ``` `tweenframe recipes render --document work/recipe.json --input '{"headline": "Summer sale", "subline": "Up to 40% off everything"}' --output work/card.gif` | Field | Meaning | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `document_type`, `schema_version` | Required. Always `"recipe"` and `1`. | | `recipe_id`, `recipe_version` | Required. Your own id (lowercase words joined by `-` or `/`) and a version from 1; a render answers them back in `recipe`. | | `title`, `description`, `tags` | Optional. What the recipe makes, shown in the check answer. | | `produces` | Optional. `"timeline"` (the default): motion, an animated format or a frame at `time`. `"scene"`: a still, printed as png, jpeg, svg or pdf; see [A still: a scene recipe](#a-still-a-scene-recipe). | | `canvas` | Required. `size`: `[width, height]` in pixels, or `{"width": 800, "aspect": "1.91:1"}` (see [canvas.md](/docs/agent/canvas.md)); `fps` (default 24; a scene recipe takes none); `background` (a color, optional). Every value may be a binding. | | `default_output` | Required, with at least `format`. The output options a render uses when it passes none: `format`, `width`, `fps`, `quality`, `loop`. A timeline recipe takes no `svg` or `pdf` here; a scene recipe only png, jpeg, svg or pdf, without `time`. | | `params` | The inputs: name to declaration (see [logic.md](/docs/agent/logic.md)). They become the `input_schema`. Optional, like `types` and `presets` (see [logic.md](/docs/agent/logic.md)). | | `prototypes` | Named clip templates. A clip has a `duration` in milliseconds and a `payload`; a scene payload holds a `scene` with `size` and `children`. | | `emit` | Required, at least one step. The program, run in order: `clip` adds one clip from a prototype, `repeat` adds clips per list item, `join` adds transitions between clips. A scene recipe has exactly one step, `scene`. | Main clips play back to back: the result lasts the sum of their durations, and a transition overlaps the seam without changing that sum. `content_time: "once_hold"` plays the scene's animations once and holds the last state until the clip ends. Give every scene `"size": "${canvas.size}"`; nodes are positioned in canvas pixels from the top-left. `canvas.background` is a color; for a gradient behind everything, set the scene's own `background`: `{"type": "linear", "angle": 90, "stops": [[0, "#1d3557"], [1, "#0b132b"]]}`. ## A still: a scene recipe A sign, a card or lettering for print has no clock: write it as a scene recipe. `"produces": "scene"` at the top, and the one prototype is the scene body itself, with `children` (the same nodes as a clip's scene: frames with auto layout, texts, images, vectors) and no `duration` or `payload`. `canvas` gives the document its `size` and `background` (no `fps`), so the prototype carries neither; `emit` is the single step `{"scene": {"prototype": "card"}}`. The result prints as png, jpeg, svg or pdf; `default_output.format` is one of these and takes no `time`. Entrance animations, loops and media slots have no place in a still: a document with them is a timeline recipe, and a png or jpeg of one is the frame at `time`. `recipes check` with inputs answers `scene: {size}` for it. [place-card.json](/docs/agent/examples/place-card.json) is a complete one; a canvas that follows the lettering (`{"width": "auto", "height": "auto"}`) works as in [canvas.md](/docs/agent/canvas.md). ## Topic guides Read only the topic you need; each one is short. | Guide | CLI | Read it for | | ------------------------------------------ | ------------------------- | --------------------------------------------------------------------------------------- | | [layout.md](/docs/agent/layout.md) | `tweenframe docs layout` | Nodes, frames, rows and columns, hug and fill, absolute children, shadows. | | [text-media.md](/docs/agent/text-media.md) | `tweenframe docs text-media` | Fonts, text fit and style, typography and look inputs, photos and focus, GIF and video. | | [icons.md](/docs/agent/icons.md) | `tweenframe docs icons` | Library icons and decor shapes, your own SVG, shapes drawn with path data. | | [patterns.md](/docs/agent/patterns.md) | `tweenframe docs patterns` | Repeating motifs behind content: dots, ruled and graph paper, grids; fades and insets. | | [logic.md](/docs/agent/logic.md) | `tweenframe docs logic` | Params and types, presets, derived numbers, substitutions, when, repeat. | | [canvas.md](/docs/agent/canvas.md) | `tweenframe docs canvas` | Aspect, a canvas that follows a picture or its content, slides that share one canvas. | | [design.md](/docs/agent/design.md) | `tweenframe docs design` | Making a document reusable; margins, hierarchy, colors, motion and reading time. | | [examples.md](/docs/agent/examples.md) | `tweenframe docs examples` | Complete example documents and what each shows. | Two more for motion: | Guide | CLI | Read it for | | ---------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------- | | [animation.md](/docs/agent/animation.md) | `tweenframe docs animation` | Entrances, exits and loops, staggered starts, keyframes, clip length, scrolling strips. | | [clips.md](/docs/agent/clips.md) | `tweenframe docs clips` | Transitions between slides, region seams, loops, overlays, sections and pages. | ## Common errors Each is `invalid_recipe_document` unless stated; the path is `details.path` without its leading `"document"`. The topic guide for each kind: sizing and layout (`hug_unsupported`, `fill_without_layout_parent`, `hug_fill_cycle`, `duplicate_id`) in [layout.md](/docs/agent/layout.md); undeclared params, item fields, `when` and repeats in [logic.md](/docs/agent/logic.md); fonts in [text-media.md](/docs/agent/text-media.md); library paths and SVG files in [icons.md](/docs/agent/icons.md). | `details.path` and message | Cause and fix | | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `…children, 0, font_szie`: Extra inputs are not permitted | A misspelled or invented field. Use only the fields this guide names; the last path element is the bad key. | | `…children, 0, text`: '${params.title}' reads undeclared param 'title' | A binding names a param that is not in `params`. Declare it or fix the name. | | `…template, children, 1, text`: '${item.txt}' reads 'item.txt', a field the items of 'features' do not declare | A misspelled item field, or one missing from `items.fields`. | | `…children, 1, text`: ${params.subline} does not resolve inside 'Only ${params.subline}' | An optional value inside a longer string. Add `when` on the node, or give the param a default. | | `emit, 0, clip, prototype`: unknown prototype 'cards' | The emit step names a prototype that does not exist. | | `…children, 0, sizing, height`: hug_unsupported … fit='box' shrinks text into a fully resolved box | `hug` on a `fit: "box"` text. Give it a fixed or `fill` height, or use `fit: "width"`. | | `…children, 0, sizing, width`: fill_without_layout_parent | `fill` on a child of a frame without `layout`, or on an absolute child. Add `layout` to the parent or give a fixed size. | | `…children, 2, sizing, height`: hug_fill_cycle … parent hugs its height while this child fills it | A `rect`, or a frame without `layout`, set to `fill` in a parent that hugs that axis. Give it a fixed size, or add `layout` to the frame. | | `…children, 2, children, 0, id`: duplicate_id … id 'headline' is already used | Two nodes of one scene share an id; in repeated children add `${i}` to every id. | | `…children, 1, when`: unsupported when clause | Only a truthy string, `eq`, `in`, `not`, `gt`, `gte`, `lt`, `lte` and a list of these exist; `ne` is `{"not": {"eq": …}}`. | | `unknown_font`, `…children, 0, font`: unknown font 'Antonn Display' | Use a name from `tweenframe fonts list` or an exact Google Fonts family. | | `invalid_recipe_inputs`, `features, 0, colour`: unknown fields | The inputs break your own `input_schema`; fix the input, or the declaration when the input was right. | | `prototypes, card, duration` with `input_path` `secs`: Input should be a valid integer | An input value that the document field it is bound to cannot take. Fix the value, or declare the param with the type the field needs (`number`). | Mistakes in motion documents; the topic guides are [animation.md](/docs/agent/animation.md) (effect params, `max_duration`, marquee cells under [Strips](/docs/agent/animation.md#strips)) and [clips.md](/docs/agent/clips.md) (joins): | `details.path` and message | Cause and fix | | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `…animation, in, params, delay`: effect 'slide-in' has no param 'delay'; params: dist, duration, stagger | A param or option the effect does not have. The message lists the valid names. | | `…animation, in, options, duration`: no effect option 'duration' | A number in `options`. Time and size numbers go in `params`, switches in `options`. | | `prototypes, headline, duration, max_duration`: derived window 7767ms exceeds max_duration 3000ms | The text, hold and effects need a longer clip than `max_duration` allows. Raise `max_duration` or shorten the hold. | | `emit, 2, join`: clips 'slide-1' and 'slide-2' are already joined by an earlier join step | Two join steps cover the same pair. Narrow one with `between` or remove it. | | `prototypes, strip, payload`: marquee cells must share one height | Cells of a strip with different heights. Give every cell scene the same height, or the same `auto_height` rule. | | `emit, 2, join, kind`: transition 'a'->'b': kind 'smart_animate' needs full-canvas scene clips; 'a' is not one | A `smart_animate` seam next to a clip with a `box` or a media clip, or (`scene of 'a' is 200x240, not the canvas …`) a scene of another size. Give both clips full-canvas scenes. | | `emit, 2, join, kind`: transition 'a'->'b': kind 'smart_animate' needs static scenes; the scene of 'a' is animated | A node of either scene has `animation`, `keyframes`, `start`, `end`, a playing image (`play` other than `off`) or `text_source`. The other faults of this kind name `media` nodes, `auto_height`/`auto_width`, two `fit`s, unequal scene backgrounds or radii. Remove the difference, or use another kind. | | `emit, 2, join, region`: transition 'a'->'b': kind 'smart_animate' morphs whole scenes and takes no region | `smart_animate` with a `region`. Remove it; with `options` the error is `option '…' does not apply to kind 'smart_animate'`. |