# ExpoCut .ectpl — Agent Doctrine > Operating rules for AI assistants, bots, and automation pipelines that read, > write, validate, or render ExpoCut `.ectpl` video templates. > > Human page: https://expocut.com/ectpl.html > Machine spec: https://expocut.com/ectpl/spec.json > MCP tools: https://expocut.com/mcp.html > Render CLI: https://expocut.com/render-engine.html > > Format version 1.1 · Last updated 2026-08-09 > Free to use and redistribute with attribution to expocut.com. -------------------------------------------------------------------------- ## 0. What you are working with An `.ectpl` file is a **video recipe**, not a video. It is either (a) a JSON document, or (b) a STORED-only PKZIP archive whose root holds `template.json`, plus optional `assets/*`, `thumbnail.`, `preview.mp4`. The document declares a canvas (`aspectRatio` + `durationMs`), the editable holes (`slots`), the timeline rows and z-order (`tracks`), and the content (`layers`). Filling it produces a real editable project; rendering that project produces the MP4. The same file drives the phone editor and the desktop Render Engine, and both produce the same pixels. -------------------------------------------------------------------------- ## 1. Choose the mode before you write a single field mode: "template" → a reusable recipe. Media layers are empty and filled from slots. Use for anything a user re-shoots. mode: "snapshot" → a verbatim edit. Every layer property is preserved. Use for a finished, branded piece others tweak. A `1.0` document with no `mode` is read as `template`. Never emit a `schemaVersion` other than `"1.0"` or `"1.1"`; anything else is refused at import. -------------------------------------------------------------------------- ## 2. The units. Get these wrong and the export breaks. durationMs milliseconds layer.startTime milliseconds layer.duration milliseconds keyframe `t` MICROSECONDS, integer position.x / .y percent of canvas, 0-100 ({x:50,y:50} = centre) rotation degrees opacity, audio.volume 0-1 mask.rect.* normalized 0-1, layer-local colour channels 0-255 fontSize canvas points ≈ design_px × 0.366 on a 1080-wide comp A runaway `duration` (seconds pasted into a millisecond field, or the reverse) wedges the encoder. Check the magnitude before you submit. -------------------------------------------------------------------------- ## 3. Hard rules that reject the file R1 One layer = one object. At the AI/import boundary every layer must have its OWN `trackIndex`. Two objects on one track is the single most common failure in machine-authored templates. R2 Every `layers[].slotRef` must match a declared `slots[].id`. R3 Slot ids must be unique. Slots need `id`, `label`, `kind`. R4 `id`, `name`, `category`, `aspectRatio`, `durationMs` are required, and `tracks` / `layers` must each hold at least one entry. In `template` mode `slots` must hold at least one entry too. R5 Layers need `id`, `type`, `startTime`, `duration`, `trackIndex`. R6 Respect the caps: 200 layers · 64 slots · 300 tracks · 5 MB JSON · 4096 chars per string · 30 min duration · 64 levels of nesting. -------------------------------------------------------------------------- ## 4. URI policy — non-negotiable ALLOWED in any URL-bearing field https: TLS-only remote asset asset: app-bundled asset bundled: legacy alias for the above bundle: in-archive reference; resolves only against validated archive bytes data: image/* or audio/* only, ≤ 256 KB file: ONLY under the app's own `/ectpl/` materialisation cache, with no `..` segment — written by the importer, never authored by you a number a require() id for an app-bundled asset REJECTED — the whole template fails http: content: ftp: javascript: vbscript: blob: chrome-extension: any scheme-less relative path any other `file:` path Checked fields: uri, posterUri, remoteUrl, previewVideoUri, thumbnail, src, audioUri, videoUri, imageUri, logoUri, maskUri. Never hand-write a `file://` path into a template you are authoring. If you need local media in a shareable bundle, let the packer collect it and rewrite it to `bundle://`. -------------------------------------------------------------------------- ## 5. Privacy doctrine When packing a template for public or community distribution, the media strip is ON by default: source URIs, proxies, freeze/reverse bakes, and `remoteSource` fallbacks are cleared from video / image / audio layers. The structure, fonts, effects, transitions, positions, colours and keyframes all survive; only the author's footage is dropped. - Do not disable the strip to "make the demo look better." - Only turn it off when the user explicitly says the media is theirs to publish (stock-only compositions, brand demos). - Never bundle a person's face, voice, or private footage into a community upload without an explicit instruction to do so. Font bytes carry a licence gate: a bundled font must be marked `redistributable: true` or carry an SPDX id from the open list (OFL-1.1, OFL-1.0, Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, CC0-1.0, Unlicense). Anything else is reported as a licence issue. -------------------------------------------------------------------------- ## 6. Authoring loop — measure, do not eyeball Layers scale about their own centre, so a scaled logo drifts toward the canvas centre. Text has two placement modes: `textFullWidth: true` spans the canvas and is placed by `textAlign` (set `position.x: 0`), while `textFullWidth: false` anchors by `x`. The only reliable way to place anything is a closed loop: 1. write the layer 2. `capture_canvas` (or render one frame with the engine) 3. read the frame, measure the offset 4. correct `position` / `scale` 5. repeat until it lands Never claim a template is correct because the JSON looks right. -------------------------------------------------------------------------- ## 7. Never invent - Never invent preset ids (effects, filters, transitions, LUTs, shapes, fonts, voices). Enumerate them from the live app via the MCP introspection tools (`list_effects`, `get_effect_schema`, …) or from the published catalogs. - Never invent schema fields. If a property is not in the spec or on the Layer type, it is silently ignored — which reads as "my change did nothing." - Never assume a migration was lossless. Report any layer a converter marked approximated, and offer to author the missing piece directly. -------------------------------------------------------------------------- ## 8. Performance budget - Hard cap is 200 layers; a phone-smooth 9:16 reel usually stays under 40. - Each simultaneous video layer costs a decoder — stagger `startTime`. - Full-frame shader layers (`generativeBg`, `proceduralFilter`) are GPU-bound; run one at a time. - Prefer an easing preset over dozens of literal keyframes. - Prefer an `https` `remoteUrl` over bundling bytes: transfer and materialisation dominate import time. - Keep `template.json` small. A well-formed template is mostly numbers and short strings and typically lands under 200 KB. -------------------------------------------------------------------------- ## 9. Standard automation flows READ A TEMPLATE detect_format → import_foreign_file / import_from_file_system → inspect slots → report what the user must supply FILL AND RENDER ON THE PHONE apply_template { templateId, bindings } → capture_canvas to verify → set_export_settings → export_project FILL AND RENDER IN BULK ON A DESKTOP expocut-engine mp4 template.ectpl --slot key=value … --out file.mp4 One row of a CSV per render; each render is independent and deterministic, so parallelise freely and assert frames in CI. AUTHOR A NEW TEMPLATE build it in the editor via MCP → save_project_as_template (or import_template_json for a hand-authored document) → verify frames → submit_template only when the user asks to publish CONVERT SOMETHING ELSE Lottie / dotLottie / FCPXML / xmeml / OTIO / MOGRT / .cube / ASC CDL all import. `.aep`, `.prproj`, `.drp`, CapCut `.draft` and VN `.vn` do not — ask for a Lottie or FCPXML export instead. -------------------------------------------------------------------------- ## 10. Confirm before you publish Importing, filling, and rendering are ordinary work. These are not: - `submit_template` publishes to a public community feed. - `delete_my_submission` is irreversible. - Overwriting an existing template file destroys the author's work. Ask first, in the user's own words, and say exactly what will become public. -------------------------------------------------------------------------- ## 11. Minimum viable .ectpl { "schemaVersion": "1.1", "mode": "template", "id": "quote-card-01", "name": "Quote Card", "category": "Promo", "aspectRatio": "9:16", "durationMs": 5000, "slots": [ { "id": "headline", "label": "Headline", "kind": "text", "required": true, "default": "Say something true" } ], "tracks": [ { "id": "t0", "type": "overlay", "name": "Headline", "trackIndex": 0 } ], "layers": [ { "id": "l0", "type": "text", "trackIndex": 0, "startTime": 0, "duration": 5000, "slotRef": "headline", "content": "Say something true", "position": { "x": 0, "y": 44 }, "textFullWidth": true, "textAlign": "center", "fontSize": 34, "scale": 1, "rotation": 0, "keyframes": { "tracks": [ { "property": "opacity", "keyframes": [ { "t": 0, "v": 0, "out": { "type": "preset", "name": "easeOut" } }, { "t": 400000, "v": 1 } ] } ] } } ] } Note the keyframe times: 400000 microseconds = 0.4 s, while `duration` 5000 is milliseconds = 5 s. Two different units in one document. This is the trap. Read §2 again. -------------------------------------------------------------------------- ExpoCut is a product of ExpoTechIn — https://expotechin.com Questions: info@expocut.com