ComfyTimeline
Maya-style global timeline for ComfyUI — a shared bottom-panel bar (scrub, range, transport, keyframe dope-sheet) + curve editor + frame-accurate viewer, with a public JS & Python API any node can connect to.
Nodes (3)
A 3D viewport that syncs to your animation timeline — no Three.js install
The curve editor that animates your strengths — not your video
Scrub an image batch like a video, and every node scrubs with it
ComfyTimeline — a global timeline standard for ComfyUI
A VFX-style global timeline for ComfyUI — one bottom-panel bar that owns frame state (playhead, animation range, playback range, fps, transport, loop) for the whole graph, plus a VFX Graph-Editor-style curve editor, a frame-accurate image-sequence viewer, and a native 3D animation viewer.
It also ships ComfyTimeline, a small public API so any node or extension can connect to the timeline — read the current frame/range/fps, react to scrubbing and playback, or draw keyframe ticks on the bar — from both JS and Python.
Status:
apiVersion 1.0.0. The surface listed in API_REFERENCE.md is the committed stable contract.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/FemtoStudio/comfyui-timeline
Restart ComfyUI, then open the bottom panel → "Timeline" tab (the panel is hidden by default — toggle it from View → Bottom Panel, or press Alt+T to jump straight to the Timeline tab). No Python dependencies beyond ComfyUI itself. The frontend works with both classic Nodes 1.0 and Vue Nodes 2.0.
The Timeline 3D Viewer uses ComfyUI's bundled native viewport and loaders. It
adds no Three.js/Python dependency; frontend 1.45.15+ is required for that
node (1.45.20 is the current tested target). Older frontends show a clear
unsupported message while the timeline and 2D nodes keep working.
Its toolbar is identical on Nodes 1.0 and Nodes 2.0 and uploads only to
ComfyUI's input/3d area. Use Upload model for a self-contained file
(prefer GLB), or Upload folder for one GLTF plus its external buffers and
textures. The browser safely embeds those local companions into a packed GLTF
before uploading it to a unique input/3d/timeline_uploads subfolder. GLTF and
GLB contents are checked for external URLs, and OBJ/MTL sidecar pairs are
excluded because they are not self-contained. No additional viewer or
conversion package is installed; for folders above 48 MiB, convert to GLB
first.
Transport shortcuts
Defaults (rebindable in Settings → Keybindings, discoverable in the command palette under "Timeline:"):
| key | action | |---|---| | Space | play / pause | | → / ← | next / previous frame (wraps in the play range) | | Alt+T | open the Timeline panel tab |
Transport buttons on the bar: ⏮ go-to-start · ‹ step · ▶/⏸ play · › step · ⏭ go-to-end · ⟳ loop · EF play-every-frame (no frame-drop, vs real-time).
Nodes provided:
- Timeline Animation — author animation curves (Hermite tangents, pan/zoom,
marquee, key/tangent editing). Its keys show as ticks on the global bar.
Outputs:
float(list-expanded, one downstream run per frame),float_list(the same samples as one list),count, andint/int_list— the samples rounded to true integers for INT-typed inputs (frame picks, seeds, step counts). - Timeline Image Viewer — scrub/play an
IMAGEbatch against the global playhead. Optionalmaskinput mattes each frame before display (white = keep, black = out); a single mask broadcasts across the batch and resolution mismatches are resized. - Timeline 3D Viewer — load/upload GLTF, GLB, OBJ, FBX, STL, PLY, or supported splat models using ComfyUI's native renderer. Choose an embedded animation clip, orbit the camera, and scrub/play it from the global timeline. Single-file and packed-GLTF-folder uploads work in both frontend generations.
Scrubbing never re-runs your graph: the playhead lives on the timeline model, not in any node input, so queueing after a scrub is a clean cache hit.
Execution guards reject non-finite values and pathological payloads before allocation: up to 100,000 output frames, 10,000 curve keys, a 2 MiB curve JSON store, and 10,000 image-preview frames per execution. Image previews also have a 512 MiB CPU working-set cap; their temp files follow ComfyUI's normal temp lifecycle so cached history entries remain valid.
Connect a node to the timeline
Python (read the frame/range/fps in execute())
from comfy_api.latest import io
try:
from comfy_timeline import timeline_inputs, read_timeline
except ImportError: # timeline pack not installed
timeline_inputs = lambda **k: []
read_timeline = None
class MyNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MyNode",
inputs=[io.Image.Input("image"), *timeline_inputs()],
)
@classmethod
def execute(cls, image, **tl):
t = read_timeline(**tl)
frame = t.current_frame # the live playhead at queue time
...
timeline_inputs() adds standard current_frame / start_frame / end_frame
/ fps widgets. The frontend auto-binds and hides them and drives them from
the bar, so the value the user set on the bar bakes into the prompt at queue
time — cache-correct, no server state. Don't convert these to socket inputs
(force_input); they must stay widgets.
JS (react to the timeline live — no boilerplate for "read")
If your node already declares the standard widgets (via timeline_inputs() or by
hand: current_frame, or both start_frame+end_frame), the auto-bind
wires them with zero JS.
To react (e.g. repaint at the new frame), subscribe to the model:
import { app } from "/scripts/app.js";
app.registerExtension({
name: "MyExt",
setup() {
const T = window.ComfyTimeline; // capability check
if (!T || parseInt(T.apiVersion) < 1) return;
const off = T.model.subscribe(["frame", "range"], (e) => {
myRepaint(T.model.currentFrame); // or e.detail.frame
});
// call off() to unsubscribe
},
});
To drive the timeline (e.g. a "shot info" node sets the range):
window.ComfyTimeline.model.setAnimRange(1001, 1100);
window.ComfyTimeline.model.setPlayRange(1001, 1100);
Events
window.ComfyTimeline.model is an EventTarget. Each event's detail carries
a source token — pass your own stable token to mutators and ignore events
where detail.source is your own, to avoid feedback loops.
| event | detail | when |
|---|---|---|
| frame | { frame:int, source } | playhead moved |
| range | { which:'anim'\|'play', animStart, animEnd, playStart, playEnd, source } | a range changed |
| fps | { fps:float, source } | fps (or speed) changed |
| play | { playing:bool, source } | transport toggled |
| loop | { loop:bool, source } | loop toggled |
| mode | { mode:'realtime'\|'everyframe', source } | playback mode toggled |
| change (on curves) | { curveId } | a curve was registered / removed / edited |
Full method/field reference: API_REFERENCE.md. TypeScript/editor typings: web/js/comfytimeline.d.ts.
Versioning
window.ComfyTimeline.apiVersion— semver of the public surface. Minor = additive, major = breaking. Feature-detect with it.window.ComfyTimeline.schemaVersion— integer; only theapp.graph.extra.timelineserialization. Independent ofapiVersion.
Migration
The pre-1.0 window.timelineAnim cross-node registry has been removed.
Use window.ComfyTimeline (model/curves/events) instead.
License
MIT — see LICENSE.