LTX Director (Timeline)

Drive the LTX Director (Timeline) node.

How to install

How to install

  1. Setup differs for this server — follow the Installation part of the README below.
  2. Claude Code: claude mcp add <name> -- <command>.
  3. Claude Desktop / Cursor: add it under mcpServers in the MCP config file.
Claude Code — installs the whole folder, not just SKILL.md
npx degit artokun/comfyui-mcp/plugin/skills/ltx-director#main ~/.claude/skills/ltx-director

For one project only, change the path to .claude/skills/ltx-director.

This one runs on your machine and can reach your files. Read the README below before you connect it.

Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Show the full text183 lines
ltx-director/SKILL.md183 lines7.0 KBpushed 27d agoRawView on GitHub

LTX Director (Timeline)

LTXDirector (pack: WhatDreamsCost-ComfyUI, category WhatDreamsCost) is a video timeline editor node. Its on-canvas UI has Add Image, Add Text and Add Audio buttons.

You cannot click those buttons. Don't try.

They are not node inputs. The pack builds them as raw DOM elements with JS handlers:

addTextBtn.addEventListener("click", () => this.addTextSegmentFreeSpace());

Panel/MCP tools drive the LiteGraph node model (widgets + inputs). They cannot invoke arbitrary DOM handlers, so "click Add Text" is impossible. This is a hard limitation, not flakiness. Say so plainly rather than retrying.

The real control surface: timeline_data

Everything those buttons do is serialized into ONE string widget, timeline_data. The pack's own code treats it as "the absolute source of truth". Set it and the editor renders it.

It is listed in the pack's HIDDEN_WIDGET_NAMES, so it does not appear in the node's visible widget list, but it is an ordinary input and is settable:

create_workflow(action="modify", workflow=<the graph>, operations=[
  { op: "set_input", node_id: "42",
    input_name: "timeline_data", value: "<json string>" },
])

The value is a JSON string, not an object. Empty state is "{}".

Sibling hidden widgets: local_prompts, segment_lengths, guide_strength, audio_data, use_custom_audio, inpaint_audio, use_custom_motion, override_audio.

Schema (verified against a real production workflow)

{
  // TRACK GATES — a track's segments are IGNORED unless its gate is true.
  "mainTrackEnabled": true,
  "audioTrackEnabled": false,
  "motionTrackEnabled": false,

  // must mirror the node's start_frame / duration_frames widgets (see Edges)
  "normalStartFrame": 0,
  "normalDurationFrames": 4393,

  "global_prompt": "",
  "overrideAudio": false,
  "inpaint_audio": false,

  // retake mode
  "retakeMode": false, "retakeStart": 24, "retakeLength": 48,
  "retakePrompt": "", "retakeStrength": 1, "retakeVideo": null,
  "retake_global_prompt": "",

  // display-only
  "propHeight": 90, "globalPropHeight": 60, "showFilenames": true,

  "segments": [],        // main track: image AND text segments
  "motionSegments": [],
  "audioSegments": []
}

Text segment (segments)

{ "id": "seg1", "start": 0, "length": 48, "prompt": "wide shot, neon city", "type": "text" }

Image segment (segments)

{
  "id": "1784657553220x4fxu",
  "start": 0,
  "length": 241.68,
  "prompt": "",
  "type": "image",
  "imageFile": "whatdreamscost/Nintendo_ZeldaUrbosaHotspringKF_00002_.png",
  "imageB64": "/api/view?filename=Nintendo_ZeldaUrbosaHotspringKF_00002_.png&type=input&subfolder=whatdreamscost"
}

imageB64 is a misnomer. It holds a /api/view URL, not base64. So an image segment points at a file already in ComfyUI's input dir. Full agent-drivable recipe:

  1. upload_image (action:"image") → puts the file in the input dir (note its subfolder/name)
  2. imageFile = "<subfolder>/<name>.png"
  3. imageB64 = "/api/view?filename=<name>.png&type=input&subfolder=<subfolder>"

