Agent skill

Timeline Edit Ops

by nodetool-ai in nodetool-ai/nodetool

Reference for editing a NodeTool timeline that already exists with edittimeline ops — animateclip roles and presets, the easing grammar, per-word/character stagger, setclipparams limits…

AGPL-3.0Auto-check passedMedia & Creative

Install Timeline Edit Ops

skills CLI
$ npx skills add nodetool-ai/nodetool --skill timeline-edit-ops -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install nodetool-ai/nodetool timeline-edit-ops --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/nodetool-ai/nodetool.git skills-src && mkdir -p .claude/skills && cp -r skills-src/packages/system-skills/timeline-edit-ops .claude/skills/timeline-edit-ops && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
timeline-edit-ops
GitHub stars
560
Token cost
~11k tokens
SKILL.md length
5,923 words
Files
1
Skills in repo
127
Repo updated
First seen
Licence
AGPL-3.0

At a glance

Reference for editing a NodeTool timeline that already exists with edittimeline ops — animateclip roles and presets, the easing grammar, per-word/character stagger, setclipparams limits…

  • A few changes to a cut that already exists — retiming a clip
  • SKILL.md covers Read before you edit, Track layering, Build the elements and Animating a clip, plus 11 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md
  • Swapping a preset

What it does

Timeline Edit Ops is an agent skill from nodetool-ai/nodetool. Reference for editing a NodeTool timeline that already exists with edittimeline ops — animateclip roles and presets, the easing grammar, per-word/character stagger, setclipparams limits, document-only fields (styleTracks, textAnimator, animationLinks, layout, camera2d, motionBlur, repeater), groups, transitions, masks and mattes, clip effects, time remap, compositions, beat-cutting ops, and validatetimeline's/previewtimelineframe's full mechanics. Use for a few changes to a cut that already exists — retiming a…

Its SKILL.md is about 11k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Media & Creative, covering Motion graphics. The repository describes itself as: Agent-first Creative Workspace. The licence is AGPL-3.0.

When your agent uses it

  • A few changes to a cut that already exists — retiming a clip
  • Swapping a preset
  • Nudging a stagger

Example prompts

  • “s/previewtimelineframe”
  • “/timeline-edit-ops”

What it can do on your machine

Read from SKILL.md and the folder at commit 58765d3. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are json).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Timeline Edit Ops loads about 11k tokens when it runs. Until then it costs about 178 tokens; SKILL.md has 5,923 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~178
When it runs · the whole SKILL.md, loaded when a task matches
~11k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from nodetool-ai/nodetool at commit 58765d3, republished under its AGPL-3.0 licence (© nodetool-ai). 5,923 words, ~10,566 tokens.

