Project scripts
How a script declares the properties it drives, when the engine calls it, and the scheduling rules that keep evaluation predictable.
Everything you need to know about how scripts are shaped, where their values go and when they run. (Component-embedded scripts follow the same rules inside their component's closed world; the differences are collected at the end.)
The shape
export const outputs = ["layer_1:transform.opacity", "layer_2:transform.opacity"];
export function frame() {
set("layer_1:transform.opacity", 40);
set("layer_2:transform.opacity", 80);
}
outputsis a STATIC declaration - a plain array literal naming everything this script may drive. An entry is a property id, or a NAMED GROUP gathering several -{ group: "fades", targets: [...] }- which theoutputsglobal then exposes asoutputs.fades, an array of live Property handles for fan-out writes (a grouped script leaves the module-level declaration out so that global stays visible; see outputs). The panel reads the declaration, the timeline's driven-row indicators read it, and the pick whip appends to it.frame()runs once per evaluated frame. With EXACTLY ONE output, its return value drives that output; with several, callset(target, value)per target - a returned value is only used when nothing was set.- The top level runs once at compile: constants, helpers and module-level handles (
const speed = control("Speed");) live there. A handle's.valuestill reads per frame - handles are live, not snapshots.
Writes are a contract
set() refuses, with typed errors, anything that would make evaluation unpredictable:
- a target outside
outputs-undeclared_output; - a key outside the current drive set, a value of the wrong shape, or an opacity outside 0 to 100 -
type_mismatch; - a target string that is not a property id at all -
reference_missing.
Coercion is friendly where it is safe: positions accept [x, y] arrays or {x, y} objects, colours accept [r, g, b, a] or a hex string. A plain return; from a single-output script drives nothing and leaves the base value showing - the honest no-op.
Scheduling: document order, demand-driven
Per comp and frame, every ENABLED script whose outputs target that comp evaluates once, in the panel's document order. Reads make the order safe: if script B reads a property script A drives, A is pulled first and B sees A's output - whatever the panel order says. The one impossibility is a loop: two scripts reading each other's outputs cannot both be first, so the read that closes the loop reports the typed cycle (its chain names who was reading whom) and falls back to the pre-expression value. A script reading its OWN output gets the base value through value - that is the designed way to build on your own property.
A script's value global and thisLayer refer to its FIRST output - the pre-expression (panel or keyframed) value and the layer that owns it.
Cross-comp scripts
Outputs may span comps; the script then runs once per comp it targets, each run closed over that comp (see the closure rules). Reads inside each run see that comp only, so cross-comp behaviour flows through written values. Precomp and component-instance layers evaluate their inner comps with the same rules - a script driving a layer inside a precomp drives it wherever that precomp is placed.
Enabled, disabled, and the panel
The Scripts panel's toggle is the one switch: a disabled script evaluates nowhere - editor, export scan and farm alike - and every property it drove returns to its base look. The panel's status chip carries each script's EVALUATED state at the current frame: ok, error or budget (with the typed code and message), disabled, or unavailable for a script whose outputs target nothing that exists.
The export gate
Publishing or exporting scans every script over the full frame range first - the same fold the farm will run - and reports the first failing frame per script before any render is queued. A script error on the farm itself is a CONTENT failure that names the frame, the script and the typed code, so a failed render tells you exactly where to look.
Component scripts
A component definition carries its own scripts, published and imported WITH the component - a library component's behaviour travels as part of it. They differ from project scripts in scope only:
- they run once per placed INSTANCE (and once for the definition context when you edit the component itself), reading that instance's effective control values through
thisComponent; - their outputs target the definition's internal layers, and their closure is the definition - they cannot see the host document;
- the panel lists them grouped under their component, with enable state carried by the definition.
The Component page's worked examples show the per-context behaviour end to end.