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

    State and time

    How state() gives a script memory across frames while scrubbing, replaying and farm renders all stay deterministic.

    Ordinary expressions are pure: the same frame always computes the same value from nothing but the document. state() adds the one thing purity cannot do - memory - without giving up determinism. This page explains the model; the state and memo reference sections carry worked examples (a gravity bounce and a frame counter you can rebuild).

    state(): memory that folds#

    export function frame({ dt }) {
      const store = state({ y: 60, vy: 0 });
      store.vy += 900 * dt;
      store.y += store.vy * dt;
      return [320, store.y];
    }
    

    state(initial) answers a per-script store. The first evaluated frame sees initial; every later frame sees whatever the PREVIOUS frame left in it. Mutate it freely - it is your object for the frame.

    The store advances along the comp's integer frame grid as a FOLD from the epoch (frame 0): the state at frame n is defined as frame n minus one's state pushed through your frame(). Jumping the playhead to frame 200 shows exactly what playing 0 through 200 would have shown, because that is literally how the value is defined - scrubbing equals playing, always. The engine keeps periodic checkpoints so seeking stays fast, and a document edit invalidates them and refolds; none of that machinery changes the value, only how quickly it is reached.

    state(initial, { key: "..." }) names additional stores when one script wants several. A top-level const store = state({...}) binds the same per-frame store as calling it inside frame().

    Plain data only#

    Stores are serialised between frames, so they hold PLAIN DATA: numbers, strings, booleans, arrays, plain objects. A function, BigInt or circular structure reports the typed state_unserialisable; a store growing past 256 KB reports state_too_large. Keep the working set, not a history of everything.

    Reads inside the fold are pinned#

    During a fold your CLOCK walks the grid - time, frame and dt take each folded frame's values - but PROPERTY and CONTROL reads always answer the frame being rendered, not each intermediate step. So fold from your own store and the clock (velocities, counters, phases), and treat scene reads as "where things are NOW". A trail of a layer's past positions is therefore not built by reading the layer during the fold - integrate your own copy of the motion instead, as the gravity example does.

    Errors do not advance the fold#

    A frame whose evaluation fails leaves the store exactly as the previous frame serialised it and the outputs at their base values - the fold skips the failed step rather than recording half of it.

    memo(): compute once per document#

    memo(key, fn) runs fn once and caches the result until the DOCUMENT changes - derived constants, lookup tables, parsed data. The body must not read time or frame (the typed memo_time_access enforces it): memo is for things that change with edits, never with playback. For per-frame memory use state(); for per-frame values just compute them - evaluation is cheap and budgeted.

    Time, spelled twice#

    frame is the canonical clock - an integer on the comp's exact rational grid - and time is derived seconds (frame / fps). Write cues in whichever reads best and convert with timeToFrames() / framesToTime(); dt (one frame in seconds) is the natural step for integration, as in the bounce above. Remember the one shadowing rule: inside export function frame() {} destructure { frame } from the argument, because the bare word names the function.