Examples

Each example is a numbered tool-call sequence. Argument objects are the tool args. Every session opens with describe_video and closes with save_version — see Getting started for why.

1. Make the title fade in over the first second

The simplest possible session — three calls.

1. describe_video()
   → the response lists video.main, image.title, image.subtitle, …
   → image.title has no opacity track yet.

2. fade_layer({
     elementId: "image.title",
     fromFrame: 0, toFrame: 30,
     fromOpacity: 0, toOpacity: 1
   })

3. save_version({ name: "add 1s fade-in to title" })

fromFrame: 0, toFrame: 30 is one second at 30 fps. The editor shows the new keyframes the next time the tab reloads — tell the user to refresh.

2. A circle of 30 stars

Bulk work the API exists for. Place 30 copies of an uploaded star.png evenly around a circle, each rotated to face outward.

1. describe_video()
   → confirm star.png is referenced or already uploaded as an asset.

2. For i in 0..29:
   angle  = i * 12                       // 360° / 30
   x      = 540 + 360 * cos(angle°)      // canvas centre is (540, 960)
   y      = 960 + 360 * sin(angle°)
   add_image_layer({
     filename: "star.png",
     x: round(x), y: round(y),
     width: 80, height: 80
   })
   // then, on the id the tool returns:
   move_layer({ elementId: "<new id>", rotation: angle })

3. save_version({ name: "circle of 30 stars" })

Radius 360 keeps every 80px star inside a 1080-wide canvas. add_image_layer returns the new layer's id — feed it straight into move_layer for the rotation. Re-using one filename for all 30 layers is fine; each gets a fresh id.

3. Swap a clip while keeping its keyframes

The user re-rendered the source video and wants the new file in, with every animation on that layer intact.

1. describe_video()
   → video.main has scale and opacity keyframe tracks. Note its id.
   → confirm demo-final.mp4 is uploaded.

2. set_video_clip({ elementId: "video.main", clip: "demo-final.mp4" })

3. save_version({ name: "swap to demo-final.mp4" })

Use set_video_clipnot remove_layer + add_video_layer. Removing and re-adding mints a new id and drops every animation track. set_video_clip keeps the id, position, size, styles, trim window, and all keyframes; only the source mp4 changes. set_image_filename does the same for image layers.

4. Vary a caption across a loop

The user wants one composition that plays three times, showing a different caption tip each pass.

1. describe_video()
   → find the caption text layer, e.g. text.caption.
   → if there's no text layer yet, create one:
     add_text_layer({ text: "First tip", x: 540, y: 1500,
                       font_family: "Anton", text_color: "#FFFFFF" })

2. set_loop({
     elementId: "text.caption",
     field: "text",
     values: ["Batch your filming", "Hook in 3 seconds", "Caption every clip"]
   })

3. save_version({ name: "caption loop — 3 tips" })

set_loop repeats the whole composition once per value, overriding field of elementId each pass. Pass an empty values array later to clear the loop.

5. Clip out a segment of a source video

Keep only source frames 90–300 of a clip, playing from the top of the timeline.

1. describe_video()
   → note video.main and its current trim window.

2. set_video_layer_trim({
     elementId: "video.main",
     source_in_frame: 90,
     source_out_frame: 300,
     timeline_start_frame: 0
   })

3. set_duration({ seconds: 7 })   // 300 - 90 = 210 frames ≈ 7 seconds

4. save_version({ name: "trim to source 90–300" })

set_video_layer_trim patches the trim window — source_in_frame/source_out_frame are positions inside the source mp4; timeline_start_frame is where the slice lands on the project timeline. Setting the composition duration to match keeps the export tight with no frozen tail. To use two disjoint segments of the same clip, duplicate the layer in the editor first, then give each copy its own window.

6. Group the header and slide it in

Wrap the title block in a group and animate the whole block as one unit.

1. describe_video()
   → confirm text.headline, text.subhead, image.logo all sit at the root
     (same parent — required for grouping).

2. group_layers({
     elementIds: ["text.headline", "text.subhead", "image.logo"],
     name: "header"
   })
   → returns the new group, e.g. group.header.

3. add_keyframe({ elementId: "group.header", property: "y",
                   frame: 0,  value: -400, easing: "outBack" })
4. add_keyframe({ elementId: "group.header", property: "y",
                   frame: 20, value: 0 })

5. save_version({ name: "group header + slide-in" })

Group x/y keyframes are translation offsets around the frozen pivot, so y: -400 → 0 slides the whole header down into place. Every child moves together. If any of the three layers had been inside a different group, you'd set_group_parent them to a common parent first — group_layers requires shared parentage.

7. Animate a backdrop colour shift

Fade the canvas backdrop from light to dark over two seconds.

1. describe_video()  → read the current background fill.

2. add_color_keyframe({ elementId: "background.canvas", property: "fill",
                        frame: 0,  value: "#FAFAFC" })
3. add_color_keyframe({ elementId: "background.canvas", property: "fill",
                        frame: 60, value: "#14141B", easing: "easeInOut" })

4. save_version({ name: "backdrop fade to dark" })

The canvas backdrop is addressed as background.canvas. Colour keyframes crossfade stop-by-stop, so this also works between two gradients, not just two solids.