Getting started
This page is the orientation an external agent needs before calling any mutating tool. Read it once; the conventions here apply to every tool.
Browse before mutating
The first call of any session is `describe_video`. It returns a cheap structural overview — canvas meta, the backdrop, and a z-ordered tree of every layer with its id, type, name, label (filename/clip/text/kind), geometry, and which properties are animated. It deliberately omits keyframe values and styles (the unbounded part). It is free and never mutates anything.
You need describe_video because you must not invent element ids or filenames. Read them from the tree. The single most common way an agent gets lost is skipping this step and constructing an id like image.title from intuition — the real id is whatever the overview says it is.
When you need a layer's full detail — keyframe values, colour tracks, track-loop modes, styles, every field — call `inspect_layers([elementId, …])` for just the layers you're about to change. Don't guess values from the overview; don't pull detail for layers you won't touch.
A typical session is small:
describe_video— see the overview tree.inspect_layers([elementId, …])— full detail on the layers you'll change (skip if only adding new layers).- Call one or more mutating tools.
save_versiononce at the end.
With the SDK each step is a callTool(projectId, name, args):
await morpha.callTool(id, "describe_video", {});
await morpha.callTool(id, "inspect_layers", { elementIds: ["text.7f3a2c"] });
await morpha.callTool(id, "add_keyframe", { elementId: "text.7f3a2c", property: "opacity", frame: 0, value: 0 });
await morpha.callTool(id, "save_version", { name: "add fade-in" });
The save-version bracket
Versions are user-visible: the editor's Versions panel lists every saved version, and the user flicks between them to compare states or roll back. An edit you make without saving a version is one the user can't easily revisit.
Wrap a session like this:
save_version(name="baseline before <task>") ← rollback point
… your mutations …
save_version(name="<short description>") ← end-of-task marker
Call save_version once per logical change-set, not once per tool call. A version named "add fade-in for stars" is useful; thirty versions each named "change" are noise. Use a short imperative-mood label — the user sees it verbatim.
Element ids
Every layer is addressed by a prefixed id. Six shapes appear across the catalog:
| Prefix | Element |
|---|---|
video.<id> | A video layer — carries a clip filename, renders its source mp4 into the layer box. |
image.<id> | An image layer — renders an uploaded bitmap. |
text.<id> | A text layer — renders live typeset text (multi-line, auto-fit). |
shapes.<id> | A shape layer — a native vector primitive (rect, ellipse, triangle, star, and ~30 more; see the tool reference). |
group.<id> | A layer group — holds an ordered children[] and composes a transform onto every descendant. |
image.background | The pinned canvas-backdrop sentinel. Exactly one per project; always painted at the back. The legacy alias background.canvas is still accepted. |
<id> is a 6-char lowercase hex token ([a-f0-9]{6}) generated when the layer is created — opaque, not meaningful. It is never derived from the layer's name, filename, or text content. Matches the pattern used by After Effects, Premiere, Final Cut, Motion, Figma, and Illustrator: ids are storage keys; the user-facing handle is the layer's name. Never construct an id by hand; never expect it to look meaningful; always pull ids from describe_video's output.
Groups take a bare id in a few places. ungroup_layers, rename_group, and set_group_parent's parentGroupId argument take the bare id (e.g. "header"). Everywhere else — add_keyframe, move_layer, set_layer_fill, set_group_parent's elementId — use the full group.<id> form.
The layer tree
project.layer_order is the root-level z-ordering only. A group's children live under that group's children[], not in layer_order. The composition is a tree, not a flat list. describe_video surfaces groups[] and a top-first ordering so you don't have to re-derive the structure.
The canvas backdrop (background.canvas) is not in layer_order — it is always painted first, behind everything.
Structure your composition — group, and use native features
A good Morpha project is a tree of grouped units, not a flat list of forty unconnected layers. Two habits keep it that way:
Group related elements — anything you drew as one thing gets one named group. Two cases, both mandatory:
- A composed object: several primitives that together depict a single thing — a butterfly (wings + body + antennae + spots), a rocket, a tree, an icon, a crest. If a human would point at it and name it with one noun, it's one group named that noun. This is the case most often missed, and it leaves the user with a dozen loose shapes where they expected
butterfly. - A layout unit: a card with its heading, body, and badge; a lower-third; a logo lockup; a labelled diagram.
Wrap each with group_layers as soon as its pieces are placed (the elements must already be siblings; set_group_parent them into a common parent first if they aren't) — not after the whole scene is built and forty siblings sit at the root. Name the group what the thing is; that name is the user's only handle on it, and three objects means three named groups, not one bag of shapes. Grouping keeps the layer list legible and sets up the animation win below. One carve-out: many copies of a single element (confetti, a starfield, a grid built with duplicate_layer) are fine left flat — that's one repeated element, not one composed thing — and ungroup_layers discards the group's own animation tracks, so group the finished thing, not speculatively.
Animate the group, not each child, for a unison move. A group's transform (x/y/scale/rotation/opacity) composes onto every descendant, so a single track on group.<id> gives every child the move, scale, spin, or fade for free — a card that slides in, a scene that pulses, a lockup that spins. That's fewer calls than keyframing each child, and the user can re-time the whole unit from one track. Reach for the per-child batch tools (apply_preset_stagger, set_keyframes_batch) only when the children animate independently — a staggered one-per-frame cascade, a rippling grid.
Use the native feature instead of faking it. Morpha has real primitives for the things agents most often reconstruct by stacking layers:
- A border / frame is a stroke on the shape itself —
set_styleborderWidth+borderColor(+borderRadius). Never a slightly-larger rectangle stacked behind the shape. - A button, CTA, chip, tag, pill, or labelled badge is ONE text layer, not a shape with a text layer on top of it:
set_text_backgroundgives it the fill, thepaddingthat sizes the box around the label, thecornerRadius, and an optionalstrokeWidth+strokeColoroutline. New text layers are alreadytext_autofit: "hug", so padding alone shrink-wraps the box to the words. Padding is what makes a button a button; a rect stacked under a text layer has none, so the two drift apart the moment the label changes or the layer scales, and the user has to move both. (One exception: the box isn't painted on curved text, so an arc-shaped chip really is a shape plus a text layer.) - A gradient is one
linear/radialFill viaset_layer_fill. Never a stack of solid bands. - A mask / crop — clip a layer to a shape, or reveal video through letterforms — is a track matte (
set_matte_source: the host shows only where the source is opaque). Never opaque shapes stacked over the parts you want hidden.
Faked versions double the layer count and drift apart the moment the composition scales or animates. The native version is one layer that stays correct.
The coordinate system
The canvas defaults to 1080 × 1920 (9:16) and can be resized per project — 1080×1080 square, 1920×1080 landscape, 1080×1350 (4:5), or a custom size.
A layer's (x, y) is the CENTRE of its bounding box in canvas pixels — not the top-left. This matches Premiere / Final Cut / Motion. Consequences:
- To centre a layer on a 1080×1920 canvas,
(x, y) = (540, 960). - To place a 200×80 label flush against the top-left corner, its centre is
(100, 40)— half its width and half its height in. (x, y) = (0, 0)puts the layer centre at the canvas corner, so three-quarters of the layer ends up off-canvas.
width and height are pixel dimensions of the bounding box. rotation is in degrees, clockwise.
Frames vs seconds
The timeline runs at 30 fps. Every frame: argument is an integer, 0-indexed frame number. Convert with frames = round(seconds × 30):
- A 1-second fade is frames
0..30. - A 2.5-second hold is frames
0..75. - "Two seconds in" is frame
60.
The composition length isn't something you set — it's derived from content, auto-fitting the furthest keyframe / video-window / audio end (1-second floor).
Animation tracks
Each video / image / text / shapes / group layer carries its animation tracks on its own animations field, keyed by property: x, y, width, height, scale, rotation, opacity. Each property's track is a sorted array of { frame, value, easing? } keyframes. (A layer's per-element data — animations, style, color_tracks, track_loops — lives on the layer record itself; inspect_layers returns all four alongside the layer's fields.)
The key rule, After Effects / Premiere / Final Cut style: when a track exists on a property, it overrides the layer's static value at every frame. So move_layer sets the un-animated default; if the layer has an x track, the track wins. To animate, use add_keyframe. To set a value that isn't animated, use move_layer.
Track values are absolute for leaf layers: x/y are the layer centre's canvas-space pixel position, width/height are pixel dimensions, rotation is degrees, scale orbits the layer centre (1 = no change), opacity is 0..1.
Groups have no static body — their x/y track values are translation offsets applied around the group's frozen pivot, and a group transform composes onto every descendant. A group rotating 30° rotates everything inside it 30° on top of each child's own rotation.
Extrapolation past the ends
A separate per-property setting controls what happens before the first keyframe and after the last. Set it with set_track_loop:
hold(default) — keep the boundary keyframe's value forever.loop— wrap past the last keyframe back to the first; the animation restarts.ping-pong— alternate direction each cycle, bouncing back and forth.cycle— wrap likeloop, but each cycle adds the boundary delta (endless rotation or scrolling).
Tracks with fewer than two keyframes ignore the loop mode.
Assets must exist first
add_image_layer, set_image_filename, add_video_layer, set_video_clip, and add_audio_overlay all reference a filename that must already be uploaded. If you reference a filename that isn't uploaded, the tool fails. describe_video only lists layers, not the asset bucket — confirm the upload before referencing a new filename.
Video clips can be uploaded over MCP: upload_clip(url) fetches a direct http(s) video link server-side, or upload_clip_presign + upload_clip_finalize stream a local/large file straight to R2 (see the tool catalog). Both return the stored filename to pass to add_video_layer. Images upload over MCP too — upload_image(url) fetches a direct image link (.png/.jpg/.jpeg/.gif/.webp/.svg; SVGs carrying scripts or external references are rejected), or find_public_image(query) pulls an openly-licensed image. Audio uploads over MCP too — upload_audio(url) fetches a direct audio link (.mp3/.m4a/.wav/.ogg/.aac) and returns the stored filename for add_audio_overlay or update_audio_overlay (or drag-drop in the editor / POST /api/upload-asset/<projectId> with raw bytes + an X-Filename header).