Download SKILL.mdSave it as .claude/skills/timeline-edit-ops/SKILL.md (or your agent's skills folder).
name
timeline-edit-ops
description
Reference for editing a NodeTool timeline that already exists with edit_timeline ops — animate_clip roles and presets, the easing grammar, per-word/character stagger, set_clip_params limits, document-only fields (styleTracks, textAnimator, animationLinks, layout, camera2d, motionBlur, repeater), groups, transitions, masks and mattes, clip effects, time remap, compositions, beat-cutting ops, and validate_timeline's/preview_timeline_frame's full mechanics. Use for a few changes to a cut that already exists — retiming a clip, swapping a preset, nudging a stagger. For a new cut, or a scene with many clips, build it with the @nodetool-ai/sandbox-timeline pack instead; see motion-graphics.
featured
true

Timeline edit ops → the edit_timeline reference

This is the op-by-op contract for changing a timeline document that already exists. motion-graphics owns the build loop and picks the surface: a new cut goes through the @nodetool-ai/sandbox-timeline pack, which handles tracks, groups, layout and most of what follows here for you. Reach for this skill once that document exists and needs a few changes.

Ops are the right tool when the timeline has no authoring code (code.get answers code: null), or for a deliberate hand tweak on one that does. On a code-backed timeline, an op inside a scene the code tracks marks that scene "edited": the next code.set/code.edit/code.rebake (api-timelines) keeps your edit and reports it in conflicts rather than overwriting it, unless force says otherwise.

Read before you edit

Call get_timeline first. You need the sequence's fps, width, height, the track list with each track's index and type, and every clip's startMs, durationMs, mediaType and existing animations. Edit nothing until you have clip ids from the document — never a guessed one.

Snapshot with create_timeline_version before your first edit. Motion work is iterative and a wrong mode: "replace" wipes animations somebody wrote.

edit_timeline takes a timeline ID and an ops array, run in order, up to 60 per call. Inspect each op's ok and the batch's failed result before a dependent edit. In an in-product agent, use the matching ui_timeline_* tool for an individual op:

json
{"timeline_id":"<id>","ops":[{"op":"animate_clip","target":"Title","animations":[{"role":"in","preset":"fade","durationMs":400}]}]}

The animate_clip input accepts catalog presets or custom curves and code, plus stagger and a typewriter caret. It does not author styleTracks, textAnimator, or beat. set_clip_params does not author layout, repeater, motionBlur, steppedTime, temporalEcho, animationLinks, or transform.depthPx. Adding those keys to an op does not write those features. For those fields, read get_timeline's timeline object, change the needed clip or sequence fields, and call set_timeline_document with a complete document assembled from it. Preserve tracks, clips, markers, tempo, setup, templateId, mediaTracks, transcript, scriptEnabled, and camera2d when present. Include timeline.updatedAt as expected_updated_at so a concurrent edit is refused. A whole-document write validates the result and snapshots the prior version. Omitted document fields are removed, so never send only the changed clip. The JSON objects in the document sections below are fields to merge into that complete document, not edit_timeline ops. The editor may lack controls for them even though they render and survive a save.

Track layering

Lowest index renders on top. Track 0 covers track 1, which covers track 2 — Premiere, Resolve and FCP order, and the opposite of a paint stack. The compositor turns the index into z = 1000 - index; preview_timeline_frame reports that z_index per layer and lists layers top of the stack first.

Four track types, and the type is a role, not a restriction on what draws:

TypeHoldsUse for
videopicture clips, imagesthe cut itself
overlaytext and shape clipstitles, lower-thirds, scrims, callouts
audiosoundmusic, voiceover, effects — never drawn
subtitletextburned-in captions

Rules that follow from the ordering:

  • A scrim goes on a higher index than the text it sits behind, and a lower index than the picture it darkens — so picture, then scrim, then text, in descending index order.
  • Give each motion element its own overlay track when their timings overlap. Two animated clips on one track that overlap in time trigger an automatic crossfade you did not ask for (see Transitions).
  • Captions are the exception you do not control: word-level captions from a voiceover clip composite above every real track regardless of which track that clip sits on. Do not build a title that expects to cover them.
  • An audio clip never contributes a layer. A frame that looks empty because everything is on audio tracks is a track-type mistake.
  • At most 8 video layers composite at once, resolved top track first. Text, shape and image clips are not counted, so a stack of titles is unaffected — but nine overlapping video clips means the bottom one silently does not draw. A preview frame is how you find that out.

Build the elements

The clips you animate are made with the same edit_timeline op list.

json
[
  {"op":"add_text_clip","text":"Maya Chen","trackId":"<overlay>","startMs":4000,"durationMs":3000,"fontSizePx":64},
  {"op":"add_shape_clip","shape":{"kind":"rect","x":0,"y":0.72,"width":1,"height":0.18,"fill":"#000000"},"startMs":4000,"durationMs":3000,"opacity":0.55}
]

add_text_clip needs only text; the style fields (fontSizePx, color, background, stroke) are read from the op itself or from a style bag, and with no trackId it lands on an overlay track. add_shape_clip takes its geometry in shape (or shapeStyle, or the op's own keys) with x, y, width and height as 0..1 fractions of the frame — no geometry at all is a full-frame rect, which is the scrim you usually want.

Tracks are the z-order, so how they are made matters:

json
[
  {"op":"add_track","type":"overlay","name":"Titles"},
  {"op":"move_track","target":"Picture","toIndex":2}
]

add_track takes type (video, audio, overlay, subtitle) and an optional name, and appends to the bottom — the highest index, which draws last. A picture track added after the overlays therefore covers them. Add the overlays last, or move the picture down afterwards: move_track takes a target and one of toIndex (0 is the top), before or after.

Animating a clip

edit_timeline with {"op": "animate_clip", "target": <clip id or name>, "animations": [...]}. Each animation is {role, preset, durationMs?, delayMs?, easing?, params?}. mode is "replace" (default) or "add".

Four roles. A clip may carry several, one of each kind or more:

  • in — entrance. delayMs offsets from the clip's start.
  • out — exit. delayMs offsets backwards from the clip's end, so 0 means it finishes exactly as the clip ends. This is the one people get wrong.
  • emphasis — a beat mid-clip. delayMs from the clip's start.
  • loop — runs continuously; durationMs is one cycle, not the total.
The presets

in / out — fade (500ms), slide (500ms; direction left/right/up/down, distance 0–1 of the frame, default 0.3), pop (500ms, easeOut; overshoot 1–1.5, default 1.08), spin (500ms; turns 0–2, default 0.25), wipe (500ms; direction, softness 0–0.5, default 0.05), blur (500ms; amount 0–40, default 12), colorFade (600ms, grayscale blooming into color).

in on text only — typewriter reveals whole characters in reading order. Set durationMs to the time for the entire text to appear. Without it, the characters type 65ms apart and finish within the clip. To set a fixed speed, use durationMs: 1 with stagger: {unit: "character", offsetMs: 40}. animate_clip stores every typewriter in that form. A document you write by hand must use it too: a stored plain durationMs with no stagger shows the whole text at once, with no caret.

emphasis — pulse (600ms; intensity 0–0.5, default 0.06), flash (400ms; intensity 0–1, default 0.6), shake (600ms; seeded noise on both axes: intensity 0–0.2 default 0.02, frequency Hz default 8, seed default 1, fadeInMs/fadeOutMs default 0; the same seed repeats the same motion), bounce (600ms; height 0–0.3, default 0.05), squash (500ms, easeOutBack; amount 0–0.5, default 0.12), followPath (also loop; d an SVG path in 0..1 space plus pathX/pathY/pathWidth/pathHeight, orient turns the clip along the tangent, startT/endT pick a stretch of the path; writes positionX/positionY, so it replaces the clip's position).

loop — kenBurns (3000ms; zoom 0–1 default 0.12, direction in/out, driftX/driftY −0.2–0.2), float (3000ms; amplitude 0–0.2, frequency cycles per period default 1, seed 0 = pure sine, else drift), breathe (3000ms; intensity 0–0.3), rotate (3000ms; direction cw/ccw), hueShift (3000ms; direction forward/reverse), orbit (3000ms; degrees default 360, direction cw/ccw) — which sweeps a 3D clip's camera and does nothing on any other clip.

Named easing includes linear, easeIn, easeOut, easeInOut, easeOutBack, easeOutElastic, easeOutBounce, exponential, quintic, and circular variants (easeOutExpo, easeInOutQuint, easeOutCirc), and hold for a segment that keeps its prior keyframe value until the segment ends. Unset means the preset's own default, and failing that the role's — in gets easeOut, out gets easeIn. That default is almost always right: an entrance decelerates into place, an exit accelerates away. Reach for easeOutBack or easeOutElastic only when the piece is playful; they read as cheap on anything corporate.

{"op": "list_animation_presets"} reports the exact param list when this summary is not enough — the catalog is the authority. It is an edit_timeline op like every other call here, not a tool of its own.

Keyframes and the easing grammar

easing takes more than the seven named ids. cubic-bezier(x1,y1,x2,y2) is the CSS curve: cubic-bezier(0.16,1,0.3,1) is the deceleration most entrances want. spring(stiffness,damping,mass) solves a real spring, so spring(180,12,1) settles with one small overshoot and spring(180,26,1) does not overshoot at all. All three constants must be positive. Anything the parser cannot read eases linearly and reports unknown_easing.

The presets are where motion starts, not where it stops. A custom animation is one extra field and it reaches shapes no preset has: an entrance that overshoots twice, a hold in the middle of a move, two channels on different schedules, a decay, an arc, a path draw-on. Reach for one as soon as a preset is close but not right — bending durationMs and easing around a preset that was never the shape you wanted takes longer and lands flatter. motion-curves carries the channel table and the worked recipes. Then {"role": "in", "preset": "custom", ...} carrying exactly one of

  • curves — [{property, keyframes: [{t, value, easing?}]}], with t running 0..1 across the animation's window. The list_animation_presets op reports the animatable properties.
  • code — a JavaScript body baked into curves once, host-side, at author time. Nothing evaluates at render.

Add mask when a curve drives wipeProgress; without it the wipe has no edge to run against.

Motion that has to follow a piece of audio is a different call: bake_audio_animation measures an audio clip and writes the curve onto a target clip through the set_baked_animation op, either following the loudness (mode: "envelope") or pulsing on each onset (mode: "beats"). Its keyframes sit in the target clip's own media, so trimming or splitting that clip re-slices the motion, and the animation carries bakedFrom — running it again with different settings replaces that curve instead of stacking a second one, and never touches a curve you keyframed by hand. set_markers_from_beats is the other tool: markers to cut against, rather than motion to animate with.

How several animations on one channel combine decides whether the second one adds to the first or throws it away:

FoldChannelsResult
addoffsetX, offsetY, rotation, blur, brightness, hue, temperature, tintValues sum, so a shake and a slide both move the clip
multiplyscale, scaleX, scaleY, opacity, saturation, contrastValues multiply, so two half-opacity ramps give a quarter
minwipeProgressThe tightest wipe wins
replacepositionX, positionY, anchorX, anchorY, trimStart, trimEndOne value survives — the last animation in document order. The other is discarded

The replace row is the trap. Two animations driving positionX over the same instants means one of them does nothing, and validate_timeline reports replace_curves_overlap. Move a clip with offsetX when you want it to compose with other motion, and with positionX only when you mean an absolute placement nothing else touches.

Durations the contract constrains

motion-principles holds the numbers — how long an entrance runs, how long a title holds. What this contract adds is what the document refuses:

  • in + out together must fit inside the clip. A window that does not fit after its delayMs is clamped or never runs, and validate_timeline says so as animation_exceeds_clip.
  • A loop on a clip shorter than one cycle shows a fragment of the motion. kenBurns at its 3000ms default on a 2000ms clip is a slow drift that stops halfway, not a Ken Burns move.
Choosing motion that matches the cut

Pick the entrance from the edit, not from variety. A title over a locked-off shot can slide; a title over a whip pan or a moving camera should fade or blur, because sliding text plus moving picture reads as two unrelated motions. Match the direction to the picture: text sliding against the camera move fights it.

One motion idea per moment. A clip with pop in, shake emphasis and spin out is not three times as expressive.

Animating typography

Text clips take every preset above, and one thing media clips do not: per-word stagger. Add stagger to an animation on a text clip:

json
{"op": "animate_clip", "target": "Title", "animations": [
  {"role": "in", "preset": "pop", "durationMs": 400,
   "stagger": {"unit": "word", "offsetMs": 80, "from": "start"}}
]}

Each unit runs the full animation for durationMs, offset offsetMs from the previous. from picks which unit leads: start (default), end, or center. A stagger needs at least two units — below that it compiles as an ordinary block animation.

unit is one of three, and the count is what the span math is built on:

unitOne unit isCounted as
worda whitespace-separated wordthe words in the clip's text
charactera grapheme cluster — an emoji, or a letter with its combining marks, counts onceevery cluster including the spaces between words, which are timed and draw nothing
linea wrapped linethe lines the text wraps to at the sequence's width and the clip's fontSizePx, not the newlines you typed

The span is durationMs + offsetMs × (units − 1), halved for from: "center" (the middle unit leads and both edges run last, so the largest delay is half the span). Six words at offsetMs: 200 with a 500ms preset span 1500ms. The same line at unit: "character" is roughly thirty units, so the same offset spans six seconds — character staggers want offsets an order of magnitude smaller than word staggers, and a line stagger changes count when the frame does.

A span that does not fit the clip is silently compressed, not cut off. The engine shrinks the per-step offset — never the per-word duration — so the last word still completes inside the clip. Ask for 200ms of offset on a clip with room for 60ms and you get 60ms: every word still animates, the motion is simply tighter and more simultaneous than you wrote. validate_timeline reports stagger_compressed. This is the most common reason staggered type "doesn't look staggered". Work out the span, compare it against durationMs minus any delayMs, and if it does not fit, lengthen the clip or lower the offset yourself rather than letting the clamp choose for you.

loop staggers are the exception: the offset is a phase shift, so it is neither stretched nor clamped.

Which offset reads as texture and which reads as one word at a time is in motion-principles, with the line-length limit that goes with it.

For several separate clips, stagger_animations shifts existing animation delays in the order of clip_ids without moving the clips. Give at least two distinct clip IDs, each already animated, and a non-negative offset_ms. The in-product tool is ui_timeline_stagger_animations with the same fields.

json
{"op":"stagger_animations","clip_ids":["logo-id","title-id","tagline-id"],"offset_ms":80}

Stagger applies only to text clips and only to transform and opacity motion. wipe, blur and colorFade stay block-level even with a stagger set — the mask and effect curves are per-layer, not per-word. A stagger on a media clip is ignored, not an error, so a "why isn't it staggering" bug is usually a clip that is not mediaType: "text".

Type sizes are authored against the sequence resolution: fontSizePx on a 1920×1080 sequence means the same thing at any preview width.

Document motion fields

The fields below belong to clips in the complete timeline document. Merge them into the clip read by get_timeline, retaining its other fields and animations, then write the full document with set_timeline_document. edit_timeline and ui_timeline_animate_clip do not accept them as animation inputs.

styleTracks animate visual properties in the same animation window as a preset. Each stored animation needs id, role, preset, and durationMs. Targets include text.color, shape.fill, shape.cornerRadius, clip.borderRadius, mask.featherPx, effect.<effect-id>.<numeric-field>, supported effect colors such as effect.bg-field.colorB, gradient stops, and compatible shape.d or mask.d paths. The clip must already have the targeted style, mask, or effect. A per-glyph target such as glyph.color also needs text stagger.

json
{"animations":[{"id":"title-color","role":"in","preset":"fade","durationMs":600,"styleTracks":[{"target":"text.color","keyframes":[{"t":0,"value":"#8bd5ff"},{"t":1,"value":"#ffffff","easing":"easeOutExpo"}]}]}]}

textAnimator changes text content over its animation window. ticker supports from, to, decimals, padTo, prefix, and suffix. Set groupSeparator to "," for grouped thousands such as 2,847; without it the output stays ungrouped. scramble supports charset and deterministic seed. It needs a text clip. An animation's beat binds its start to a one-based beat index at the live sequence tempo; scope is clip or sequence, and offsetMs moves it around that beat. delayMs still applies on top.

json
{"id":"count-up","role":"emphasis","preset":"custom","durationMs":900,"beat":{"index":3,"scope":"sequence","offsetMs":-20},"textAnimator":{"kind":"ticker","from":1,"to":7,"padTo":2}}

animationLinks copy a source clip's authored positionX, positionY, scale, rotation, or opacity channel, with optional time offset, loop, scale, and offset. A wiggle link supplies seeded motion without a render-time script. Links do not follow the source's own links. Resolve each sourceClipId against the document's clip IDs before writing it. An opacity link replaces the target clip's own opacity while its group and transition coverage still multiply the result.

json
{"animationLinks":[{"target":"positionX","sourceClipId":"leader-id","source":"positionX","scale":0.35},{"target":"rotation","kind":"wiggle","amplitude":0.025,"frequencyHz":1.4,"seed":8}]}

layout turns a group clip into a flex container for its real children (their parentId names the group): flexDirection: "row" | "column", gap, justifyContent, alignItems, padding. A child's own sizing is flexItem (grow, width, position: "absolute" + inset to fill the container). repeater.count includes the original clip and is capped at 128. timeStepMs offsets each copy in time. These are fields on a clip, not new clip records to add by hand.

Do not scroll a still plate by tiling it with a repeater. Each copy meets the next at a seam, and the seam shows when the image differs at its edges. A generated rain plate is denser at the top, so it always does. To make rain fall, move one plate to a random offset on hold keyframes every two frames. This reads as falling rain and has no seam.

json
{"layout":{"display":"flex","flexDirection":"row","gap":24},"repeater":{"count":3,"positionStep":{"x":180,"y":0},"timeStepMs":67}}

(logo-id/wordmark-id are separate clips with parentId naming this group.)

camera2d is a sequence document field. It needs position, depthPx, and positive focalLengthPx. Keyframes use absolute timeMs and include position and depth. Add depthPx to a clip's existing transform for perspective; focusDepthPx and aperturePx add depth-of-field blur. camera2d: null clears the camera.

For a tilted wall of cards, use a parent group's rotation, rotationX, and perspective for the shared tilt, each card's depthPx and scroll animation for the separate planes, and camera2d for the push and depth blur. Keep a shape's normalized geometry inside the frame-sized source raster. Move the clip or group with transform.position or custom offsetX/offsetY curves to scroll or fly it across the frame; geometry outside that raster is clipped before the transform places it.

json
{"camera2d":{"position":{"x":0,"y":0},"depthPx":0,"focalLengthPx":1400,"keyframes":[{"timeMs":0,"position":{"x":0,"y":0},"depthPx":0},{"timeMs":1200,"position":{"x":90,"y":-20},"depthPx":120}]}}

motionBlur is per clip: samplesPerFrame is 1–32 and shutterAngle is 0–360 degrees. One sample disables blur for that clip. A higher count sets a minimum; the scene renders at its highest requested count, with evenly weighted samples across each layer's own shutter window. steppedTime.fps quantizes a clip's source and animation clock. temporalEcho adds delayed copies through copies, intervalMs, and opacityDecay.

json
{"motionBlur":{"samplesPerFrame":8,"shutterAngle":180},"steppedTime":{"fps":12},"temporalEcho":{"copies":3,"intervalMs":80,"opacityDecay":0.5}}

Groups

{"op":"add_group","name":"Lower third","startMs":4000,"durationMs":4000,"children":["name-id","role-id"]} makes a clip with no media whose transform, opacity and window every clip naming it inherits. {"op": "set_parent", "target": "Name plate", "parentId": "<group id>"} adopts a clip that already exists; "parentId": null releases it, and a cycle is refused.

Rig a lower third this way: scrim, name, role, each on its own overlay track, all three parented to one group. Then animate the group. One slide in and one fade out on the group moves the assembly together and keeps the parts locked to each other, where animating them separately is three chances to drift.

Children keep their own tracks, so grouping does not change what covers what. A plain group's opacity multiplies into each child. A group with an effect, non-normal blend mode, or transition precomposes its children into one surface first. The effect or transition then runs once on the assembled picture. Put a glow on the group for the whole look, on a child for that child alone.

parent_missing, parent_not_group and parent_cycle all mean the child renders unparented, losing the group's transform, opacity and window.

Transitions

A transition is between clips, not on one clip. It is authored on the incoming clip and resolved for both: the compositor finds the clip beneath it on the same track and gives that one the complementary half of the cut. The incoming clip may be a group: set_transition on the group transitions its whole precomposed assembly, with children still parented on their own tracks. Its outgoing partner is the previous overlapping sibling on the group's track under the same parent.

Two clips on the same track whose times overlap dissolve across the overlap with no tool call at all. The corollary: an accidental overlap is an accidental dissolve. When two clips look soft where you wanted a hard cut, check their startMs and durationMs before looking anywhere else.

set_transition picks a cut instead of taking the default: {"op": "set_transition", "target": "Shot 2", "transition": {"type": "wipe", "durationMs": 500, "direction": "left", "softness": 0.1}}. Pass "transition": null to clear it. The cut plays over the target's head, so overlap the two clips by at least durationMs or there is nothing beneath for it to play against — a transition with nothing under it reads as a fade from black.

TypeWhat movesReach for it when
crossfadeIncoming fades upTwo shots of the same scene; the safe default
dipToColorBoth fade through a solidA scene change, a chapter break. color is yours to pick
wipeFeathered edge reveals the incomingA graphic or split-screen feel. softness 0 is a hard edge
pushBoth clips travel one frame widthLateral energy; the two read as one moving picture
slideOnly the incoming travelsThe new shot arrives over a shot that holds
zoomOutgoing grows, incoming comes in from 0.8A push into the next beat
whip, zoomBlurMotion blur across a directional or zooming cutFast camera energy
glitchDisplaced digital cutA deliberate electronic break
gradientWipe, irisSpatial threshold or aperture revealDesigned graphic transitions
lightLeakLocalized color field across the cutA flare-like bridge

direction on wipe, push, slide, whip, and gradientWipe names the incoming edge. gradientWipe also takes map: "linear" | "radial" | "noise", scale, seed, and softness. iris takes softness; lightLeak takes color, scale, and seed. easing takes a named id, cubic-bezier(...), or spring(...).

fadeInMs and fadeOutMs on set_clip_params are audio fades. The compositor does not read them, so they will not fade a picture — for that, animate the clip with a fade in or out.

A transition's opacity multiplies with animation opacity rather than replacing it, so a clip that both dissolves in and carries a fade in ramps twice and reads slower and softer than either alone. Pick one.

validate_timeline reports transition_exceeds_duration when the cut is longer than the clip carrying it, and unknown_transition for a type or direction this build cannot draw — which falls back to a cross-fade running left rather than failing the render.

Show full SKILL.md (2,246 more words)Show less

Masks and mattes

{"op": "set_mask", "target": "Title", "mask": {"kind": "rect", "y": 0.2, "height": 0.6, "featherPx": 24}} cuts the layer to a shape in the clip's own 0..1 space. kind is rect, ellipse or path (with an SVG d). featherPx softens the edge, invert keeps the outside instead, and "mask": null clears it. A path that cannot rasterize reports mask_path_invalid and the layer draws unmasked — the whole picture shows through, the opposite of what you asked for.

{"op": "set_matte", "target": "Fill", "matte": {"source": "Text shape", "mode": "alpha"}} uses another clip as this one's transparency. alpha reads the source's opacity, luma its brightness, and invert flips it. The source clip stops drawing itself. That is the point, and it is also why a matte pointed at a clip you still wanted on screen reads as a deletion. Reach for it when text has to be filled with moving footage: footage on the fill clip, text on the source. matte_source_missing is an error, and the layer then draws unmatted, showing everything the matte was hiding.

Clip effects

set_effects replaces the whole chain and applies it in the order given; an empty list clears it. The tool also accepts pixelate, posterize, directionalBlur, lensDistortion, stylize, generator, lut, and grain alongside color, blur, glow, dropShadow, vignette, sharpen, chromaKey, curves, levels, and liftGammaGain. Its flat input uses {type, ...typeFields}. It assigns effect IDs and enables each effect; it does not accept stored id or enabled fields. validate_timeline reports unknown_effect for an unrecognized type. The Canvas 2D preview applies all known effects through CPU passes when its host supplies scratch surfaces. preview_timeline_frame.effects_not_applied lists unknown types; degraded reports missing surfaces and other draw limitations.

json
{"op":"set_effects","target":"Shot 2","effects":[{"type":"grain","amount":0.12,"size":1.1},{"type":"stylize","mode":"dither","amount":1,"seed":4}]}

Time remap

{"op": "set_time_remap", "target": "Shot 3", "timeRemap": {"keyframes": [{"t": 0, "sourceMs": 0}, {"t": 1, "sourceMs": 4000, "easing": "easeInOut"}]}} maps timeline time onto source time. set_time_remap replaces the whole map each time it is called. t runs 0..1 over the clip's window and must start at 0, end at 1 and ascend; sourceMs says which millisecond of the source plays there, so a descending pair is a reverse and a flat pair is a freeze. "timeRemap": null clears it, and the remap replaces the clip's rate. A repeated or backwards t is time_remap_not_monotonic, an error.

split_clip and trim_clip refuse a remapped clip. Both would have to re-derive the map, and re-deriving it silently is how a speed ramp turns into a different move. Clear the remap, cut, re-apply.

Compositions

A composition is a group saved as a reusable rig with named parameters. list_compositions reports what is available, get_composition shows one rig's parameters and their defaults, and {"op":"insert_composition","composition_id":"lower-third","startMs":4000,"params":{"name":"Ada"}} instantiates it into the document with fresh clip ids. Use parameter names returned by get_composition; name here is an example.

Bundled examples include title-card, lower-third, caption-bar, callout, cta-end-card, logo-sting, title-slam, word-cards, window-frame, and number-ticker. Use list_compositions for the templates available in the current workspace.

Insert first, then override params. The rig's timing and motion are already balanced against each other, and rebuilding it from bare clips throws that away. Edit the instantiated clips only where the brief actually differs.

When a group should be reused, save_composition {timeline_id, group_target, name, params} extracts it as a composition. params map names to typed defaults and JSON pointers into child fields. insert_composition remaps the group and child clip IDs, including internal layout and animation links, while preserving references to clips outside the composition.

Cutting to the beat

detect_audio_events on the music clip reports onsets and a tempo. Its onsets.times are in seconds and the ops take milliseconds, so multiply by 1000. Read the tempo's reliable flag before building a grid from bpm: speech and room tone produce a confident-looking number from nothing.

{"op":"set_markers_from_beats","onsets_ms":[0,500,1000],"count":3} lays the grid down so you can see it. {"op": "snap_to_beats", "targets": "all", "tolerance_ms": 60, "mode": "start", "action": "move"} pulls clip edges onto it. Either takes onsets_ms or a bpm with an offset_ms.

The default 60ms tolerance is about two frames at 30fps: close enough that a cut reads as on the beat, tight enough that it will not drag an edge to a beat it was never near. Widen it only when the grid is loose, because a large tolerance moves cuts you meant to leave alone. snap_to_beats reports every target, including the ones no beat was in reach of, with the reason.

action: "move" slides the clip and keeps its length, which shifts everything downstream. action: "trim" changes the length and leaves the neighbours where they are. Move a title that has to land on a hit; trim a picture clip whose out point has to meet the next shot.

Validate the output

Two checks, and they answer different questions.

validate_timeline answers "is this document sound": unknown animation presets and transitions, fades and cuts longer than the clip carrying them, clips on tracks the document lacks, overlapping clips, clips shorter than a frame, in/out points that cannot render, duplicate ids. Run it after every batch of edits.

It also names the motion failures that used to be yours to remember. Each finding carries a stable code; these are the ones that mean the picture will not perform the motion you wrote:

CodeSeverityWhat to do about it
parent_cycleerrorA parentId chain loops, so the group cannot be resolved at all. Break the loop
matte_source_missingerrorThe matte names a clip the document lacks, or itself. The layer draws unmatted, showing everything the matte was there to hide
time_remap_not_monotonicerrort repeats or goes backwards. sourceMs may descend — that is a reverse — but t may not
custom_animation_invaliderrorA stored preset: "custom" animation has unusable baked numeric curves and no style track or text animator to render. Re-bake its curves or add a valid typed track
animation_style_invaliderrorA style target has no matching clip style, a path cannot morph, a color cannot parse, or a glyph track lacks text stagger. Fix the target or the clip style
animation_exceeds_clipwarningThe window does not fit the clip after its delay, so the motion is clamped or never runs. Shorten durationMs, cut the delay, or lengthen the clip
stagger_compressedwarningThe units did not fit, so the per-unit offset was shrunk. The line lands faster and flatter than you wrote it. Shorten the per-unit durationMs or give the clip more time
typewriter_not_staggeredwarningA typewriter is stored with a plain durationMs and no stagger. Nothing draws for that time, then the whole text appears with no caret. Store durationMs: 1 with a character stagger, or apply the preset again through animate_clip
animation_holds_rest_before_windowwarningA custom out curve starts away from the channel's rest value after clip start. An out does not apply before its window, so the frames before it show the rest value (a gauge drawn from trimEnd: 0 shows a full ring). Start the window at clip start. For a trim, position or anchor curve, you can instead set the clip's own value to the first keyframe
replace_curves_overlapwarningTwo animations drive one absolute channel (positionX/Y, anchorX/Y, trimStart/End) at once. The last in document order wins and the other is discarded. Separate them in time or fold them into one curve
text_backing_unprovenwarningText draws over picture with no background, stroke or shape behind it, so nothing decides whether it reads. Preview the frame and look, or give it a plate
text_illegiblewarningType under 2.5% of frame height, or under a 3:1 contrast ratio against its own background plate or a full-frame shape behind it. Raise fontSizePx, darken the scrim, or add a stroke
unknown_easingwarningOutside the grammar, so it eases linearly. Check the spelling: easeOut, not ease-out
unknown_transitionwarningThis build cannot draw that type or direction; it cross-fades left instead. Pick one from the table above
unknown_effectwarningThis build cannot apply that effect; the layer draws ungraded
unknown_shape_kindwarningNo geometry for that shapeStyle.kind, so the shape draws nothing. A scrim that vanished is usually this
transition_exceeds_durationwarningThe cut is longer than the clip carrying it. Shorten the transition or lengthen the clip
parent_missing, parent_not_groupwarningThe parentId names nothing, or names a clip that is not a group. The child renders unparented, losing the group's transform, opacity and window
mask_path_invalidwarningThe kind or path d cannot rasterize, so the layer draws unmasked and the whole picture shows through
layer_cap_exceededwarningMore video clips overlap at that instant than the compositor draws; the ones on lower tracks are dropped
font_not_portablewarningThe family is not one NodeTool ships, so every host resolves it against its own installed fonts and the editor and the render can differ. Pick a bundled family for anything you hand back
field_strippedwarningA field the schema drops, lost on the next save. Usually a newer build wrote it; re-author it in this build's grammar

The contrast half refuses to guess: a colour it cannot parse, a translucent plate or gradient-filled type produces no contrast finding. Type over picture with no plate at all is the case it can name without measuring, and it says so as text_backing_unproven. Either way a silent text_illegible is not a pass — that is what the frames are for.

preview_timeline_frame answers "what does it look like". It composites the real frame — every track layered in order, transforms and opacity applied, animations sampled, transitions part way through, text and shapes drawn — at the timecodes you name. It needs no browser, no GPU and no open editor.

json
{"timeline_id": "<id>", "times_ms": [1200, 1450, 3000]}

range {from_ms, to_ms, count} samples evenly across a span instead of naming each timecode — the way to watch a whole entrance rather than guess three points inside it. sheet: true tiles every frame into one labelled contact sheet, so a sweep is one image to read instead of a handful of handles.

It returns an image handle per timecode; call view_image on the ones you need to see. Beside each frame it lists the layers top of the stack first, with the opacity, blend mode and wipe progress each one resolved to — often enough to diagnose a problem without looking at a pixel.

Sample the middle of a motion, not its ends. The endpoints are the states you already know: before an entrance the clip is absent, after it the clip is at rest. A 400ms fade-in starting at 1000ms tells you nothing at 1000ms or 1400ms and everything at 1200ms. For each animation you added, preview at:

  • the midpoint of the entrance,
  • a held moment between the in and out,
  • the midpoint of the exit,
  • and, for staggered text, a point after the first word lands but before the last does — the frame where half the line is on screen is the one that reveals a stagger that is too slow.

What the frames catch that a validator cannot: text over a busy or same-colored plate, a lower-third under the wrong element because two tracks are the wrong way round, a title that has slid outside the frame, a stagger still finishing when the clip ends, a scrim covering the face it was supposed to sit beside, an element that never appears because its clip is on an audio track.

Each frame carries degraded entries for a difference the Canvas 2D host could not draw, such as a missing scratch surface, a hard mask edge, or an unsupported perspective transform. Read the actual entries before deciding which part of the frame needs a GPU render. Known clip effects have CPU passes.

Read the skipped field on any layer that drew nothing — it says whether the clip is still draft, its asset would not read, or the source had no frame at that time. A missing picture is usually an unrendered clip, not a motion bug.

effects_not_applied names enabled effect types the Canvas 2D compositor does not recognize. A known effect such as chroma key, vignette, sharpen, or LUT should render in the frame preview; a missing scratch surface instead appears in degraded.

The render loop

Change one thing, preview the same timecodes, compare. When motion is wrong, the order to check is: is the element on screen at all (layer present in the report), is it in the right stacking position (z_index), is it at the right opacity (opacity), is it in the right place (look at the frame). Working down that list beats re-authoring the animation.

For every defect named by a view_image look, a preview layer report, or a model critique of the render, fix it and re-check the same timecode, or tell the user it remains open and why. Never ship past a named defect silently. If view_image returns no pixels, tell the user you could not see the frames. Do not claim a visual check.

compare_timeline_frames {a, b, times_ms | range} measures what actually moved between two documents or two versions. Run it after a change the user did not ask for (a restructure, a composition insert, a snap pass) so you can say which frames it touched instead of hoping it touched none.

Before calling the cut done, render it. render_timeline with a preview_scale below 1 while you are still iterating, then once at full size. The preview answers what one timecode looks like; the render answers whether the file plays.

A whole-document rewrite goes through set_timeline_document, which validates before it writes and snapshots what it replaces. Name that snapshot (snapshot_name: "before the title pass") so the version list says what you were about to do. For anything smaller, create_timeline_version first and edit_timeline after.

Stop when the motion is right, and say what you animated.

© nodetool-ai, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in packages/system-skills/timeline-edit-ops of nodetool-ai/nodetool.

Open the folder on GitHubat commit 58765d3

Compare with similar skills

Timeline Edit Ops next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Timeline Edit Ops compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Timeline Edit Ops this skillnodetool-ai/nodetool560—~11kAutomated safety check: PassAGPL-3.0
HyperFrames Animationheygen-com/hyperframes59k3 repos~2.1kAutomated safety check: PassApache-2.0
Slack GIF Creatoranthropics/skills180k29 repos~2kAutomated safety check: PassApache-2.0
Stitch to Remotion Walkthrough Videosgoogle-labs-code/stitch-skills8.4k6 repos~3.2kAutomated safety check: NotesApache-2.0
Faceless Explainer Videoheygen-com/hyperframes59k3 repos~7.7kAutomated safety check: NotesApache-2.0
Video ShotcraftVincentwei1021/video-shotcraft11k—~2.6kAutomated safety check: PassApache-2.0

Similar skills

  • HyperFrames Animation

    heygen-com/hyperframes

    Collects motion rules, scene blueprints, transitions and runtime adapters for HyperFrames video compositions, with GSAP as the default animation runtime.

    59k GitHub starsUsed in 3 repos~2.1k tokens
    Media & CreativeAuto-check passed
  • Slack GIF Creator

    anthropics/skills

    Official

    Provides Slack size and frame limits, Python animation helpers and validators for building animated GIFs sized for emoji and messages.

    180k GitHub starsUsed in 29 repos~2k tokens
    Media & CreativeAuto-check passed
  • Stitch to Remotion Walkthrough Videos

    google-labs-code/stitch-skills

    Official

    Builds walkthrough videos from Stitch design projects using Remotion, with transitions, zoom effects and text overlays on each screen.

    8.4k GitHub starsUsed in 6 repos~3.2k tokens
    Media & CreativeAuto-check: notes
  • Faceless Explainer Video

    heygen-com/hyperframes

    Turns an article, notes or a topic brief into an explainer video whose visuals are invented per scene, built frame by frame in HyperFrames with no footage.

    59k GitHub starsUsed in 3 repos~7.7k tokens
    Media & CreativeAuto-check: notes
  • Video Shotcraft

    Vincentwei1021/video-shotcraft

    Makes cinematic product videos with Remotion from shot recipe cards, a ready template, real page screenshots, camera moves and sound design, or builds a single animated shot.

    11k GitHub stars~2.6k tokensUpdated 4 days ago
    Media & CreativeAuto-check passed
  • Figma to HyperFrames

    heygen-com/hyperframes

    Imports Figma assets, brand tokens, components and motion into a HyperFrames video composition, using the Figma REST API with a connector or native export for shaders.

    59k GitHub starsUsed in 3 repos~4.5k tokens
    Media & CreativeAuto-check: notes

More from nodetool-ai/nodetool

All 127 skills in this repo
  • Beat Sync Editing

    nodetool-ai/nodetool

    Cut a NodeTool timeline to music and shape its pacing — detect the beat grid, place cuts on phrases, pick a cut type, build speed ramps with time remap, and give the piece an arc.

    560 GitHub stars~2.6k tokensUpdated today
    Auto-check passed
  • Caption Titles

    nodetool-ai/nodetool

    Add and animate a consistent text layer on an existing NodeTool timeline.

    560 GitHub stars~1.9k tokensUpdated today
    Auto-check passed
  • Color Motion

    nodetool-ai/nodetool

    Choose and animate colour on a NodeTool timeline, including shape and text gradients, colour grades, 3D LUTs, and dither.

    560 GitHub stars~2.3k tokensUpdated today
    Auto-check passed
  • Commercial Beat Sheet

    nodetool-ai/nodetool

    Write a shootable, precisely timed commercial beat sheet and store it as a NodeTool storyboard, with a consistent entity roster behind every shot.

    560 GitHub stars~4.6k tokensUpdated today
    Auto-check passed
  • Elevenlabs Audio Prompting

    nodetool-ai/nodetool

    Direct ElevenLabs speech, dialogue, sound effects and music — the bracketed audio tags v3 acts on and why the voice decides whether a tag lands, stability as the delivery dial, punctuation instead…

    560 GitHub stars~1.9k tokensUpdated today
    Auto-check passed
  • Frame Composition

    nodetool-ai/nodetool

    Stage the frame on a NodeTool timeline — grids, focal placement, safe areas per aspect ratio, depth layers and parallax, camera moves, and where elements enter and leave.

    560 GitHub stars~3.5k tokensUpdated today
    Auto-check passed

Questions about Timeline Edit Ops

What does Timeline Edit Ops do?

Reference for editing a NodeTool timeline that already exists with edittimeline ops — animateclip roles and presets, the easing grammar, per-word/character stagger, setclipparams limits…. Timeline Edit Ops is an agent skill from nodetool-ai/nodetool. Reference for editing a NodeTool timeline that already exists with edittimeline ops — animateclip roles and presets, the easing grammar, per-word/character stagger, setclipparams limits, document-only fields (styleTracks, textAnimator, animationLinks, layout, camera2d, motionBlur, repeater), groups, transitions, masks and mattes, clip effects, time remap, compositions, beat-cutting ops, and validatetimeline's/previewtimelineframe's full mechanics.

When should I use Timeline Edit Ops?

Timeline Edit Ops fits situations like: A few changes to a cut that already exists — retiming a clip; swapping a preset; nudging a stagger.

How do I install Timeline Edit Ops in Claude Code?

Run `npx skills add nodetool-ai/nodetool --skill timeline-edit-ops -a claude-code`. Or copy the skill folder (packages/system-skills/timeline-edit-ops in nodetool-ai/nodetool) into .claude/skills/timeline-edit-ops in your project. Claude Code loads it when a task matches its description.

How do I install Timeline Edit Ops in Codex?

Run `npx skills add nodetool-ai/nodetool --skill timeline-edit-ops -a codex`. Or copy the skill folder (packages/system-skills/timeline-edit-ops in nodetool-ai/nodetool) into .agents/skills/timeline-edit-ops in your project. Codex loads it when a task matches its description.

Can I use Timeline Edit Ops in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add nodetool-ai/nodetool --skill timeline-edit-ops -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/timeline-edit-ops, .gemini/skills/timeline-edit-ops, .github/skills/timeline-edit-ops and .opencode/skills/timeline-edit-ops in your project.

What does Timeline Edit Ops need to run?

SKILL.md names no scripts, command-line tools or credentials: Timeline Edit Ops is instructions for the agent only.

Does Timeline Edit Ops access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Timeline Edit Ops safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Timeline Edit Ops use?

Timeline Edit Ops is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Timeline Edit Ops use?

About 11k tokens (SKILL.md is roughly 42k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Timeline Edit Ops?

Skills that share tags, products or a category with Timeline Edit Ops: HyperFrames Animation (heygen-com/hyperframes, 59k stars), Slack GIF Creator (anthropics/skills, 180k stars), Stitch to Remotion Walkthrough Videos (google-labs-code/stitch-skills, 8.4k stars) and Faceless Explainer Video (heygen-com/hyperframes, 59k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Timeline Edit Ops?

nodetool-ai (a GitHub organization) maintains it in nodetool-ai/nodetool, which has 560 GitHub stars. The repository holds 127 skills in this directory. The repository was last updated on October 9, 2026.

Source: nodetool-ai/nodetool on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.