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
    / Objects

    Property

    The property handle's members: the address, the authored value and the keyframe list behind it.

    A property handle.

    Table of contentsidWorked example: The full address in handkeyWorked example: Which property is thisvalueWorked example: Link one property to anotherWorked example: Read a colour, write a darker onenumKeysWorked example: Counting keyframeskeyAtWorked example: First key, last keynearestKeyWorked example: Snapping to the nearest key

    id#

    Type: string (field).

    The property id (layer_id:key).

    Worked example: The full address in hand#

    The scene. One pale card resting near the centre of the stage with its rotation keyframed from 0 to 90 degrees - a still scene for reading a property's identity.

    • The comp Plaque: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 120 by 80, filled #e8e6e3.
      • Keyframe transform.rotation at frame 0: 0 (linear).
      • Keyframe transform.rotation at frame 96: 90 (linear).

    The script. Add a project script named Address check and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      const pos = thisLayer.position;
      return pos.id === "layer_1:transform.position" ? 70 : 0;
    }
    

    Line by line:

    • Lines 4-5: Every Property handle knows its own id - the layer id, a colon, then the key. That id is the exact string outputs declare and set() accepts, so a handle can hand its address onwards. The check passes and the Card rests at 70.

    What you see. The card rests at 70 percent opacity - the visible receipt that a Property handle's id really is its layer id plus its key.

    The full address in hand
    The card rests at 70 percent opacity - the visible receipt that a Property handle's id really is its layer id plus its key.

    key#

    Type: string (field).

    The property key.

    Worked example: Which property is this#

    The scene. One pale card resting near the centre of the stage with its rotation keyframed from 0 to 90 degrees - a still scene for reading a property's identity.

    • The comp Plaque: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 120 by 80, filled #e8e6e3.
      • Keyframe transform.rotation at frame 0: 0 (linear).
      • Keyframe transform.rotation at frame 96: 90 (linear).

    The script. Add a project script named Key length and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      const key_name = thisLayer.rotation.key;
      return key_name.length * 4;
    }
    

    Line by line:

    • Line 4: key is the property's name on the wire - "transform.rotation", the same spelling the timeline's rows and layer.prop() use. Handles passed around as data can always say which property they are.
    • Line 5: Eighteen characters, times four: the Card rests at 72 percent - a visible receipt for the read.

    What you see. The card rests at 72 percent opacity, four percent for each character of the key name transform.rotation.

    Which property is this
    The card rests at 72 percent opacity, four percent for each character of the key name transform.rotation.

    value#

    Type: T (accessor).

    The resolved value (overlay or keyframed) at the current frame.

    A read answers the WIRE value of the row's kind, exactly as the property vocabulary's units column states it: 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, null when unset), booleans, a dropdown control as its item index, corner radii / gradient stops / curve points / mask paths / subpath lists / width profiles / crop transforms as their wire arrays and objects, a font reference as its wire object (null when unset) and an asset reference as the asset id (null when unset). A control key reads by its control's kind. What you read is the shape set() accepts back, so a value can round-trip untouched.

    Worked example: Link one property to another#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

    The script. Add a project script named Property link and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const leader_pos = prop("Leader:transform.position").value;
      return add(leader_pos, [0, 100]);
    }
    

    Line by line:

    • Line 4: value on a Property handle is the resolved value at the current frame: keyframes are sampled, and if another script drives the property, its output is what you read. The Leader is keyframed to march across the stage, and this read follows every step.
    • Line 5: One vector add hangs the Follower exactly 100 pixels below the Leader - a property link in two lines, no keyframes copied.

    What you see. The follower marches in perfect lockstep 100 pixels below the keyframed leader - a live link, not a copy: re-time the leader's keys and the follower obeys.

    The follower marches in perfect lockstep 100 pixels below the keyframed leader - a live link, not a copy: re-time the leader's keys and the follower obeys.

    Worked example: Read a colour, write a darker one#

    The scene. A teal leader square keyframed to cross the stage left to right, with an orange follower resting below its start point.

    • The comp Leaders: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Leader: a solid layer, at [80, 140], 40 by 40, filled #2ec4b6.
      • Keyframe transform.position at frame 0: [80, 140] (linear).
      • Keyframe transform.position at frame 96: [560, 140] (linear).
    • Follower: a solid layer, at [80, 240], 40 by 40, filled #f2a833.

    The script. Add a project script named Shade match and paste:

    export const outputs = ["layer_2:content.fill_colour"];
    
    export function frame() {
      const leader = prop("Leader:content.fill_colour").value;
      return [leader[0] * 0.5, leader[1] * 0.5, leader[2] * 0.5, 1];
    }
    

    Line by line:

    • Line 4: A read answers the WIRE value of the row's kind: a colour comes back as [r, g, b, a] in 0..1, never a hex string and never a bare number. The Leader's teal #2ec4b6 reads as [0.18, 0.769, 0.714, 1].
    • Line 5: Halving the three colour channels and keeping alpha at 1 builds a darker teal in the very same shape, and the single-output return writes it to the Follower's fill colour - a colour link that follows any repaint of the Leader.

    What you see. The follower wears a darker shade of the leader's teal (#17625b) for the whole comp - recolour the leader and the follower re-shades itself.

    The follower wears a darker shade of the leader's teal (#17625b) for the whole comp - recolour the leader and the follower re-shades itself.

    numKeys#

    Type: number (accessor).

    M10 DOM2-2: the track length (0 while un-keyed; projections read their one list track - the layerTrackLength rule).

    Determinism: pure.

    Worked example: Counting keyframes#

    The scene. One pale card near the centre with its rotation keyframed from 0 to 90 degrees across the comp, for scripts that read and reshape keyframed motion.

    • The comp Keyed: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 120 by 80, filled #e8e6e3.
      • Keyframe transform.rotation at frame 0: 0 (linear).
      • Keyframe transform.rotation at frame 96: 90 (linear).

    The script. Add a project script named Key counter and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      const rotation = prop("Card:transform.rotation");
      const position = prop("Card:transform.position");
      set("layer_1:transform.opacity", 40 + rotation.numKeys * 10 + position.numKeys);
    }
    

    Line by line:

    • Lines 4-5: Two handles on the same card: the rotation track carries the fixture's two keys, the position track carries none.
    • Line 6: numKeys reads the track length, 0 while un-keyed, so the opacity lands on 40 + 20 + 0. Projections follow the same rule by reading their one list track.

    What you see. The card fades to 60 percent: two rotation keys counted at ten each over the 40 base, and the un-keyed position track counted zero.

    The card fades to 60 percent: two rotation keys counted at ten each over the 40 base, and the un-keyed position track counted zero.

    keyAt#

    keyAt(index: number): Keyframe | null
    

    M10 DOM2-2: the keyframe at a 0-based index; null out of range (the soft-miss rule). Named keyAt because the shipped key member is the property-key string - the dossier's key(i) spelling, corrected.

    Determinism: pure.

    The method is keyAt, not key(i): the shipped key member is the property-key string and could not be repurposed. keyAt is 0-based and answers null out of range, the same soft-miss rule the layer traversal follows.

    Worked example: First key, last key#

    The scene. One pale card near the centre with its rotation keyframed from 0 to 90 degrees across the comp, for scripts that read and reshape keyframed motion.

    • The comp Keyed: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 120 by 80, filled #e8e6e3.
      • Keyframe transform.rotation at frame 0: 0 (linear).
      • Keyframe transform.rotation at frame 96: 90 (linear).

    The script. Add a project script named Key spread and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      const rotation = prop("Card:transform.rotation");
      const first = rotation.keyAt(0);
      const last = rotation.keyAt(rotation.numKeys - 1);
      const past_the_end = rotation.keyAt(2);
      const spread = last.value - first.value;
      set("layer_1:transform.opacity", past_the_end === null ? spread + 5 : 0);
    }
    

    Line by line:

    • Lines 5-6: keyAt is 0-based: index 0 is the first key (frame 0, value 0) and numKeys - 1 the last (frame 96, value 90). Each answer is a read-only Keyframe carrying the wire shape verbatim.
    • Line 7: Index 2 is past the end of a two-key track, so the answer is null, never a throw.
    • Lines 8-9: The value spread (90 degrees) drives the opacity, plus five only because the out-of-range read behaved.

    What you see. The card settles at 95 percent opacity: the 90 degree spread between the first and last keys plus the five point proof that keyAt(2) answered null.

    The card settles at 95 percent opacity: the 90 degree spread between the first and last keys plus the five point proof that keyAt(2) answered null.

    nearestKey#

    nearestKey(frame: number): Keyframe | null
    

    M10 DOM2-2: the closest key by |frame| distance (ties round EARLIER, pinned); null on an un-keyed property.

    Determinism: pure.

    Worked example: Snapping to the nearest key#

    The scene. One pale card near the centre with its rotation keyframed from 0 to 90 degrees across the comp, for scripts that read and reshape keyframed motion.

    • The comp Keyed: 640 by 360 at 25 fps, 100 frames (4 seconds).
    • Card: a solid layer, at [320, 180], 120 by 80, filled #e8e6e3.
      • Keyframe transform.rotation at frame 0: 0 (linear).
      • Keyframe transform.rotation at frame 96: 90 (linear).

    The script. Add a project script named Nearest key and paste:

    export const outputs = ["layer_1:transform.opacity"];
    
    export function frame() {
      const rotation = prop("Card:transform.rotation");
      const tie = rotation.nearestKey(48);
      const late = rotation.nearestKey(90);
      set("layer_1:transform.opacity", tie.frame === 0 && late.frame === 96 ? 70 : 0);
    }
    

    Line by line:

    • Line 5: Frame 48 sits exactly halfway between the keys at 0 and 96. Ties round EARLIER, so the answer is the frame 0 key.
    • Line 6: Frame 90 is six frames from the late key and ninety from the early one, so the frame 96 key wins.
    • Line 7: Both answers drive the proof value; an un-keyed property would have answered null instead.

    What you see. The card rests at 70 percent opacity, proving the halfway tie snapped to the earlier key and the late probe snapped forward.

    The card rests at 70 percent opacity, proving the halfway tie snapped to the earlier key and the late probe snapped forward.