Nubu
  • Features
  • How it works
  • Pricing
  • Integrations
  • FAQ
  • Contact
  • Docs
  • Blog
  • Log in
  • Log inSign up
    Nubu

    Creative automation for teams that ship campaigns, not busywork.

    Try for free
    Product
    FeaturesNubu MotionHow it worksPricingIntegrationsFAQ
    Company
    AboutContactDocsBlogLog in
    Legal
    Privacy policyTerms of serviceData deletion
    © 2026 Bear Studios · Nubu
    Introduction
    Templates overviewInstalling Nubu BuilderAdding template compsAdding propertiesAdding footageSetting defaultsExporting your templateUploading a template
    Flows overviewAvailable nodesAdding a nodeConnecting nodesWorking with dataTemplates and outputsUsing AI nodesBuilding a creative
    CampaignsTemplatesAssetsGlossariesRendersSettings
    Connecting Meta AdsConnecting Google AdsConnecting OpenAIConnecting Gemini AI StudioConnecting Anthropic
    AI Assistant overviewPrompt examplesAssistant settings and controls
    Nubu Motion scripts: the reference
    OverviewQuick startDifferences from After EffectsDeterminism
    JavaScript supportReferences and idsProject scriptsState and timeBudgets and errorsFormatting values
    GlobalTime conversionInterpolationVector mathsRandom and noiseColour conversionOther maths
    LayerCompPropertyKeyframePathGeometryProjectComponentConsoleComponentControlExpressionControl
    The property vocabularyAnimators propertiesAudio propertiesContent propertiesContents propertiesEffects propertiesExpression controls propertiesFills propertiesInstance controls propertiesLayer propertiesLayout propertiesMasks propertiesStrokes propertiesText style propertiesTransform propertiesTrim path properties
    Error codesBudgets
    Changelog
    All docs
    Motion Scripting
    / Language

    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(" or prop(", the editor lists the scene's entries BY NAME and inserts the id when you pick one. For prop(" 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 holds layer("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 real layer_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 as layer.control(id), which answers null instead of raising when absent.
    • comp() answers the evaluating comp; comp(id) for any OTHER id raises reference_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.numComps reports 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.