References and ids
Why scripts written with names keep working after a rename: the id model behind layer(), prop(), control() and comp().
Scripts reach into the scene through four lookups - layer(), prop(), control() and comp() - plus the handles they return. This page explains the one model behind all of them: you author and read names; the document stores ids.
Names on the surface, ids underneath
Every entity has two labels. The NAME is what people see and edit - Square, Rig, Speed. The ID is a stable address the engine mints once and never changes - layer_2, xcontrol_10, comp_1. References in scripts are id-bound in storage, always, which is why renaming anything is safe by construction: there is nothing name-shaped in the saved script to go stale.
The editor keeps the ids out of your way:
- Completions. With the caret inside the string of
layer(",control("orprop(", the editor lists the scene's entries BY NAME and inserts the id when you pick one. Forprop("it is a two-step: pick the layer, then pick one of its property keys. - Display. Applied scripts render the current name over each stored id - you read
layer("Square")while the document holdslayer("layer_2"). Hover shows both. Rename the layer and the display updates in place. - The
@pick whip. Type@in the IDE and drag onto a property row, a layer or a control; the canonical id reference drops in at the caret, already wearing its name. - Healing on Apply. Type a name by hand -
layer("Square")- and Apply (or Format) rewrites it to the id before storing, with a quiet note saying what it resolved. A name matching NOTHING keeps a plain diagnostic; a name matching SEVERAL entries gets a typed diagnostic with a pick-one quick fix per candidate. An id always wins over a name: a layer literally named "layer_1" can never hijack a reference to the reallayer_1. - A dangling id - the one way to break a reference is deleting its target - renders in the error tint, and the run reports the typed
reference_missing.
The property id grammar
A property id is the owning layer's id, a colon, then the property key:
layer_2:transform.position
The key half comes from the generated property vocabulary - the same keys the timeline's rows carry. Property ids appear in three places: the outputs declaration, set() targets and prop() lookups. Handles know their own address too: thisLayer.position.id answers the full id string.
What resolves where: the closure rules
Lookups are CLOSED over the comp being evaluated, which keeps every run deterministic and self-contained:
layer(id)finds layers of the evaluating comp only.control(id)finds expression controls on the evaluating comp's layers only (control ids are document-unique, so there is never ambiguity). A layer's OWN controls are also reachable aslayer.control(id), which answers null instead of raising when absent.comp()answers the evaluating comp;comp(id)for any OTHER id raisesreference_missing. A project script whose outputs span several comps runs once per comp - cross-comp influence flows through the values it writes, never through reads.- Component scripts are closed over their component: they see the definition's internal comp, its layers and
thisComponent- never the host document.project.numCompsreports 1 inside one.
Failing lookups raise the typed reference_missing rather than returning undefined, so a broken reference is a visible status, never a silent NaN somewhere downstream.
Reading and writing
Reads go through handles: prop(id).value, layer(id).position.value, control(id).value. Every read resolves at the current frame with keyframes sampled and other scripts' outputs applied. Every read answers the WIRE value of the row's kind: numbers in wire units (degrees, percent, pixels, decibels), points as [x, y], colours as [r, g, b, a] in 0..1, strings verbatim (text, enums; a layer reference as the id or null), booleans, a dropdown control as its item index, the list and object kinds (gradient stops, curve points, mask paths, subpath lists, width profiles, corner radii, crop transforms, font references) as their wire arrays and objects, and an asset reference as the asset id (null when unset). The property vocabulary's units column is that contract kind by kind, a control key reads by its control's kind, and what you read is the shape set() accepts back. Writes go ONLY through the output mechanism - return for a single-output script, set() otherwise - and only to declared outputs within the current drive set. The DOM has no assignable members: layer("Square").position = ... is not a thing, which is precisely why evaluation stays predictable.