Audio segment (audioSegments)

{
  "id": "17846562234967gjvk", "type": "audio",
  "start": 0, "length": 4393, "trimStart": 0, "audioDurationFrames": 4393,
  "audioFile": "whatdreamscost/Intergalactic Hip Hop.mp3",
  "fileName": "Intergalactic Hip Hop.mp3",
  "waveformPeaks": [0.047, 0.274, 0.537, "…~200 floats…"]
}

waveformPeaks is the rendered waveform. It is cosmetic. Omit it or pass [] if you're writing a segment programmatically; the audio still plays. Don't fabricate plausible-looking peaks and imply they were measured.

Edges (get these wrong and it silently does nothing)

  1. Track gates. Pushing into audioSegments does nothing while audioTrackEnabled is false. Set the gate in the same edit. (The reference workflow ships an audio segment with the track OFF, which is easy to misread as broken.)
  2. Frame bookkeeping must agree. normalDurationFrames == duration_frames widget == segment_lengths widget (observed: all 4393), and normalStartFrame == start_frame. Change the timeline length and you must update all of them.
  3. Frames, not seconds. start / length are pixel-space frames and may be fractional (241.68). duration_seconds / frame_rate / time_units are display concerns.
  4. global_prompt exists twice, inside timeline_data and as a forceInput socket on the node. In the reference workflow the socket is wired from a PrimitiveStringMultiline; prefer the wired source and keep them consistent.
  5. guide_data must go somewhere. LTXDirector.guide_dataLTXDirectorGuide. That partner node is what injects the keyframes.
  6. Unsupported by the pack. The widget tooltip says "auto-managed; do not edit by hand". Well-formed blobs load fine; malformed JSON will break the editor. Round-trip and re-read after writing, and tell the user this is unofficial.

Reference wiring (from a working LTX 2.3 Director graph)

Power Lora Loader (rgthree) ─ model ─┐
DualCLIPLoader ──────────── clip ────┤
VAELoaderKJ ───────────── audio_vae ─┤   LTXDirector #42
PrimitiveStringMultiline ─ global_prompt ─┘
        │ model         → LTX2_NAG
        │ positive      → ConditioningZeroOut, LTXVConditioning
        │ audio_latent  → LTXVConcatAVLatent
        │ guide_data    → LTXDirectorGuide   ← required partner
        │ frame_rate    → AudioToFrameCount, VHS_VideoCombine
        └ combined_audio→ AudioToFrameCount, PreviewAudio, …

Procedure

  1. Find the node: get_workflow (action:"query") with types: ["LTXDirector"], fields: "detail" (or panel_query_graph on the live canvas).
  2. Read the current timeline_data and JSON.parse it. Never hand-splice the string.
  3. Mutate the parsed object (append a segment, flip a track gate, retime).
  4. Keep normalStartFrame / normalDurationFrames in sync with the widgets (edge 2).
  5. Write it back as a string via set_input.
  6. Re-read to confirm it round-trips, then create_workflow (action:"validate").

Related

PromptRelayEncodeTimeline (pack: ComfyUI-PromptRelay) carries the same timeline_data hidden-widget pattern, so this skill's approach applies there too.

Any node whose controls are DOM widgets over hidden state is invisible to clicking and must be driven through its state widget. LTX Director is the clearest example.

Sources

  • Official: none found.
  • Empirical: timeline_data schema and edge rules from a working LTX 2.3 Director graph.
