Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Build a Beutl timeline from a shot list through the Agent Editing Toolkit MCP tools.
Beutl Agent Timeline From Shot List
Use this skill when an agent needs to turn a shot list, storyboard, or timed brief into a Beutl project through the Agent Editing Toolkit.
Workflow
Author storyboard-first: plan a fine shot breakdown, build the static layout of every shot (static storyboard) with no keyframes and no effects, lock it, then add effects, then motion. Do not animate or add effects until the static storyboard reads correctly. The phases below map to that order.
Phase -1 — Video type classification
Before classifying, check the intake. If the incoming request is terse (missing two or more of: subject specifics, target duration, mood or audience, explicit style/palette constraints, an asset inventory) or the user supplied reference images/video/URLs for the intended look, load beutl-agent-brief-expansion first and use its recorded expandedBrief as the brief for classification and Phase 0. Expanded-brief fields are derived defaults: user-stated constraints stay literal, and the derived paletteDirection feeds derive_palette inputs rather than replacing its outputs. Runs that started from brief expansion are low-effort mode and must run the visual-review convergence loop in Phase 4 (step 21a).
Classify the brief before Phase 0. Record videoType and a one-line reason in notes, then call get_started(videoType) and follow the returned type workflow. Supported values are motion-graphics, footage-cut, slideshow, lyric-captions, and logo-intro. If the brief requires source clips, photos, music, SFX, or fonts that are missing from the workspace, load beutl-agent-asset-sourcing before Phase 0, decide source-or-generate per asset, and record provenance in assets/manifest.json before placing media.
videoType
Signals
motion-graphics
Authored graphics, kinetic type, promo/explainer/infographic motion, BPM or beat-grid language, layered background grammar, no source media inventory as the main driver.
footage-cut
User supplies video clips, says edit my clips/vlog/event/interview/B-roll, asks for trim/order/source audio/music bed, or needs narrative clip coverage.
slideshow
Photos/stills plus music, photo movie/album/memories language, per-photo duration grid, gentle Ken Burns motion, consistent transitions, caption read time.
lyric-captions
Lyrics, subtitles, captions, transcript, timestamp sync, per-line timing, readability and contrast as the core deliverable.
logo-intro
Logo animation, intro, stinger, bumper, brand mark, 3-10 seconds, single subject, anticipation/reveal/settle/hold arc.
Pass the resolved videoType on every preview_quality_risks, suggest_quality_fixes, evaluate_edit_quality, and final_preflight call. Do not omit it after classification; omitted videoType intentionally means the legacy motion-graphics gate behavior. For motion-graphics and slideshow, pass the derived palette role colors to rendered Phase 4 quality calls as paletteRoleColors using #RRGGBB values, preferably as an array of { "role": string, "color": "#RRGGBB" } objects. If an MCP client cannot send that complex array reliably, pass paletteRoleColors as a JSON string containing the same array. If derive_palette returned #AARRGGBB, drop the alpha channel before calling the quality tools.
Phase 0 creative direction/palette/background grammar becomes media inventory plus cut list unless authored graphic overlays are requested; when required clips are missing, load beutl-agent-asset-sourcing and record assets/manifest.json provenance before placing media; shotBreakdownPlan becomes clip in/out/order/Start/Length; audio handling is explicit.
derive_palette and get_background_grammar unless graphic overlays are requested; motion-graphics density/beat-grid plan conformance.
beatGridPlan becomes a timestamp sync table; shotBreakdownPlan becomes one Element per line/caption with Start/Length from the sync table; type roles are hero line/echo/credit.
Motion-graphics background richness and density planning; BPM-driven tempo targets unless the brief separately requests them.
shotBreakdownPlan collapses to one 3-10 second shot with anticipation/reveal/settle/hold; storyboard subdivision reviews the internal motion arc instead of adjacent cuts.
Author the creative direction yourself. list_creative_directions is optional divergence stimulus, not a menu to pick from.
Decide the concept, base hue, tonal seed, harmony scheme, type system, motion vocabulary, and shot structure from the brief (or from scratch when the brief is vague) before leaning on any tool. Do not anchor on returned seed names.
Record the derivation reason before any color or background authoring: why this subject, audience, mood, keywords, and delivery surface lead to this hue family, tone, material vocabulary, and motion vocabulary. Unjustified choices are disallowed; if you cannot explain a hue/tone/motion choice, derive again instead of using it.
Call list_creative_directions (pass a fresh seed each run to vary the stimulus) and read its recentToAvoid list. Your direction MUST differ structurally from those recent fingerprints: change the dominant motion verbs, layout, palette family, and type treatment, not just the words. Do not default to the same look every run (e.g. hero-glow-on-dark + dashed selection-marquee + magnetic letter-spacing) — that repetition is the monotony this step exists to prevent.
Call derive_palette with baseHueDegrees, tonalSeed, harmonyScheme, the recorded derivationReason, and structuralSignature. Resolve any hue-band or structural repeat warning by changing the direction, or record a concrete reason why the repeat is intentional for this brief before continuing.
Call get_background_grammar, then instantiate the grammar in notes: one base layer, one required depth layer, optional second depth layer, one motion slot, and the three depth bands background, midground, and foreground. Concrete values come from the brief and derived palette, not from the grammar ranges alone.
The required workflow is: derive direction with recorded reason -> derive_palette -> instantiate background grammar. Any deviation, including hand-picked colors, skipped grammar, missing depth band, static background, or ignored repeat warning, requires a recorded reason in notes before apply_edit.
If the user prompt specifies concrete constraints (motif, style, palette, message, audience, subject), keep them literal; use the stimulus only to make the result less generic.
Once the concept is locked, call record_creative_direction with its fingerprint (concept label, palette roles, motion verbs, structural signature) so future sessions steer away from it.
Contrast Exemplars - derive, don't copy
These examples describe relationships to derive with derive_palette; they are not literal palettes to reuse.
Good: text-primary is much lighter than a dark bg-base and still clears the 4.5:1 floor against the brighter bg-accent; the saturated accent is small and clears 3.0:1 against bg-base.
Good: on a light tonal seed, text-primary is a dark low-saturation role, foreground is the readable material color, and the bright accent is reserved for small motion cues.
Bad: cyan text on a pale blue or bright yellow background relies on hue difference but fails luma contrast.
Bad: three fully saturated roles at similar lightness compete; mute support colors and let one accent carry saturation.
Bad: dark teal base plus cyan and magenta neon is only allowed when the brief explicitly asks for that look and the reason is recorded.
Phase 1 — Static storyboard
Build the static layout of every enumerated shot with NO keyframes and NO effects. The deliverable is a readable static storyboard: correct composition, hierarchy, typography, color, and layering at each shot's representative frame.
Create or attach a session:
Stdio/headless: create_project or open_project with a .bep project path. Paths without an extension are normalized to .bep; .beutl is reserved for exported project packages.
Live editor: attach_active_editor. In the in-app host, open_project/create_project open the project in the running Beutl editor itself (the editor holds a single open project) and return a LiveEditor session — opening a project different from the one already open is rejected; attach or have the user close it first.
If live attach fails and the task allows headless output, switch to the stdio/headless create_project route rather than creating a custom generator.
When an output directory is requested, create/update notes.md there before the first edit and after every apply_edit, save_project, render_storyboard, preview_quality_risks, suggest_quality_fixes, render_still, evaluate_motion_variation, evaluate_edit_quality, compare_revisions, final_preflight, and export_video result. Record success/failure, change count or verdict/path, and the next action. While drafting a large patch before the next tool call, append a short heartbeat note every few minutes with the current stage and blocker risk.
Call read_document and keep the returned schemaVersion.
Build the static layout as a declarative document (Phase 1 — no motion, no effects):
Use PascalCase property names exactly as returned by get_schema.
New timeline Elements require $type: "[Beutl.ProjectSystem]:Element".
Use stable Id handles when modifying existing elements.
Omit Id only for genuinely new elements/objects so the toolkit can mint one.
Do not add a second Object to an ordinary existing Element. To place another visible item, create another Element with its own single EngineObject. Add multiple to one only for an intentional flow chain such as , , , or ; keep the parent and omit only for genuinely new child objects in that flow chain.
Call render_storyboard to render one still per enumerated shot plus a contact-sheet PNG of the whole storyboard. Review it as a static storyboard: every planned shot present, one clear focal point per shot, readable typography, the intended layering and color, and correctly aligned text/backing-plate pairs. For a scene with many Elements the synchronous call can exceed the MCP client request timeout; pass background: true to get { status: "running", jobId } immediately, then poll read_render_job(jobId) until state is completed (its result holds the storyboard payload) — do not issue apply_edit while a background render is running; call cancel_render_job(jobId) to abort a background render you no longer need.
For a continuous single-shot piece whose Element boundaries do not create useful auto shots, pass explicit timeSeconds to render_storyboard instead of hand-assembling contact sheets from render_still. Choose representative times for opening, reveal/development beats, settle, and final hold; use subdivisionLevel:1 or 2 to inspect the motion arc between those anchors.
Run preview_quality_risks(videoType:<resolved>) (it does not evaluate motion). Of its categories, only elementStructure can fail the gate; shapeIntent, decorativeShapeClarity, and gradientFalloff are advisory, but fix their major findings before still rendering when they reflect a genuine accident (not a recorded creative choice). For multiple related issues, call suggest_quality_fixes(videoType:<resolved>) and apply the smallest repair. For high-tempo promos, set a styleProfile such as high-tempo-promo, kinetic-type, or high-tempo-promo 130bpm.
Run evaluate_edit_quality(videoType:<resolved>, staticLayout:true) — the storyboard-phase gate. It skips motion checks, so the motionless storyboard is judged on composition, typography, readability, and structure only. Resolve critical/major issues before leaving this phase.
Use measure_object_bounds for any text/backing-plate pair that looks misaligned in the contact sheet.
Do NOT run evaluate_motion_variation, evaluate_edit_quality without , or in this phase — those are motion-phase gates and will false-block a static storyboard (identical frames read as zero motion).
Iterate Phase 1 until the storyboard reads correctly; only then proceed.
Phase 2 — Effects
Add effect chains onto the locked storyboard, one named job at a time. Do not start motion until effects read correctly on stills.
Every effect chain needs a named job: material texture, hierarchy separation, transition energy, color grade, or text legibility. Remove decorative stacks that do not serve one.
For organic heat, ink, glass, smoke, grain, caustic, or atmospheric fields, prefer SKSLScriptEffect (from list_effect_recipes) over stacking only blurred gradient shapes; SKSL is CPU-safe in still renders. Prefer SKSL over GLSL for low-context file sessions. The fine-film-grain-field recipe ships a monochrome film-grain shader; organic-shader-field ships a colored field shader — pick by intent, and validate_shader any custom SKSL before apply_edit.
GPU/stylize effects (GLSL, PixelSortEffect, ColorShift on split-character text) are render-guarded to skip degenerate targets rather than crash, and run on the SwiftShader software fallback when no hardware GPU is present (slower, not skipped); confirm every effect with render_still/render_storyboard. Vary the effect vocabulary instead of over-restricting to one safe effect.
For masked reveals, knockouts, alpha mattes, and wipes, use the real masking mechanisms rather than covering rectangles: every Drawable has an animatable BlendMode whose Porter-Duff modes (SrcIn, DstIn, SrcOut, DstOut, Modulate) composite it against the content below it in the same flow — scope the matte by putting mask and content inside one DrawableGroup/DrawableDecorator Element so it does not knock out the whole frame — and the Clipping FilterEffect (animatable Left/Top/Right/Bottom) is the rectangular wipe/crop primitive. Blend-mode matte behavior depends on flow order, so source-ground the exact compositing rule (beutl-agent-source-grounding) and verify with render_still before building a sequence on it.
For a true emissive glow/bloom (light that adds over the original, not a DropShadow fake), duplicate the drawable with duplicate_object passing wrapInGroup=true, then apply_edit the additive-bloom recipe (blur + BlendModePlus + reduced Opacity) onto the returned so the copy glows over the untouched original. puts the original and copy under a (an ) so 's check stays clean; move the copy to a separate Element at a higher instead when it needs independent timing or z-order. Lower or switch to for bright footage that blows out.
Phase 3 — Motion
Add keyframes and animation on top of the locked storyboard and effects. Build the reveal, development, and resolution phases from motionContinuityPlan, and animate multiple property families (transform, opacity, brush/gradient, effect parameters, text spacing) — not just X plus opacity. Vary easing families per the 12 principles: use cubic/quintic ease-out for entrances, ease-in-out for moves, and reserve linear motion for deliberate mechanical travel. The easing catalog is much wider than cubic/sine — BackEase*/ElasticEase* give overshoot and anticipation, BounceEase* gives physical settles, and SplineEasing takes custom cubic-bezier control points; use them deliberately on accents (a bounce on every element turns cartoonish). Stagger related element starts by about 0.1-0.3 seconds and vary durations/directions so follow-through and overlapping action are visible. Apply motion in small apply_edit stages per beat and inspect validation after each; pass quiet: true for large patches.
Author the camera moves from cameraPlan first, before per-element accents: keyframe the [role:camera-rig]DrawableGroup's TransformGroup children — TranslateTransform.X/Y for pans and whip-pans, ScaleTransform.Scale for push-in/pull-back (set TransformOrigin so the zoom centers on the shot's focal point), RotationTransform.Rotation for roll. Fetch get_examples for insert-camera-rig-push-in (nested rig) or insert-camera-rig-portal (timeline rig) and reuse its structure. Camera moves are slow and eased by default (a push-in of roughly 3-8% scale over the shot already defeats the static-slide feel); whip-pan bridges are the fast exception. For parallax, keyframe each depth-band rig's translate at a different amplitude (background slowest). Remember the inverse: camera-left = rig-right, camera-in = scale-up. For a true 3D dolly or orbit, use a Scene3D object and keyframe its Camera.Position/Camera.Target instead of faking perspective with 2D scale.
For perspective moves on 2D content (card flips, page turns, tilted reveals), use Rotation3DTransform (RotationX/RotationY/RotationZ, CenterX/Y/Z, and Depth — the perspective distance, default 500) inside the TransformGroup instead of faking depth with 2D scale alone.
For kinetic type, set TextBlock.SplitByCharacters=true (animatable) so glyphs composite individually — this is the enabling property behind "split-character" effects such as per-glyph ColorShift fringing or PartsSplitEffect shatter — and animate TextBlock.Spacing for tracking-in/out reveals. Stagger per-character timing rather than moving the whole block as one unit.
For line-drawing reveals ("the stroke draws itself on"), animate Pen.TrimStart/Pen.TrimEnd (0-100) on a GeometryShape/shape/text stroke; for marching-dash accents set Pen.DashArray (a static float list) and animate Pen.DashOffset.
Audio is authorable, not analysis-only: keyframe Sound.Gain (percent: 100 = unity, values above 100 amplify) for fade-ins/outs and ducking under narration, set Sound.Effect to an with /// children for echo/EQ/dynamics, and use (an — the pairing rule applies) to submix multiple audio Elements.
Phase 4 — Motion verification and export
Re-render the storyboard with subdivisionLevel: 1 after Phase 3 motion authoring; raise to subdivisionLevel: 2 for suspicious gaps. READ the contact sheet, including every kind: "inbetween" frame, and check cutEyeTrace before judging continuity. For multi-shot types (motion-graphics, footage-cut, slideshow, lyric-captions), every adjacent shot pair must show the planned continuity: an element crossing the cut, a camera move continuing across it (matched push-in, whip-pan), a sweep, shared background continuity, an overlapping transform/opacity ramp, a deliberate hard-cut reason, or a consistent slideshow transition. Also verify the cameraPlan executed: shots planned as push-in/pan/parallax must show visible framing change between their in-between frames — identical framing on a non-locked shot is a rework finding. If the in-between frames look identical to the anchors except for an unplanned hard swap, treat it as a rework finding and add bridge animation or transition consistency. If cutEyeTrace.exceedsEyeTraceBudget is true and no planned bridge or hard-cut reason exists for that pair, treat it as a rework finding: add a bridging element, sweep, or focal-point realignment. For logo-intro, review the single-shot motion arc instead of cuts: anticipation, reveal, easing, settle, and the final hold. Record cutContinuityActuals or motionArcActuals in notes with pass/fail, evidence frame names, cutEyeTrace displacement when present, and the repair when needed.
Verify with render_still at representative shot boundaries. Treat any returned warnings as a blocker for export until you have either revised the scene or recorded why the warning is acceptable. In live-editor sessions, renders honor the editor's layer state — TimelineLayer lock/solo/audio-mute/video-mute and Element.IsEnabled — so if an expected layer is missing from a still, check those flags before diagnosing your own edit as the cause. For each still, record visibilityAnalysis.visiblePixelRatio, foregroundPixelRatio, occupiedBoundsRatio, and maxQuadrantForegroundRatio; compare activeElements against the planned visible elements; note the primary focal point, whether text/title elements are readable for their duration, whether effect chains still serve their named jobs, and whether foreground/background/accent density is present. Development and resolution stills should show at least three visible layer types, such as background/surface, primary motion, accent/detail, and typography; if text is present, it must have clear contrast against the background.
If a large decorative RectShape remains active behind several unrelated text shots, treat it as a likely text-background-fit problem before quality review. Limit it to the shot where it belongs, move it clearly into the background/surface role, or replace it with stroke/ellipse/path/procedural texture.
Run evaluate_motion_variation across 4-6 samples. If it reports low-motion-variation or poor-frame-coverage, or if the still review shows planned elements are never visible/readable, revise the edit.
Run evaluate_edit_quality(videoType:<resolved>) with the same sample set (the full motion gate; leave staticLayout off here). For motion graphics, pass plannedForegroundElementsPerShot from quantitativePlanSheet; for motion-graphics and slideshow flows, also pass paletteRoleColors from the derive_palette roles so paletteBalance can report the authored 60-30-10 area mix. Use an array of { "role": string, "color": "#RRGGBB" } objects, or a JSON string containing that same array when the MCP client cannot send complex arrays reliably. When beatGridPlan came from analyze_audio_rhythm, also pass beatTimesSeconds so audioSync advisories can flag near-miss cuts. Pass allowMinimalDensity=true only when the brief explicitly asks for minimal/negative-space density and the deviation is recorded. Only gate-failing issues (typographyReadTime, rendered typographyContrast, elementStructure, motionContinuity, and supplied-plan layerDensity) block export; shapeIntent, motionIntent, decorativeShapeClarity, gradientFalloff, tempoRhythm, paletteHarmony, and backgroundRichness major issues are advisory and do not block. Revise and re-run until passesQualityGate is true; fix genuine-accident advisory findings when time permits, or record the explicit user reason for allowing an issue.
For motion-graphics deliverables, animatedPropertyCount: 0 is a blocker even when evaluate_edit_quality.passesQualityGate is true. Add explicit transform, opacity, spacing, brush, or effect animation before export.
For textBackgroundFit issues involving decorative glass/light/texture rectangles, prefer a real design fix over suppressing the issue: constrain the rectangle's Start/Length to the intended beat, align it as a named backing plate with measure_object_bounds, lower it into the background, or replace it with a non-plate visual treatment.
For high-tempo/BPM briefs, inspect metrics.tempo.RequiredTimelineEventsPerSecond, TimelineEventsPerSecond, RequiredTotalEventsPerSecond, LongForegroundGapCount, and LongestForegroundEventGapSeconds. A scene is too slow if background motion hides sparse foreground changes or long foreground gaps.
After each revision apply_edit made in response to Phase 4 quality or visual-review findings, call compare_revisions before the next broad fix. Record a delta ledger in notes with metric deltas, issues resolved, issues introduced, regression, and the paired still paths. If regression=true, treat the introduced/worsened axis as a rework finding before export.
Plan-conformance check (before finishing). Before final_preflight/export, compare the authored result against your quantitativePlanSheet on the same four axes — not just the pass/fail gate. Gather actuals from read_document_summary for shot/Element count, evaluate_edit_qualitymetrics.tempo (TimelineEventsPerSecond, SlowHoldCount, LongestForegroundHoldSeconds), metrics.layerDensity (Bands, AverageForegroundLayerCount, MinimumForegroundLayerCount, BandsBelowHalfPlannedForegroundLayerCount), and evaluate_motion_variation. Confirm all four: actual shot/Element count ≥ planned shot count; actual TimelineEventsPerSecond ≥ planned edits/sec target (and ≥ RequiredTimelineEventsPerSecond); every hold ≤ its planned ceiling (no SlowHoldCount beyond named resolves); every measured time band's ForegroundLayerCount meets the planned density floor, with a Major layerDensity issue if authored density falls below half the supplied plan. Any axis short of the plan is a rework blocker — add the missing shots/events/layers and re-verify, or record a true minimal-density brief and pass allowMinimalDensity. Record a conformance summary in notes: per axis, planned vs actual vs pass/fail.
21a. Convergence loop for low-effort runs. When the run started from beutl-agent-brief-expansion (or the coordinator explicitly requests convergence), run beutl-agent-visual-review in its convergence loop mode after the deterministic gates pass and before final_preflight: iterate revision passes until every rubric axis scores ≥ 3 or the pass budget (default 3) is exhausted, phrasing every directive in the expanded brief's own concept vocabulary. An unconverged result hands off to the human with the delta ledger instead of exporting silently.
Prefer final_preflight(videoType:<resolved>) before export when the tool is available. For motion graphics, pass requireAnimatedProperties=true and plannedForegroundElementsPerShot from quantitativePlanSheet; for motion-graphics and slideshow flows, pass the same paletteRoleColors shape used in evaluate_edit_quality; pass beatTimesSeconds from when a music bed was analyzed. Export only when is true. If is unavailable, use the separate + + sequence above.
Motion Graphics Quality Bar
Author storyboard-first: build and verify the static layout of every shot (Phase 1, via render_storyboard + evaluate_edit_quality(staticLayout:true)) before adding effects (Phase 2) or motion (Phase 3). A storyboard that does not read clearly will not improve by animating it.
Use at least three timing phases: reveal, development, and resolution. Avoid a single continuous drift.
After Phase 3 motion authoring, verify adjacent cuts with render_storyboard(subdivisionLevel:1) and raise to 2 for suspicious gaps on multi-shot types. In-between frames must show bridge motion, transition consistency, or recorded hard-cut intent, and cutEyeTrace must not exceed the budget without a planned bridge or hard-cut reason. For logo-intro or any continuous single-shot piece, pass explicit timeSeconds anchors and inspect the subdivided frames as a motion arc instead of a cut-continuity pass.
Build fast tempo through contrast between quick accents and held readability beats. Do not make every layer move at the same speed.
Avoid motion monotony: do not leave most keyframe transitions linear, and do not start every animated element at the same time with the same duration and direction.
For 120-140 BPM briefs, work from a beat grid instead of the vague word "fast". At 130 BPM, 1 beat is about 462 ms, 2 beats about 923 ms, and 4 beats about 1.85 s. Plan enough foreground element boundaries and keyframes for the piece to read as fast in tempoRhythm metrics.
Keep normal foreground beats near 2-4 beats. Longer holds are acceptable only for named background texture, ambient support, or a deliberate final resolve; add visible foreground events when readability requires a longer text hold. Background-only drift does not satisfy a fast-tempo brief.
Animate multiple property families across the piece, such as transform, opacity, brush/gradient, effect parameters, and text spacing. Do not rely only on X movement plus opacity.
Move the camera, not just the elements. A piece whose viewpoint never moves — every shot a static composition with only element-level animation, swapped by cuts — reads as a slide deck. Use [role:camera-rig]DrawableGroup transforms for push-ins, pans, parallax, and whip-pan cut bridges per cameraPlan; an all-locked-off piece needs an explicit recorded brief reason.
Keep ordinary timeline Elements to one EngineObject. Multiple objects in one Element are reserved for IFlowOperator chains such as DrawableGroup, DrawableDecorator, SoundGroup, or ; otherwise split each visual object into its own Element.
Originality Rules
For creative briefs, build an original timeline with small staged apply_edit calls; do not use list_compositions, plan_composition, or empty-scene examples as the default output path. To bootstrap from a non-template original starting point, plan_original_scaffold returns a one-call seed-derived scaffold (background/headline/subtitle/decorative foreground) whose placeholder copy and structure you then rewrite for the brief — it is an original starting point, not a reusable template.
Treat list_creative_directions output as raw inspiration only. Do not copy returned seed names as the final concept title, Element/Object names, layer order, or file basename.
Use composition templates only when the user explicitly asks for a template, starter, quick draft, or named template style.
When a template is explicitly requested, pick a specific returned template name from list_compositions; do not rely on an implicit first template selection.
Treat examples as schema snippets or fallbacks. Adapt their structure to the brief instead of copying a full starter scene unchanged.
Avoid overused no-context motifs such as orbit rings, radar sweeps, map/atlas labels, signal nodes, dashboard bars, and dark teal cyan/magenta neon unless the user asks for them.
Cross-session variety is a hard requirement: the same brief should NOT keep producing the same video. Before locking a direction, compare it against recentToAvoid, pass the structural signature into derive_palette, and deliberately change the structural language (motion verbs, layout grid, palette family, type treatment, transition style) from recent runs; then record_creative_direction so the next run can diverge too.
Pass quiet: true to apply_edit for large staged patches; the full echoed change set can exceed the response size limit, so keep individual patches small and use the compact summary while authoring.
Shot List Mapping
Drive Element boundaries from the enumerated shotBreakdownPlan. One shot normally maps to one or more Element entries with explicit Start, Length, and ZIndex; each ordinary Element contains exactly one drawable/audio EngineObject.
Use multiple Objects inside one Element only for explicit IFlowOperator flow chains such as DrawableGroup, DrawableDecorator, SoundGroup, or Scene3D; otherwise split compound visuals into separate Elements.
A shot with a planned camera move maps to one [role:camera-rig] rig Element — either a portal rig (PortalObject.Count = N) pulling the shot's N contiguous content layers directly above it, or a nested DrawableGroup parenting the content as Children — with locked background plates as separate Elements below the rig, outside the portal's ZIndex range.
Background plates should be lower ZIndex; titles, logos, and overlays should be higher.
Prefer explicit durations over relying on media original duration unless the brief explicitly asks to preserve source timing.
For repeated visual treatments, duplicate structure deliberately; do not rely on implied defaults when the brief gives concrete values.
Merge-Patch Rules
Arrays of objects with Id are id-keyed. A bare id-less array merges/appends into the existing members; it does NOT replace them.
Use { "Id": "...", "$delete": true } for removals.
To wholesale-replace an id-keyed array in one patch (e.g. swap a FilterEffectGroup.Children chain instead of appending to it), make the FIRST element the sentinel { "$replace": true }; the following elements rebuild the array in order (omit Id to mint fresh, or reuse an Id to keep that child), and [{ "$replace": true }] alone clears it. Replacement elements cannot also carry $delete/$index/$after/$before. Keep the group's own Id so only its children change.
Use $index, $after, or $before for ordering; do not combine ordering directives.
Unknown Id means stale handle; call read_document again instead of guessing.
Existing parent with new flow child example: { "Elements": [{ "Id": "<existing-flow-element-id>", "Objects": [{ "$type": "<discriminator-from-get_schema>", "Name": "new-flow-child" }] }] }. Use this only when the existing Element is an intentional IFlowOperator flow chain. Ordinary Elements should not receive a second Object.
New Element example: { "Elements": [{ "$type": "[Beutl.ProjectSystem]:Element", "Name": "new-element", "Start": "00:00:00", "Length": "00:00:02", "Objects": [{ "$type": "<drawable-discriminator-from-get_schema>", "Name": "new-object" }] }] }. New Elements and Objects omit Id.
New flow-operator Element (e.g. a [role:camera-rig]DrawableGroup): the Objects array must pair a PortalObject immediately before the flow operator — "Objects": [{ "$type": "[Beutl.ProjectSystem]:PortalObject", "Count": <N> }, { "$type": "<DrawableGroup-discriminator>", "Name": "[role:camera-rig] ...", "Children": [...] }]. A bare flow operator without the PortalObject is rejected. Count is an inclusive ZIndex span, not an element count: every active Element with ZIndex in rig+1..rig+Count is pulled into the operator (leave Children empty then; empty rows contribute nothing). pulls no timeline rows, so with the portal as the Element's first object the operator consumes only its nested ; set to explicitly discard earlier same-Element flow. Applies to all s: , , , . See (nested) and (timeline).
Progress Watchdog
Keep notes.md granular enough for another observer to reconstruct the route: every apply, save, render, evaluate, export, validation failure, and route change gets an entry.
During long patch authoring between tool calls, update notes.md before the three-minute mark with a heartbeat such as drafting stage N patch; next tool: apply_edit; if you cannot do that, stop and report a blocker.
If no tool success, saved project artifact, render/export artifact, or notes update happens for about three minutes while editing, stop and report the blocker/status instead of silently continuing.
If the user or coordinator asks for status, call read_operation_status when available and respond immediately with the current session/source, last successful stage, and blocker before continuing.
Safety Rules
Keep values in documented ranges. If apply_edit reports coercion or rejection, adjust the request and retry the same small stage.
Confirm destructive output overwrites only when the user explicitly asked for overwrite.
Do not write outside BEUTL_WORKSPACE.
In notes, record the authored concept label, derivation reason, derived palette roles/hue band, background grammar slots, motion verbs, structural signature, any stimulus names you used, derive_palette warnings, and how you diverged from recentToAvoid.
Map the concept into your own named Beutl elements/objects. Do not reuse returned seed names as Element/Object names.
For unconstrained briefs, keep project, still, and video basenames neutral, such as project.bep, preview.mp4, and still-*.png, or use the requested output directory slug. Record the concept name in notes instead of naming files after it.
Inventory source media before schema authoring for media-driven types, then call get_schema before authoring if the required drawable, media, or audio type is not already known.
For footage-cut, list every available clip, duration note, usable range, and audio role. For slideshow, list supplied photos or the needed image collection. For lyric-captions, list the music bed or the reason none is needed.
If any required clips, photos, music, SFX, or fonts are missing, load beutl-agent-asset-sourcing, follow its license/provenance contract, and write every downloaded or generated asset to assets/manifest.json before apply_edit.
For organic heat, ink, glass, smoke, grain, caustic, or other procedural fields, call list_effect_recipes with a shader/organic intent and consider SKSLScriptEffect instead of stacking only blurred gradient shapes. Prefer SKSL over GLSL for low-context file sessions because it is CPU-safe in still renders.
GPU/stylize effects (GLSL, PixelSortEffect, ColorShift on split-character text) are render-guarded to skip degenerate targets rather than crash the renderer, so use them for richer looks when wanted — and they run on the bundled SwiftShader software fallback when no hardware GPU is present (slower, not skipped), so always confirm the result with render_still before relying on them. Do not over-restrict to a single safe effect; varying the effect vocabulary is part of avoiding monotone output.
For particle-like density (sparks, dust, confetti, floating tokens, glyph debris), use the real ParticleEmitter drawable (get_schema type=ParticleEmitter) instead of faking a swarm with many ellipse Elements — fake particle swarms also trip decorative-shape review. Its animatable surface covers EmitterShape (point/line/circle), EmissionRate, Lifetime, Speed/Direction/Spread, Gravity, TurbulenceScale, size/color/opacity over particle life, and ParticleDrawable — any Drawable (a glyph fragment, a GeometryShape) can be the emitted sprite.
For music-driven briefs, the audio-reactive drawables AudioWaveformDrawable, AudioSpectrumDrawable, and AudioSpectrogramDrawable render real waveform/spectrum motion from an audio source with pluggable shape styles (bar, radial, mirrored bars, line, filled area, dots, block). Use them for lyric videos, visualizers, and beat-driven accents instead of hand-animating fake meters.
If source-code reading is allowed, use beutl-agent-source-grounding before authoring layout, transform, bounds, text measurement, render scale, effect-unit, reconciliation, or live-session semantics.
This is mandatory when the task mentions centered placement, coordinates/origin, TranslateTransform, TransformOrigin, backing plates, object bounds, render/export range, or when a rendered/user-observed result contradicts the plan.
Read .claude/skills/beutl-agent-source-grounding/SKILL.md, then use narrow rg/read passes over the source and tests it identifies.
Record a sourceGrounding note with assumption, evidence, rule, and uncertainty before the first relevant apply_edit.
If the user explicitly forbids source reading, skip this step and record that limitation.
Before authoring, record a quality preflight plan in notes:
directionContract: state the objective, audience, emotional temperature, brand posture, delivery surface, one-sentence promise, and the recorded derivation reason for hue, tone, material, and motion vocabulary. The reason must tie the subject/brief to the palette seed and motion vocabulary; unjustified choices are disallowed.
messageHierarchy: name the primary message, secondary emphasis, and supporting/caption information for each shot.
textCasePlan: use Title Case or sentence case by default; do not use long all-caps text unless the user explicitly asked for it.
typographyRolePlan: assign type roles such as hero, secondary, caption, label, and texture text before choosing sizes.
readTimePlan: keep fast-beat copy to a word, phrase, or symbol; hold or split longer text.
beatGridPlan: when a music bed file exists, call analyze_audio_rhythm before authoring and record estimatedBpm, confidence, beatTimesSeconds, and strongOnsetTimesSeconds; anchor Element boundaries, accent keyframes, and transition hits to those measured beats. If no music bed exists, convert explicit BPM or fast-tempo language into a nominal beat grid instead. For 120-140 BPM, default to 130 BPM when unspecified: 1 beat is about 462 ms, 2 beats about 923 ms, and 4 beats about 1.85 s. Plan visible foreground changes every 1-2 beats, normal foreground holds around 2-4 beats, no foreground event gaps longer than 4 beats, and only named final resolves/background textures may hold longer.
highTempoDensityPlan: for 120-140 BPM or roughly 1.5s shots, keep hero text to 1-3 words and supporting labels to 2-4 word tokens. Add density through nodes, particles, strokes, texture, accent motion, and secondary shapes rather than long copy. Do not count background-only drift as foreground tempo.
shapeBudget: reserve RectShape for full-frame/background plates or deliberately plain geometry; use rounded rectangles, ellipses, paths, media, strokes, or procedural texture for foreground structure. Do not leave a large persistent foreground RectShape behind multiple text beats unless it is an intentional named text backing plate with matching timing and measured padding. Do not use abstract glint/glow/aperture/lens ellipses as foreground decoration; replace them with parseable systems such as strokes, particles, letter fragments, editor/timeline marks, masks, media, or procedural texture.
elementStructurePlan: one ordinary timeline Element owns exactly one EngineObject. Multiple objects inside one Element are allowed only when that Element contains an IFlowOperator such as DrawableGroup, DrawableDecorator, SoundGroup, or Scene3D; otherwise split content into separate Elements. Choose DrawableGroup for multi-child grouping (camera rigs, portal intake); choose DrawableDecorator when the point is applying one shared transform/effect/opacity/blend to its children as a single composited unit (group fade, shared blur/shadow) — the difference is subtle, so source-ground it when the composited result matters.
roleTagPlan: name important objects/elements with role tags such as [role:background], [role:text-backing], [role:decorative], or [role:camera-rig] so MCP quality tools can distinguish real text plates from decorative accents and camera rigs from content groups.
shapeIntentPlan: every large or animated foreground shape must have a clear role, purpose, and motion intent in the Element/Object name, such as [role:decorative] beat sweep, [role:text-backing] title plate, or [role:background] surface. Do not create anonymous blobs, panels, abstract light ellipses, or shapes whose job cannot be named in viewer-visible terms.
paletteRoles: use the derive_palette output roles (bg-base, bg-accent, foreground, text-primary, accent) and record the contrast checks. Do not hand-pick alternate colors unless the reason and contrast are recorded. Avoid dark teal plus cyan/magenta unless requested. For ambient/aperture/glow backgrounds, instantiate get_background_grammar with at least three depth bands and at least three gradient falloff stops or a real Blur/SKSL/procedural texture so color boundaries do not read as hard bands.
backgroundGrammarPlan: record the selected base layer, depth layer A, optional depth layer B, motion slot, and which authored Elements will satisfy background/midground/foreground depth bands.
textPlatePlan: if text needs a backing plate, plan matching Start/Length, centered transforms, and padding for the named [role:text-backing] text/plate pair. Decorative light slashes, glass bands, and texture plates should be tagged [role:decorative], shot-limited, lower-Z background/surface elements, or non-rectangular/stroke/procedural treatments so quality review does not misclassify them as text backing plates.
effectIntentPlan: name the job of each effect chain: material texture, hierarchy separation, transition energy, color grade, or text legibility.
shotBreakdownPlan: derive the shot/beat count from duration × tempo before authoring (for 120-140 BPM, default 130 BPM: a 30s piece is about 65 beats). Plan a visible foreground event roughly every 1-2 beats, so a typical 30s high-tempo piece is tens of fine shots, not 6-8 coarse ones. Enumerate every shot with an index, Start, Length, primary focal point, primary message, role, planned foreground element count, and planned hold seconds. Subdivide long holds into distinct beats instead of letting one shot span many beats; only named final resolves and background textures may span multiple beats. This enumerated breakdown is the source of Element boundaries in Phase 1.
quantitativePlanSheet: turn shotBreakdownPlan into a numeric target table on the same four axes the quality tools measure later (shot count, edits per second, hold seconds per shot, foreground elements per shot), so the plan can be checked against actuals before export. The foreground-elements-per-shot target must be passed to evaluate_edit_quality / final_preflight as plannedForegroundElementsPerShot so metrics.layerDensity can compare authored layer density against the plan. Set every target with a deliberate 2-3× margin above the gate minimum, because execution always shrinks the plan — planning to the bare floor guarantees a below-gate result. Do not plan to the minimum; if a target only just clears the floor, raise it. Record:
Shot count: beats = duration_sec × BPM / 60; target ≥ 2-3× a coarse 6-8 baseline AND at least one distinct shot per 1-2 beats — take the larger. A 30s high-tempo piece plans roughly 30+ fine shots, never 6-8.
Edits per second: target timeline events/sec ≥ 2-3× metrics.tempo.RequiredTimelineEventsPerSecond; record the expected total timeline-event count that produces it.
Foreground elements per shot: target ≥ 2-3 foreground layers per shot (2-3× the single-element structural floor). Single-element shots are forbidden except a deliberately minimal named final resolve.
Hold seconds per shot: holds are a ceiling, so keep planned holds at 1/2-1/3 of the overlong-hold threshold (the 2-3× margin applied to an upper bound); normal foreground beats stay near 2-4 beats and only named resolves/background textures hold longer.
compositionPlan: define one primary focal point per enumerated shot and how grouping, alignment, scale, color, and repetition support it.
motionContinuityPlan: define reveal, development, and resolution phases plus how boundaries are bridged.
cameraPlan: Beutl's 2D pipeline has no scene camera, so camera (viewpoint) work must be planned explicitly as animated rig transforms — a piece where every shot is a static full-frame composition swapped by cuts reads as a slide deck, not motion graphics. For each shot or shot run, record the camera treatment: locked (deliberate static frame), push-in/pull-back (rig ScaleTransform around the focal point via TransformOrigin), pan/tilt drift (rig TranslateTransform.X/Y), whip-pan (fast rig translate + blur as a cut bridge), roll (rig RotationTransform), or parallax (per-depth-band rigs translating at different amplitudes, background slowest). A camera move is authored on a named [role:camera-rig]DrawableGroup that parents the shot's content; there is no automatic inverse, so moving the camera left/up means translating the rig right/down, and zooming in means scaling the rig up. Plan at least one non-locked camera treatment per shot run for motion-graphics briefs; an all-locked plan requires an explicit brief reason recorded in notes.
cutContinuityPlan: for every adjacent shot pair, record the planned visible bridge across the cut: an element crossing the cut, a camera move continuing across it (matched push-in or whip-pan from cameraPlan), a sweep, shared background continuity, overlapping transform/opacity ramp, or a deliberately documented hard-cut reason.
transformIntentPlan: for every rotated moving object, state whether motion is in screen space or local/rotated space. For screen-space motion, plan static orientation transforms before the animated TranslateTransform; for local-axis motion, state the intended axis and verification sample.
verificationSamples: choose at least three still times plus the motion/quality review sample set.
Objects
Element
IFlowOperator
DrawableGroup
DrawableDecorator
SoundGroup
Scene3D
Element.Id
Id
Keep element Start, Length, and layer/Z values consistent with the enumerated shot breakdown from shotBreakdownPlan.
When cameraPlan assigns a shot a camera move, structure that shot for it now with a named [role:camera-rig] rig Element, choosing one of two patterns so Phase 3 animates one rig transform instead of retrofitting per-element motion:
Portal (timeline) rig — preferred for multi-element shots: keep the shot's content as ordinary one-object Elements on contiguous ZIndex rows, and put the rig Element directly below them (lower ZIndex) with Objects = a PortalObject whose Count spans the content rows, then a DrawableGroup with empty Children. Count is an inclusive ZIndex span, not an element count: the portal pulls every active Element with ZIndex in rig+1..rig+Count into the group, so content keeps per-element Start/Length and stays visible as timeline layers to quality metrics. Keep the pulled rows ZIndex-contiguous and time-aligned with the rig — pulled Elements render ungrouped whenever the rig Element is not active. Fetch get_examples for insert-camera-rig-portal.
Nested rig — for a small cluster that shares the shot's timing: parent the shot's drawables inside the DrawableGroup's Children (nested children have no Start/Length of their own); the PortalObject's Count stays 0. Fetch get_examples for insert-camera-rig-push-in.
In both patterns the Objects array must contain the PortalObject entry immediately before the flow operator — a bare IFlowOperator is rejected by apply_edit. The same portal+flow grouping works for the other IFlowOperators (DrawableDecorator, SoundGroup for audio, Scene3D). Author the rig's transform statically at the shot's end-state framing in this phase, and keep locked full-frame background plates outside the rig (below the portal's ZIndex range) — that separation is also what makes parallax possible.
Author only static property values in this phase — no KeyFrameAnimation/KeyFrame (that is Phase 3) and no FilterEffect (that is Phase 2).
For default-aligned TextBlock and shape objects, treat TranslateTransform(0, 0) as centered in the scene; TranslateTransform(x, y) is an offset from the scene center. Do not use half-frame coordinates such as (960, 540) to center content in a 1920x1080 scene unless AlignmentX=Left/AlignmentY=Top was deliberately selected and source-grounded.
Author GeometryShape path coordinates with the artwork's top-left at (0, 0) (all coordinates non-negative): the drawn center lands at the alignment-resolved center plus the path bounds origin, so a path centered on (0, 0) renders up-left by half its size and scene-absolute coordinates shift by their full offset. When a path cannot be normalized, add a static TranslateTransform(-boundsX, -boundsY); measure_object_bounds reports geometryBoundsOrigin and preview_quality_risks flags uncompensated offsets as geometryPathOffset.
Use measure_object_bounds after creating or modifying layout-sensitive text, shape, and backing-plate pairs to confirm render-node size, scene-space center, transformed bounds, and padding before relying on still renders.
If you only need the required container shape, fetch the targeted insert-new-element-skeleton example; do not inspect a full-scene starter just to learn $type placement.
Apply the static layout in small apply_edit stages that map to your enumerated shot breakdown — background/surface, then primary structure/shapes, then typography, then text backing plates — using static property values only (no motion, no effects yet). Inspect valid, changes, validation, and createdIds after each stage before continuing. Pass quiet: true for large staged patches.
If apply_edit returns validation_rejected, unknown_type, stale handles, invalid animation discriminator tokens, or fallback-object guidance, fix the patch from get_schema/get_examples/read_document and retry only that stage. Do not invent shorthand values for colors, pens, animations, brushes, transforms, or effects. Do not silently fall back to cut-only timing after a keyframe failure unless the user explicitly accepts that reduced motion model.
For file sessions, call save_project after every successful major apply_edit before continuing to the next stage; omit session unless you are deliberately disambiguating an older session id. For LiveEditor sessions, save_project should report that saving is not required/supported; record that message instead of treating it as a blocker.
After each major stage, verify with read_document_summary. Compare every expected element name/role from your synthesized scene plan against the actual elements and revise before rendering unless the omission is recorded with a concrete reason. If any object has isFallback: true, stop rendering and fix the patch from schema because fallback objects are placeholders, not usable visuals. Audit every object whose summary shows nested transform animation plus a static rotation/skew/scale: confirm the TransformGroup.Children order still matches transformIntentPlan, and patch the order with $before, $after, or $index before rendering if it does not. Also audit object counts: any ordinary Element with multiple objects must be split unless it contains a named IFlowOperator.
Verify the static storyboard before adding any effects or motion:
staticLayout
final_preflight
objectId
wrapInGroup=true
DrawableGroup
IFlowOperator
evaluate_edit_quality
elementStructure
ZIndex
Opacity
BlendMode
Screen
Re-verify with render_storyboard/render_still and evaluate_edit_quality(videoType:<resolved>, staticLayout:true) that effects serve their named jobs and did not break readability. Motion-phase gates still do not apply yet.
AudioEffectGroup
DelayEffect
EqualizerEffect
CompressorEffect
LimiterEffect
SoundGroup
IFlowOperator
PortalObject
Decide the animation clock mode deliberately. With UseGlobalClock=false, KeyFrame.KeyTime is local to the owning timeline Element and should normally stay within 00:00:00..Element.Length. With UseGlobalClock=true, KeyFrame.KeyTime is a scene timeline time and should intersect the visible Element range.
For explicit keyframes, fetch get_examples for animate-float-property-keyframes when animating an existing object, or insert-new-animated-text-keyframes when creating a new animated text object. Copy the concrete KeyFrameAnimation<T> and KeyFrame<T> discriminators from the example instead of inventing animation type names.
Before authoring multiple keyframed properties, make a small local keyframe helper snippet in your draft from the MCP example and reuse that exact JSON shape for every animated Single, Boolean, Color, Size, or transform property. Do not hand-type or manually Unicode-escape the generic discriminator strings; invalid tokens around KeyFrameAnimation or KeyFrame mean the helper is wrong and must be rebuilt from get_examples before continuing.
If apply_edit.validation contains a Warning for relative keyframes outside the Element local range, treat it as a timing bug unless the user explicitly asked for that state. Fix by either converting the keyframes to local times or setting UseGlobalClock=true when scene timeline times were intended.
For rotated moving shapes, do not animate only TranslateTransform.X and assume it will travel along the rotated visual axis. If the intended path is screen-space horizontal/vertical drift with a tilted object, put static orientation transforms such as RotationTransform, SkewTransform, or non-animated ScaleTransform before the animated TranslateTransform in the TransformGroup. If the intended screen-space path is diagonal, animate both X and Y as a vector. If the intended local-axis path depends on transform order, record that explicitly and verify the order with a rendered still/motion sample before export.
analyze_audio_rhythm
readyForExport
final_preflight
render_still
evaluate_motion_variation
evaluate_edit_quality(videoType:<resolved>)
Export a short preview with export_video when an encoder is available; if export is unavailable, record the reason in notes. Control output size with crf (0-51, higher = smaller; raise it to ~28-30 for full-frame grain or other hard-to-compress content) or bitrate (bits/s, ABR) — the two are mutually exclusive. For a long export, pass background: true and poll read_render_job(jobId) the same way as render_storyboard. In the final report, surface any CC-BY-SA share-alike obligations, user-supplied URL assets recorded as license: "unverified", and all required attribution text from assets/manifest.json.
Save with save_project for file sessions after final revisions. For LiveEditor sessions, call read_operation_status or save_project once near the end if you need to report that the live edit is already applied but not file-saved by the toolkit.
Scene3D
Every shot needs one primary focal point. Supporting text, marks, panels, and effects should sit lower in scale, contrast, timing, or density.
Keep hero-scale typography to one primary message per beat; make captions, labels, and texture text visibly quieter before running evaluate_edit_quality.
Readability is timed: short-lived text must be short, split across beats, or held longer. At roughly 1.5s per shot, use 1-3 words for hero text and 2-4 words or compact symbols for supporting labels.
Maintain visual density: use layered background, foreground motion, accents, and typography/labels. A lone title over one moving shape is too sparse unless the brief asks for minimalism; verify with metrics.layerDensity, not only a manual object count.
For fast promos, add perceived information through short typography, repeated non-rectangular nodes, particles, strokes, texture, and accent motion rather than adding long text.
Use role tags consistently: [role:background] for full-frame surfaces, [role:text-backing] only for real measured text plates, and [role:decorative] for glass bands, slashes, glints, or rhythm marks that should not be interpreted as backing plates.
Large or animated foreground shapes must expose role, purpose, and motion intent in names. If a shape's job cannot be stated as beat sweep, scan texture, pulse reveal, transition wipe, text backing, or another concrete intent, remove it before export.
Do not use abstract foreground glint/glow/aperture/lens/glass ellipses as a quality shortcut. If viewers cannot parse what the shape represents without reading its layer name, replace it with strokes, particles, letter fragments, editor/timeline marks, masks, media, or procedural texture.
For ambient/aperture/glow backgrounds, avoid hard two-stop falloff. Use at least three gradient stops, widen alpha/color transitions, add a real Blur/SKSL texture, or replace the shape with procedural surface texture.
Use procedural texture when the concept is organic or atmospheric. A short SKSLScriptEffect on a broad shape is often better than many low-contrast blurred ellipses for heat, ink, glass, smoke, caustics, grain, or shimmer.
Every effect chain needs a named job: material texture, hierarchy separation, transition energy, color grade, or text legibility. Remove decorative stacks that do not serve one.
Give each major visual part a clear name in the patch so read_document_summary exposes the intended structure.
Treat your synthesized scene plan as a completion checklist. A final scene that omits planned accent/density elements without a recorded reason is incomplete.
After still renders, use evaluate_motion_variation; treat low adjacent-frame variation or persistent one-quadrant/sparse frame coverage as a failed self-check for motion graphics.
For motion graphics, a passing rendered-difference check is not enough when the document has no explicit animated properties. If evaluate_edit_quality reports animatedPropertyCount: 0, revise the edit to add deliberate animation on transform, opacity, typography spacing, brush, or effect parameters before export.
Numerical motion variation is necessary but not sufficient: planned elements must also be visibly present across representative stills, and text/title elements must be readable before export.
A still that is mostly a smooth background after the reveal phase is not dense enough even if evaluate_motion_variation passes.
Held title/logo sections need real continuous motion to pass evaluate_motion_variation (it requires ≥2% changed pixels at ≥48-per-channel delta between adjacent samples). Drift the held text and add a slow scale "breath"; a soft low-alpha ambient wash stays under the 48 delta and does NOT count as motion. tempoRhythm defaults to a 130 BPM target, so a calm 3-act brand/logo piece trips its minor advisories harmlessly — passesQualityGate/readyForExport stay true.
Long all-caps text, overloaded visual hierarchy, unreadable short-lived copy, rendered low text contrast (typographyContrast), foreground RectShape dominance, abstract decorative light ellipses, hard ambient gradient falloff, flat single-layer backgrounds, unclear or arbitrary animated shapes, ordinary Elements with multiple EngineObjects, sparse layer density/depth coverage, sparse high-tempo event density, long foreground event gaps, overlong high-tempo foreground holds, misaligned text backing plates, dark teal/cyan/magenta palettes, low-scoring hue-wheel palettes, dense effect stacks without a named job, repeated card shadows, low temporal variation, and unmotivated hard cuts are quality failures unless explicitly requested by the user.
evaluate_edit_quality.passesQualityGate must be true before final export for normal deliverables.