Common mistakes

These are the recurring ways an agent gets a Metamorpha session wrong. Most trace back to skipping describe_video or misreading a convention.

Inventing element ids

Always pull element ids from describe_video. Don't construct image.title or shapes.0 from intuition — the real id is whatever the project JSON says, often an opaque slug. An invented id makes the tool fail or, worse, silently target nothing. The single most common cause of a lost session is skipping the describe_video call.

Treating (x, y) as top-left

A layer's (x, y) is the centre of its bounding box, not the top-left corner (Premiere / Final Cut / Motion convention). To place a 200×80 label flush against the canvas's top-left, its centre is (100, 40) — half its width and half its height in. Setting (x, y) = (0, 0) parks three-quarters of the layer off-canvas.

Confusing frames and seconds

The timeline is 30 fps and every frame: argument is an integer frame number. A 2-second fade is frames 0..60, not 0..2. Convert with frames = round(seconds × 30). The one exception is set_duration, which takes seconds directly.

One version per tool call

save_version is for logical change-sets, not individual mutations. Versions are user-visible in the editor's Versions panel — thirty versions each named "change" bury the user. Bundle a session's mutations and save one version at the end with a short, descriptive, imperative-mood label.

Animating with move_layer

move_layer sets a layer's static base value. If the property already has a keyframe track, the track overrides that static value at every frame — your move_layer call appears to do nothing. To animate, use add_keyframe. To change an un-animated default, use move_layer. Check describe_video for an existing track before deciding which.

remove_layer on a group

remove_layer deletes leaf layers (video / image / text / shape) and errors on a group. To get rid of a group use ungroup_layers, which dissolves the group but keeps its children alive, spliced into the group's old parent.

Losing keyframes on a swap

To change a layer's image or clip, use set_image_filename / set_video_clip — they keep the layer's id and every animation track. remove_layer followed by add_image_layer mints a new id and drops all the keyframes. Same trap, different tool: ungroup_layers discards the group's own animation tracks (the children survive, but the group's keyframes are gone) — save a version first if the user might want them back.

Mixed-parent grouping

group_layers requires every listed element to currently share the same parent — all at the root, or all inside one existing group. To group elements that live in different parents, first set_group_parent them into a common parent, then group_layers.

set_style fields that don't apply

set_style accepts image-only fields — fit, anchorX, anchorY, tintColor, tintStrength, alphaMask. They land in the JSON on a shape or video but the renderer ignores them there (tintColor/tintStrength are image-only; fit/anchor* are image+video). set_style on a group.<id> is an outright error — groups have no styled body; colour a group with set_group_box + set_layer_fill instead.

Referencing an asset that isn't uploaded

add_image_layer, set_image_filename, add_video_layer, set_video_clip, and add_audio_overlay all reference a filename that must already exist in the project's asset/clip bucket. There is no API tool for uploading a file — uploads happen via the editor's drag-drop or the /api/upload-asset / /api/upload-clip HTTP routes. describe_video lists layers, not the asset bucket, so confirm the upload with the user before referencing a new filename.

Expecting the editor to auto-reload

A tool call writes to storage immediately, but an editor tab already open shows its in-memory copy of the project — it does not auto-refresh. If you mutate a project while the user is watching it in the editor, tell them to reload the tab to see the change. Versions you save appear in the Versions panel on the next reload too.

Emptying the embed allowlist by accident

set_embed_origins replaces the whole allowlist with the array you pass. Passing [] — or removing the last entry with remove_embed_origin — turns embedding off: the public embed endpoint then 404s the project. If you only mean to add or drop one hostname, use add_embed_origin / remove_embed_origin rather than rebuilding the list.