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

    ExpressionControl

    The script half of the controller pattern: reading a slider, checkbox, colour, point, dropdown, angle or layer control.

    An expression control handle (the controller pattern's script half). value and a dropdown's text are LIVE getters (the track's sample at the fold frame, else the base) in WIRE units: slider/angle number, dropdown the 0-based index, point [x, y], colour [r, g, b, a] in 0..1, checkbox boolean, layer a Layer handle or null.

    Table of contentsidWorked example: Stable ids under any namenameWorked example: Read the control's labelkindWorked example: One script, many control kindslayerWorked example: Find the control's homeenabledWorked example: The eye greys, scripts still readminWorked example: The slider's floor as datamaxWorked example: The slider's ceiling as datastepWorked example: Snap to the slider's gridunitWorked example: Honour the declared unititemsWorked example: The dropdown's menu as datavalueWorked example: The live dialtextWorked example: The dropdown's words

    id#

    Type: string (field).

    The control id ("xcontrol_").

    Worked example: Stable ids under any name#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Id proof and paste:

    export const outputs = ["layer_1:transform.rotation"];
    
    export function frame() {
      const start = control("Start angle");
      return start.id === "xcontrol_3" ? 30 : 0;
    }
    

    Line by line:

    • Lines 4-5: Expression controls carry document-unique ids. The editor SHOWS the control's name in the control() call and heals a typed name into the id on Apply, but what is stored - and what id answers - is always the stable id, so renaming Start angle can never break this script. The check passes and the Sun turns 30 degrees.

    What you see. The sun square rests turned 30 degrees - the receipt that the control's stored identity survives any renaming.

    The sun square rests turned 30 degrees - the receipt that the control's stored identity survives any renaming.

    name#

    Type: string (field).

    The display name (the kind's default label when unnamed).

    Worked example: Read the control's label#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Label glow and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const speed = control("Speed");
      return 40 + speed.name.length * 10;
    }
    

    Line by line:

    • Lines 4-5: name is the label the control tile shows - "Speed", five characters, resting the Planet at 90 percent. A control that was never named answers its kind's default label (a bare checkbox reads "checkbox").

    What you see. The planet rests at 90 percent opacity - forty as the floor plus ten per letter of the Speed control's name.

    The planet rests at 90 percent opacity - forty as the floor plus ten per letter of the Speed control's name.

    kind#

    Type: 'slider' | 'checkbox' | 'colour' | 'point' | 'layer' | 'dropdown' | 'angle' (field).

    The control kind.

    Worked example: One script, many control kinds#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Kind dispatch and paste:

    export const outputs = ["layer_2:transform.scale"];
    
    export function frame() {
      const toggle = control("Show planet");
      return toggle.kind === "checkbox" ? [120, 120] : [80, 80];
    }
    

    Line by line:

    • Lines 4-5: kind names one of the seven control kinds - slider, checkbox, colour, point, layer, dropdown or angle - and with it the shape .value will take. Show planet is a checkbox, so the wide branch wins and the Planet renders at 120 percent. Dispatching on kind lets one helper script accept whatever control it is pointed at.

    What you see. The planet renders at 120 percent scale - the branch for a checkbox control, picked by inspecting the control's kind.

    The planet renders at 120 percent scale - the branch for a checkbox control, picked by inspecting the control's kind.

    layer#

    Type: string (field).

    The owning layer's id.

    Worked example: Find the control's home#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Rig altitude and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const speed = control("Speed");
      const host = layer(speed.layer);
      return [value[0], host.position.value[1] - 100];
    }
    

    Line by line:

    • Lines 4-5: layer is the id of the layer hosting the control - controls always live on a layer, conventionally a null acting as the rig. Handing that id to layer() climbs from the control to its home.
    • Line 6: The Planet floats 100 pixels above wherever the Rig sits: move the rig null and the planet's perch moves with it.

    What you see. The planet floats exactly 100 pixels above the Rig null - drag the rig around the stage and the planet keeps its perch.

    The planet floats exactly 100 pixels above the Rig null - drag the rig around the stage and the planet keeps its perch.

    enabled#

    Type: boolean (field).

    The display flag (an unused control still evaluates).

    Worked example: The eye greys, scripts still read#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

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

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const speed = control("Speed");
      return speed.enabled ? 90 : 20;
    }
    

    Line by line:

    • Lines 4-5: enabled mirrors the control tile's eye toggle. IMPORTANT: the eye only greys the tile for humans - a disabled control still evaluates, still keyframes and still answers .value. Read enabled when you want your script to HONOUR the eye; ignore it and the control keeps working regardless.

    What you see. The planet rests at 90 percent while the Speed control's eye is on - this script chooses to dim to 20 if someone greys the control, even though the value itself would keep flowing.

    The planet rests at 90 percent while the Speed control's eye is on - this script chooses to dim to 20 if someone greys the control, even though the value itself would keep flowing.

    min#

    Type: number (field).

    The slider/angle minimum (settings).

    Worked example: The slider's floor as data#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Floor size and paste:

    export const outputs = ["layer_2:transform.scale"];
    
    export function frame() {
      const speed = control("Speed");
      return [speed.min + 80, speed.min + 80];
    }
    

    Line by line:

    • Lines 4-5: min is the slider's configured lower bound - 0 on Speed, so the Planet renders at 80 percent. min, max, step and unit are the SETTINGS a slider was given, readable as plain data; the editor clamps the slider's own value into [min, max] as you scrub it.

    What you see. The planet renders at 80 percent scale - the Speed slider's floor of zero plus eighty.

    The planet renders at 80 percent scale - the Speed slider's floor of zero plus eighty.

    max#

    Type: number (field).

    The slider/angle maximum (settings).

    Worked example: The slider's ceiling as data#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Half ceiling and paste:

    export const outputs = ["layer_1:transform.rotation"];
    
    export function frame() {
      return control("Speed").max / 2;
    }
    

    Line by line:

    • Line 4: max is the slider's configured upper bound - 100 on Speed, so the Sun turns a steady 50 degrees. Reading the bounds instead of hard-coding them means retuning the slider's range in the panel retunes the script with it.

    What you see. The sun stands turned 50 degrees - half the Speed slider's ceiling, and it re-tunes itself if the slider's range is edited.

    The sun stands turned 50 degrees - half the Speed slider's ceiling, and it re-tunes itself if the slider's range is edited.

    step#

    Type: number (field).

    The slider step (settings).

    Worked example: Snap to the slider's grid#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Step lift and paste:

    export const outputs = ["layer_2:transform.position"];
    
    export function frame() {
      const speed = control("Speed");
      const notches = 20 / speed.step;
      return [value[0], value[1] - notches];
    }
    

    Line by line:

    • Lines 4-5: step is the slider's scrub increment - 1 on Speed, so twenty units are twenty notches. A script can respect the same grid the slider snaps to, keeping script-made and hand-made values on one lattice.
    • Line 6: The Planet lifts one pixel per notch, twenty in all.

    What you see. The planet floats 20 pixels above its resting spot - one pixel for each notch the Speed slider would click through across twenty units.

    The planet floats 20 pixels above its resting spot - one pixel for each notch the Speed slider would click through across twenty units.

    unit#

    Type: string (field).

    The unit label (settings; "" when none).

    Worked example: Honour the declared unit#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Unit gate and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const speed = control("Speed");
      return speed.unit === "%" ? 65 : 100;
    }
    

    Line by line:

    • Lines 4-5: unit is the display suffix the slider was configured with - "%" on Speed (an empty string when none was set). It is a hint for humans, but a careful script can branch on it to interpret the number the way the rig's author intended.

    What you see. The planet rests at 65 percent - the branch taken because the Speed slider declares itself a percentage.

    The planet rests at 65 percent - the branch taken because the Speed slider declares itself a percentage.

    items#

    Type: string[] (field).

    A dropdown's items ([] for other kinds).

    Worked example: The dropdown's menu as data#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Menu census and paste:

    export const outputs = ["layer_1:transform.rotation"];
    
    export function frame() {
      const direction = control("Direction");
      return direction.items.length * 20;
    }
    

    Line by line:

    • Lines 4-5: items is a dropdown's menu as an array of strings (empty for every other kind). Direction offers Clockwise and Anticlockwise - two entries, so the Sun turns 40 degrees. Reading the menu lets a script stay correct when options are added later.

    What you see. The sun stands turned 40 degrees - twenty for each entry in the Direction dropdown's menu.

    The sun stands turned 40 degrees - twenty for each entry in the Direction dropdown's menu.

    value#

    Type: T (accessor).

    The LIVE effective value at the fold frame in wire units (see the class doc).

    Determinism: pure.

    Worked example: The live dial#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Speed dial and paste:

    export const outputs = ["layer_1:transform.rotation"];
    
    export function frame() {
      return control("Speed").value;
    }
    

    Line by line:

    • Line 4: value is the control's LIVE reading at the current frame, in the same units the panel shows: sliders and angles answer numbers, a checkbox true or false, a point [x, y], a colour [r, g, b, a] in 0..1, a dropdown its 0-based index, and a layer control a Layer handle or null. Keyframe the control and the read animates; scrub it and the Sun turns under your pointer. This one line is the whole controller pattern: the control is the surface, the script is the wiring.

    What you see. The sun stands turned 40 degrees - the Speed slider's value read as an angle. Scrub the slider and the sun turns live under your hand.

    The sun stands turned 40 degrees - the Speed slider's value read as an angle. Scrub the slider and the sun turns live under your hand.

    text#

    Type: string | null (accessor).

    A dropdown's item at the sampled index; null out of range or for other kinds.

    Determinism: pure.

    Worked example: The dropdown's words#

    The scene. A gold sun near the centre, a small blue planet above it, and a hidden Rig null carrying the expression controls that steer the scene.

    • The comp Orbit: 640 by 360 at 25 fps, 200 frames (8 seconds).
    • Sun: a solid layer, at [320, 180], 80 by 80, filled #f2a833.
    • Planet: a solid layer, at [320, 60], 30 by 30, filled #4d9de0.
    • Rig: a null (an invisible controller host), at [320, 180].
      • An expression control: Speed, a slider (0 to 100, step 1, unit %), set to 40.
      • An expression control: Show planet, a checkbox, set to true.
      • An expression control: Start angle, a angle, set to 90.
      • An expression control: Direction, a dropdown (items Clockwise / Anticlockwise), set to 0.
      • An expression control: Centre, a point, set to [320, 180].
      • An expression control: Target, a layer, set to layer_2.

    The script. Add a project script named Direction gate and paste:

    export const outputs = ["layer_2:transform.opacity"];
    
    export function frame() {
      const direction = control("Direction");
      return direction.text === "Clockwise" ? 80 : 30;
    }
    

    Line by line:

    • Lines 4-5: text answers the dropdown's SELECTED item as its string - the menu entry at the current index - and null when the index is out of range or the control is not a dropdown. Branching on the words reads clearly; branching on .value (the index) survives the menu being reworded. Direction rests on Clockwise, so the bright branch wins.

    What you see. The planet rests at 80 percent while the Direction dropdown says Clockwise - and dims to 30 the moment the menu is switched to Anticlockwise.

    The planet rests at 80 percent while the Direction dropdown says Clockwise - and dims to 30 the moment the menu is switched to Anticlockwise.