How composition works

The engine never stores a workflow. Every ComfyUI /promptis assembled at generate time from library content — atoms for the nodes, compounds and workflows for the order, protons for the types. This walks one real composition,ltx_i2v_clip (LTX 2.3), a stage at a time. The numbers are from running the composer on it: 46 nodes, six under s1_, 39 unders2_, plus one loader.

The current default, ltx25_i2v_clip (LTX 2.5), works the same way but is shaped differently: its models live inside the atom, and asave_video member ends the chain, so the graph carries its own output node.

Port colours match the Library's port chips: the same slot type looks the same in both places. Colour is never the only signal — every port is labelled with its type.

Fetched from the MX Library

Read from Postgres — the runtime source of truth. An agent never reads the library from GitHub.

  • compoundltx_i2v_clip

    the ordered member list

  • atomltx_models

    a ComfyUI node block + exit slots

  • atomltx_i2v

    a node block + sentinels + exit slots

  • protonone per node class

    per-slot TYPES only — no graph content (the type index)

Nothing pre-authored is loaded. There is no stored workflow JSON anywhere in this picture — the graph below exists only for this one request.

Composing ltx_i2v_clip into one ComfyUI promptltx_modelsatom · provides capability ltx_modelsltx_i2vatom · requires ltx_modelsLoadImageinjected for the image input/prompt
1 / 7

The request

generate takes exactly ONE root — an atom id or a compound id.

Here the agent asks for the compound ltx_i2v_clip, with an image as an input (an asset edge) and a prompt as a param. Nothing else is named: the rest is worked out from the library. A disabled library item is refused before anything composes. Inside a plan, every ComfyUI step arrives through this same call.

The four entity kinds

workflow
Top of the hierarchy. An ordered members list, end to end. Contributes order, not nodes. A pure ComfyUI chain flattens into one graph; a workflow whose members include agent, service or touchpoint steps, or that carries a ## Plan block, is run as a plan instead (see below).
compound
The same shape, one level down, and it can itself provide a capability. Flattened recursively into the atom chain. It can declare its own suggested_defaults.
atom
A comfy atom is the only kind carrying real ComfyUI nodes — a machine-generated JSON block whose dangling inputs are __INPUT_<name>__ sentinels, plus theexit_slots it exposes. Atoms also have a runtime:http (a microservice, called with call_service) oragent (the model itself does the step). Those carry no graph, so the composer skips them by design and generate refuses them.
proton
One ComfyUI node class, derived from the host's /object_info. Carries no graph content at all — only the per-slot types, which is what lets the composer wire by type rather than by hope. A class with no proton is invisible to type wiring, so a missing proton surfaces as an "unresolved sentinel", not as a broken atom.

A compound is not a plan

Everything above describes composing one ComfyUI/prompt. A compound flattens its members into exactly that: one graph, wired across members, containing whatever save node the chain ends in. A plan is different — the plan engine runs each step as its own prompt, and passes an artifact from one to the next.

That distinction has a sharp edge. ComfyUI refuses any prompt with no output node, so an atom that is a pure transform — image in, image out, nothing saved — is submittable inside a compound but not on its own. A plan step whose graph has no output node therefore gets aSaveImage appended, wired to its IMAGE exit slot (MIL-524). That is also what makes the next step possible: a step that saves nothing produces no artifact to consume.

Not everything is a graph: plans

Most real work mixes ComfyUI renders with things that are not graphs — an agent step that writes a concept or looks at frames, a service call (voice-over, transcription, a render service), a question for the user. The plan engine runs those as an ordered list of steps over one shared context. A step is either an atom (of any runtime) or a touchpoint — confirmation, choice, text input, asset upload, review/edit, or selection — which pauses the plan until it is answered.

  • Wiring. A step's inputs can reference an artifact an earlier step produced (fromStep), or one shot of an approved storyboard. Steps declare what they provides and consumes, so the engine knows which steps depend on which keys.
  • Conditions. onlyIf runs a step only when a touchpoint was answered a certain way (or per shot, so a mixed film routes footage shots and animation shots to different members). A step that does not run can hand the artifacts of a named key straight on, so everything downstream reads one key either way.
  • Failure. A failed atom step becomes a recovery question — retry, skip or edit params — rather than ending the run.
  • Saving. A finished run can be promoted to a workflow. Since MIL-1016 that keeps the plan itself (wiring, questions, onlyIf), so the saved workflow replays; a run that was already expanded from a workflow is refused, because its plan is in the library already.

Living Graph still holds inside it: each ComfyUI step is composed fresh, exactly as above.check_composition reports a workflow like this as not_applicable(plan-not-graph), not broken — there is no single graph to check.

How the agent drives all of this

The agent has no file, shell or GitHub tools and never writes a ComfyUI graph. It has library tools (list_atoms, search_atoms, read_item,check_composition), generate, the plan tools, the touchpoint tools, and a few for services, images and characters. Which one it reaches for is a ladder:

  1. One generate call with one root — an atom or, preferably, a compound that already covers the request. This is the default.
  2. plan_from_workflow when the workflow declares a ## Plan block: the engine writes the wiring, so the agent never hand-writes a fromStep.
  3. propose_plan only for a workflow with no declared plan, or a one-off chain, where one render must finish before the next.
  4. call_service forhttp atoms and call_agentfor agent atoms — generate answers not_composable for both.

The generate tool checks before it composes: parameters against the item (invalid_params), plus preflights for talking-head and MiniMax H3 items. A missing input comes back as missing_inputs, steering the agent to ask the user (ask_param or request_upload) rather than invent a value. After a render it lists the artifacts as refs to pass straight back in, and aNOT APPLIED line names any override the item has no input for — the render used its defaults for them.

Inside a plan the same render goes through the plan bridge, which calls the generate port directly — so those tool-level preflights do not run for plan steps. Plan wiring mistakes (afromStep written as a string, or pointing at a touchpoint instead of the render) are refused when that step is reached, not when the plan is proposed. Questions to the user go through the touchpoint tools (confirm, choose, ask_text,ask_param, request_upload, review_edit,select); only an asset_upload can be pre-answered, because the others ask the user to decide something.

Promoting a run to a workflow is not an agent tool: it is a Control Plane operation, offered from the Agents Manager.

Source of truth for this page: packages/composer(Composer.compose / composeCompound / wire.ts),packages/plan-engine, the harness's generate adapter and tools (apps/harness/src) and the orchestrator prompt. If the composer's behaviour changes, this page changes with it.