Channels
The host-published live-value primitive. Something outside the timeline owns the value and publishes it; resolution reads the publication.
A channel is how an observed value enters a scene without breaking determinism. The timeline never writes it — a renderer, an adapter or an external party publishes, and frame resolution reads through a defined precedence ladder.
Channel vs signal
signal— timeline-authored state. The scene generator owns it; writes are compile-time events replayed deterministically byat(frame).channel— host-published state. A renderer, adapter or external party publishes the value at run time.
Rule of thumb
If the value's history is authored (set / tween / play), it is a signal. If the
value is observed — measured, fetched, simulated, sensed — it is a channel.
Declaring
class VideoSurface extends LayoutActor {
// No default: reads throw until the first publication.
playbackTime = channel<number>();
// With default: reads return the default until published.
volume = channel({ default: 1 });
// Frame-indexed recording (see below).
measuredBox = channel<ResolvedLayoutBox>({ record: true });
}Declared as an actor field, a channel is spawn-scoped and keyed
`${actorId}:${name}`. Declared free — in a scene body or at module top level — it
keys by its enclosing scope and is shared by reference.
Read precedence
Every read resolves in this order. The first layer that applies wins.
Per-call override
at(frame, { channelValues }) pins a value for that resolution only. The publication
store is untouched, which is what makes this safe to call during React rendering.
Run log at frame
For record: true channels, the value at the greatest recorded frame ≤ the requested one.
Publication store
A host published a value this run.
Registry
An authored or registered cell value.
Declared default
channel({ default }).
Throw
No default and nothing published.
Writing
// Via the resolved handle
actor.channels.playbackTime.write(3.2);
// By id — the form adapters (physics, renderers, sensors) use
publishToChannel(actorId, "playbackTime", 3.2, { frame });
// Retract, so reads fall back down the precedence ladder
unpublishChannel(actorId, "playbackTime");write is a publication, not a timeline event. It creates no authored history, which is
why set and tween do not exist on channel handles — those are signal verbs and would
imply a history the channel does not have.
Recorded channels
By default publications are latest-wins: resolution at any frame sees the most recent value. That is wrong for values that are a function of the frame — measured boxes, stepped physics — because seeking backwards would read a value from the future.
record: true opts into a frame-indexed run log. Each publication records (frame, value)
and a read at frame N returns the value at the greatest recorded frame ≤ N, so scrubbing
backwards replays what was actually published.
measuredBox = channel<ResolvedLayoutBox>({ record: true });Demand-gated publication
Producing a channel value can be expensive — DOM measurement forces layout. Producers can gate on demand:
demandChannel(actorId, name)declares refcounted intent at consumer mount.- Reading through a resolved handle registers sticky observed demand.
- Producers check
hasChannelDemand/subscribeChannelDemand.
Gating is producer-opt-in and degrades safely: a leaked demand refcount means "always publish", never "value missing".
Per-resolution overrides
The safest way for a renderer to supply a host value is per call, not by publishing:
const state = compiled.at(frame, {
channelValues: new Map([[createCellKey(card.id, "availableWidth"), width]]),
});Nothing is written to the store, so repeating the frame with the same inputs produces the same output regardless of publication order. The Remotion example uses exactly this.
Measured-box conventions
Layout reads two channel names by convention, exported as constants:
MEASURED_BOX— the renderer-measured DOM box for a flow-hosted actor.TARGET_MEASURED_BOX— published by an offscreen target subtree during a layout transition.
FLIP-style projection compares the two frozen measurements and animates between them. See layout transition.