1---
2name: ltx-director
3description: Drive the LTX Director (Timeline) node. Its Add Image/Text/Audio buttons are DOM-only and cannot be clicked by an agent; edit the hidden timeline_data JSON widget instead. Load when a workflow contains LTXDirector / LTXDirectorGuide / PromptRelayEncodeTimeline, or when asked to add, move, retime, or remove timeline segments (image / text / audio / motion).
4---
5 
6# LTX Director (Timeline)
7 
8`LTXDirector` (pack: **WhatDreamsCost-ComfyUI**, category `WhatDreamsCost`) is a
9video *timeline editor* node. Its on-canvas UI has **Add Image**, **Add Text**
10and **Add Audio** buttons.
11 
12## You cannot click those buttons. Don't try.
13 
14They are not node inputs. The pack builds them as raw DOM elements with JS
15handlers:
16 
17```js
18addTextBtn.addEventListener("click", () => this.addTextSegmentFreeSpace());
19```
20 
21Panel/MCP tools drive the LiteGraph **node model** (widgets + inputs). They
22cannot invoke arbitrary DOM handlers, so "click Add Text" is impossible. This
23is a hard limitation, not flakiness. Say so plainly rather than retrying.
24 
25## The real control surface: `timeline_data`
26 
27Everything those buttons do is serialized into ONE string widget, `timeline_data`.
28The pack's own code treats it as *"the absolute source of truth"*. Set it and the
29editor renders it.
30 
31It is listed in the pack's `HIDDEN_WIDGET_NAMES`, so it does **not** appear in the
32node's visible widget list, but it is an ordinary input and is settable:
33 
34```
35create_workflow(action="modify", workflow=<the graph>, operations=[
36 { op: "set_input", node_id: "42",
37 input_name: "timeline_data", value: "<json string>" },
38])
39```
40 
41The value is a JSON **string**, not an object. Empty state is `"{}"`.
42 
43Sibling hidden widgets: `local_prompts`, `segment_lengths`, `guide_strength`,
44`audio_data`, `use_custom_audio`, `inpaint_audio`, `use_custom_motion`,
45`override_audio`.
46 
47## Schema (verified against a real production workflow)
48 
49```jsonc
50{
51 // TRACK GATES — a track's segments are IGNORED unless its gate is true.
52 "mainTrackEnabled": true,
53 "audioTrackEnabled": false,
54 "motionTrackEnabled": false,
55 
56 // must mirror the node's start_frame / duration_frames widgets (see Edges)
57 "normalStartFrame": 0,
58 "normalDurationFrames": 4393,
59 
60 "global_prompt": "",
61 "overrideAudio": false,
62 "inpaint_audio": false,
63 
64 // retake mode
65 "retakeMode": false, "retakeStart": 24, "retakeLength": 48,
66 "retakePrompt": "", "retakeStrength": 1, "retakeVideo": null,
67 "retake_global_prompt": "",
68 
69 // display-only
70 "propHeight": 90, "globalPropHeight": 60, "showFilenames": true,
71 
72 "segments": [], // main track: image AND text segments
73 "motionSegments": [],
74 "audioSegments": []
75}
76```
77 
78### Text segment (`segments`)
79 
80```json
81{ "id": "seg1", "start": 0, "length": 48, "prompt": "wide shot, neon city", "type": "text" }
82```
83 
84### Image segment (`segments`)
85 
86```json
87{
88 "id": "1784657553220x4fxu",
89 "start": 0,
90 "length": 241.68,
91 "prompt": "",
92 "type": "image",
93 "imageFile": "whatdreamscost/Nintendo_ZeldaUrbosaHotspringKF_00002_.png",
94 "imageB64": "/api/view?filename=Nintendo_ZeldaUrbosaHotspringKF_00002_.png&type=input&subfolder=whatdreamscost"
95}
96```
97 
98**`imageB64` is a misnomer. It holds a `/api/view` URL, not base64.** So an
99image segment points at a file already in ComfyUI's **input** dir. Full
100agent-drivable recipe:
101 
1021. `upload_image (action:"image")` → puts the file in the input dir (note its `subfolder`/name)
1032. `imageFile` = `"<subfolder>/<name>.png"`
1043. `imageB64` = `"/api/view?filename=<name>.png&type=input&subfolder=<subfolder>"`
105 
106### Audio segment (`audioSegments`)
107 
108```json
109{
110 "id": "17846562234967gjvk", "type": "audio",
111 "start": 0, "length": 4393, "trimStart": 0, "audioDurationFrames": 4393,
112 "audioFile": "whatdreamscost/Intergalactic Hip Hop.mp3",
113 "fileName": "Intergalactic Hip Hop.mp3",
114 "waveformPeaks": [0.047, 0.274, 0.537, "…~200 floats…"]
115}
116```
117 
118`waveformPeaks` is the rendered waveform. It is cosmetic. Omit it or pass `[]` if
119you're writing a segment programmatically; the audio still plays. Don't fabricate
120plausible-looking peaks and imply they were measured.
121 
122## Edges (get these wrong and it silently does nothing)
123 
1241. **Track gates.** Pushing into `audioSegments` does nothing while
125 `audioTrackEnabled` is `false`. Set the gate in the same edit. (The reference
126 workflow ships an audio segment with the track OFF, which is easy to misread
127 as broken.)
1282. **Frame bookkeeping must agree.** `normalDurationFrames` ==
129 `duration_frames` widget == `segment_lengths` widget (observed: all `4393`),
130 and `normalStartFrame` == `start_frame`. Change the timeline length and you
131 must update all of them.
1323. **Frames, not seconds.** `start` / `length` are pixel-space frames and may be
133 fractional (`241.68`). `duration_seconds` / `frame_rate` / `time_units` are
134 display concerns.
1354. **`global_prompt` exists twice**, inside `timeline_data` and as a `forceInput`
136 socket on the node. In the reference workflow the socket is wired from a
137 `PrimitiveStringMultiline`; prefer the wired source and keep them consistent.
1385. **`guide_data` must go somewhere.** `LTXDirector.guide_data`
139 `LTXDirectorGuide`. That partner node is what injects the keyframes.
1406. **Unsupported by the pack.** The widget tooltip says *"auto-managed; do not
141 edit by hand"*. Well-formed blobs load fine; malformed JSON will break the
142 editor. Round-trip and re-read after writing, and tell the user this is
143 unofficial.
144 
145## Reference wiring (from a working LTX 2.3 Director graph)
146 
147```
148Power Lora Loader (rgthree) ─ model ─┐
149DualCLIPLoader ──────────── clip ────┤
150VAELoaderKJ ───────────── audio_vae ─┤ LTXDirector #42
151PrimitiveStringMultiline ─ global_prompt ─┘
152 │ model → LTX2_NAG
153 │ positive → ConditioningZeroOut, LTXVConditioning
154 │ audio_latent → LTXVConcatAVLatent
155 │ guide_data → LTXDirectorGuide ← required partner
156 │ frame_rate → AudioToFrameCount, VHS_VideoCombine
157 └ combined_audio→ AudioToFrameCount, PreviewAudio, …
158```
159 
160## Procedure
161 
1621. Find the node: `get_workflow (action:"query")` with `types: ["LTXDirector"]`, `fields: "detail"`
163 (or `panel_query_graph` on the live canvas).
1642. Read the current `timeline_data` and `JSON.parse` it. **Never** hand-splice the string.
1653. Mutate the parsed object (append a segment, flip a track gate, retime).
1664. Keep `normalStartFrame` / `normalDurationFrames` in sync with the widgets (edge 2).
1675. Write it back as a string via `set_input`.
1686. Re-read to confirm it round-trips, then `create_workflow (action:"validate")`.
169 
170## Related
171 
172`PromptRelayEncodeTimeline` (pack: ComfyUI-PromptRelay) carries the **same**
173`timeline_data` hidden-widget pattern, so this skill's approach applies there too.
174 
175Any node whose controls are DOM widgets over hidden state is invisible to
176clicking and must be driven through its state widget. LTX Director is the
177clearest example.
178 
179## Sources
180 
181- **Official:** none found.
182- **Empirical:** timeline_data schema and edge rules from a working LTX 2.3 Director graph.
183 

Discussion

Alternatives

Also in Video production