This document records what the Creator plugin API really does at runtime. The
published typings are @lottiefiles/creator-api-types 1.0.1. Version 1.0.1
fixed most of the omissions and dropped the members that never existed. Each
member appears once below, under the one status that describes it: verified
live, typed but not verified yet, live but untyped, or absent from the host
altogether. Runtime introspection inside real Creator established the
findings — own and prototype property enumeration, stamped into traces —
together with live replay verification during 2026-08-21/22. Treat this file
as the authority over the typings. Introspect first when you add a
capability; the record-start debug probe dumps the surfaces into traces.
The runtime type strings and 1.0.1 agree:
| Runtime | 1.0.1 says | Notes |
|---|---|---|
SHAPE_LAYER / CONTAINER |
SHAPE_LAYER |
layer holding shapes |
SCENE_LAYER |
SCENE_LAYER |
content in instance.scene.layers (shared between instances) |
TEXT_LAYER |
TEXT_LAYER |
see text section |
| image layers (from drops) | IMAGE_LAYER |
generic layer surface works |
RECTANGLE ELLIPSE POLYGON STAR PATH GROUP |
✓ | shape stack members |
History: 0.0.2 said Container for a shape layer and SCENE_INSTANCE for a
scene layer, and it named no text or image layer type.
Trace 2026-08-21T21-45-57-555 (rev .36): playback.begin with
atPlayhead: true returned frameOffset: 16 with the playhead parked at
frame 16, and the keyframes landed at 16/63 (recorded 0/47). Stagger cascaded
per selected target (+10 each).
All three are mutable plain properties on 1.0.1 LayerMixin, and the recorder
reads them every tick (engine/snapshot.ts PLAIN_PROPS): readable live in
136 record traces; writes pending one live trace at rev 2026-09-04.1. The
traced timelineOffset values include negatives, so its sign convention is
unverified.
1.0.1 also types shiftTo(frame: number) on LayerMixin. It shifts the
layer's timeline window — a TIME shift, not a move in the layer stack. This
plugin never calls it live. The engine's earlier guessed calls —
shiftTo(node) and shiftTo({to}) — threw, because neither argument is a
number. Both are gone: a coercible argument would have retimed the user's
layer instead of throwing.
delayLayer (sandbox/applier.ts) is the only writer. It moves startFrame
and timelineOffset by the same delta, reads both back, and reports a kept
value as a note. It never writes endFrame, and it notes an endFrame the
host moves on its own — that read is the evidence for the pending live check.
The live position-keyframe proxy surface (trace 2026-08-21T21-53-02-401, rev
.38) is easing, frame, id, remove, value — nothing else. The doc example in
0.0.2 that writes positionKeyframe.inTangent/outTangent does not reflect
the runtime, and 1.0.1's Keyframe<T> still has no tangent fields. See
limitations.md. The engine keeps its defensive read and write of
KfSnap.inTangent/outTangent so a future host lights it up automatically.
The same trace also verified rect.roundness. It IS a full Animatable
(addKeyframes, clearKeyframes, getKeyframeAt, getValueAt, isAnimated, keyframes, staticValue), and no other rounding-shaped property (radius,
cornerRadius, corners, modifiers) exists on the rectangle or its layer.
1.0.1 types Rectangle.roundness as Animatable<number>, which matches the
live proxy. But it is a dead proxy: its staticValue is 0 on every
rectangle ever seen, rounded or not, and a corner-radius drag produces no
snapshot change (limitations.md, 2026-08-23).
1.0.1 types these members, and runtime introspection confirms each one:
- Every node/shape:
moveBefore(sib),moveAfter(sib),bringToFront(),sendToBack(),getMatrix(),clone()(inserts copy after self, returns it — verified). 1.0.1 spreads them over three interfaces: the four reorder calls onLayerMixinANDShapeMixin,clone()onBaseNodeMixin, andgetMatrix(frame?)onTransformMixin, which only layers and groups extend. - Every
Animatable:clearKeyframes()(bulk animated→static),getValueAt(frame). node.data(PluginData) — this plugin's OWN per-node storage:get/set/delete/clear, plus ausedQuotanumber. Live, and inert: the rev .51 token hunt read it on every touched node and foundusedQuota: 0with null reads. It is storage, not a document surface, so no host value is ever readable through it (limitations.md).- Shape container (a shape layer or a group):
createTrimPath(),trimPathslist,createMask,createFill,createStroke(the specs are plain objects, and the API accepts gradient specs). 1.0.1 calls the interfaceShapeContainerMixin; 0.0.2 called itContainer. - Scene:
createShapeLayer(),createSceneLayer(),createImageLayer(),createTextLayer(),isNestableScene. - Text layer:
text,fontFamily,fontStyle,alignment(plain strings),fontSize(plain number), singularfill/strokepaints. - Scene layer:
break()(spills content into the parent scene — works). creator.selection.keyframes— live but EMPTY in practice (2026-08-25/26). It is a real own-property array oncreator.selection(the probe surface confirms it), but it readsarray(0)at everyselectionIntrospectionprobe, andselectedCountstayed 0 on every tick across five debug sessions. The getter never reflects the timeline selection, so the entry shape is still unknown. Rev .46 also listens for the typedselection:keyframesevent (feature-detected) and reportsevents: {supported, fired, lastCount}in the probe. SETTLED 2026-08-26: the event DOES fire (events.fired: 32, trace 2026-08-26T03-55-48), but its payloads are as empty as the getter —lastCount: 0throughout. Both typed routes exist, and neither carries the timeline selection; the ask is upstream. Seelimitations.md.
1.0.1 types these members, and the engine calls them, but no trace confirms one yet. Every call stays feature-detected, and each one has a fallback:
creator.utils.isLayer(node), and its siblingisShape(node), onUtilsAPI.sandbox/playback.ts#isLayerNodeusesisLayeronly when it answers with a boolean. The fallback readsstartFrame, aLayerMixinmember that no shape carries.Scenesettings writes:name,size(aSize—{width, height}),backgroundColor(Color | null, wherenullis a transparent scene),framerate, andduration. All five are plain mutable members.sandbox/playback.ts#applySceneSettingwrites one perset-scenestep and reads it back. It notes a value Creator keeps unchanged.Animatable.getValueAt(frame)as a playback baseline. The member is live-verified (see the list above); this use of it is not.sandbox/applier.ts#readBaselinereads the value atcreator.timeline.currentFramewhen the property has keyframes, because a keyframed property'sstaticValueis a stale leftover. It falls back tostaticValue.ShapeContainerMixin.createGroup(opts?: GroupOptions)— an object with ashapesarray, not a bare array.sandbox/applier.tscalledcreateGroup(created), which leftopts.shapesundefined and built an EMPTY group on the real host. It callscreateGroup({ shapes: created })since rev2026-09-06.2.SceneLayer.sceneand its layer factories —createShapeLayer,createTextLayer, andcreateSceneLayer.sandbox/playback.ts#nestByRebuildbuilds each copy of a nested layer with them (route 1). The shell'sscene.layersIS live-verified: traces2026-09-06T17-13-19-190_playback-Macro-5.jsonand2026-09-06T17-13-38-332_playback-Macro-5.jsonread it as an empty array (content=0). The factories on it are not verified. The fallback is route 2 below; with neither route the step notes a skip and leaves the layers alone.creator.createScene(opts)andScene.createSceneLayer({ scene })— route 2 of the same function. It copiesname,size,framerate, anddurationfrom the active scene, omits every undefined key, and then requires the shell to reference the scene it passed. The fallback removes both the scene and the shell, and the step notes a skip.Scene.isNestableScene— read on the scene route 2 creates.nestByRebuildaccepts any value exceptfalse, so a host that omits the member takes the route.Scene.remove()— the cleanup path of route 2.nestByRebuildcalls it on a scene it created and could not attach. A throw is a[nest] …breadcrumb alone: the step reports its skip either way.creator.clientStorage.usedQuota()— typed as a METHOD returningPromise<number>.sandbox/store.tsaccepts either the method or a number-valued property, caches the last reading, and reports it in thehelloresult. The host exposes the bytes USED and no maximum, so a plugin cannot measure the cap:saveMacroblames a full store only when the host's own message names the quota.
These members are live on the runtime surface, and 1.0.1 omits them:
- Every node/shape:
toJSON()(raw Lottie document — how the plugin reads fill and stroke opacityo) andgetBounds(). Scene:toJSON()andexport(). Caveat (2026-08-26, rev .51 host): on the live Creator build probed that day,node.toJSON()ANDscene.toJSON()returned bare{id, type}stubs — no shapes, no fills, no document at all (traces 2026-08-26T07-39-25/-52, 07-40-35, two independent sessions). The raw-document form is NOT guaranteed. Every reader oftoJSON()must treat an id/type stub as a normal, empty outcome — which they do. The readers are the per-fill opacity recovery insandbox/serialize.ts#collectPaintOpacitiesand the rev .51 token hunt. - Paint lists live at DIFFERENT DEPTHS per layer topology. A flat ellipse
keeps
fillsat the layer root; a group-based layer keeps them insideshapes[0](live: Circle 3 vs Ellipse 1, trace 2026-08-26T03-56-02). Pre-existing geometry shape nodes can lackfillsANDcreateFillentirely. Check the capability before removal, and resolve a recorded fill path by role (the nearest paint list from the root), never verbatim.
Nothing below exists on the live host. Do not spend a call on any of it.
0.0.2 declared these members, the runtime never had them, and 1.0.1 no longer declares them:
Scene.createSceneInstance(layers)— absent from the runtime, and no substitute works:createSceneLayer(layers)→ undefined, and the no-arg call creates an empty scene layer (it ignores the selection). CONFIRMED limitation (limitations.md).container.addFill/removeFill/addStroke/removeStroke/addMask/removeMask— absent. Create viacreateFill/createStroke/createMask; remove via the OBJECT's own.remove()(paints, strokes, masks, trims, keyframes, nodes all have it), which is how 1.0.1 types removal.MoveOptions/move()— gone; the real reorder methods are in the typed section above.
1.0.1 also drops the names Container, SceneInstance, PluginAPI, and
PluginEvent*.
These members no version ever declared, and the runtime does not have them either (checked against 1.0.1 on 2026-09-06):
- Per-paint opacity, on both the read side and the create side. 1.0.1 types
SolidPaintas{type, color, remove}andGradientPaintas{start, end, stops, remove};opacityappears only onColorStopsentries,Mask,LayerMixin, andGroup, andPaintOptionshas no such key.sandbox/applier.ts#paintSpecdoes not emit it: an unknown key makes the host reject the wholecreateFillwith✗ Invalid input, so the key lost the fill as well as the opacity. Seelimitations.md. TrimPath.mode. A 1.0.1 trim path isstart,end,offset, andremove, and nothing else.sandbox/serialize.tsreadsmodedefensively and omits it when it is absent, so a host that adds the member starts recording it with no change here. The member is NEVER live-verified.- Keyframe spatial tangents, effects, and ungroup. See the tangent section
above, and
limitations.mdfor the other two.
Still wrong in 1.0.1: UIAPI.onMessage(pluginMessage: unknown): void
declares a member that RECEIVES a message, but the runtime takes a callback.
sandbox/plugin.ts passes a function, and it compiles only because a function
is assignable to unknown.
Other documents cite these items by number, so keep the numbering stable:
- Keyframe ids are not identity. Creator reassigns
kf.idon a value edit and recycles ids from a pool. Diff and apply strictly by frame. - Creator silently ignores
addKeyframesat frame 0 on a not-yet-animated property (it may writestaticValueinstead). The engine verifies the add, seeds a sentinel at frame+1, retries, then removes the sentinel. getKeyframeAt(0)can return a truthy phantom on a keyframe-less property. The engine never consults it whenkeyframes.length === 0.- The host discards
staticValuewrites while keyframes EXIST — not whileisAnimatedis true, because that flag can stay true with zero keyframes. Guard onkeyframes.length > 0. PathDataand its points are getter-based. The fields are invisible toObject.keysand to the generictoJson, so read them structurally:closed, and the per-pointvertex/inTan/outTanvectors.- Per-fill opacity is unreachable through paint proxies, which expose
only
color/type/remove; colors are RGB, with no alpha. The documentoexists intoJSON()and recording tries to recover it from there — on a host that returns the{id, type}stub (see the caveat above) it finds nothing. There is no write path either way. - Duplicate detection must ignore the layer's own transform. Creator offsets ⌘D copies, and the copies inherit live rotation.
createSceneLayer()creates an EMPTY scene layer and does not consume the selection. Since rev2026-09-07.1the engine rebuilds the selected layers inside the shell's own scene, and then removes the originals.- Fill and stroke on text layers are singular objects, not lists — the engine models them as one-item lists.
- Host events: our introspection found only
selection:nodes/selection:keyframes/message. 1.0.1 types those three pluschange:theme,change:scenes,change:images, andchange:fonts. No node-change event exists, so polling plus diff is the only recording mechanism.change:scenes,change:images, andchange:fontsare typed in 1.0.1 and not verified live. The theme route is under probe: 1.0.1 typescreator.ui.themeand thechange:themeevent, and the ui-library docs document the same pair (ThemeProvider sync). AThemeTokenscarriestokens,themeName, andisLight, andsandbox/theme.tsforwards all three to the panel. It implements that relay fully feature-detected — NEVER live-verified, because our introspection predates the probe. If a trace shows the frame matching Creator's theme, the event exists; move this note accordingly.