Live dropdowns¶
An isolated node's INPUT_TYPES is a snapshot taken once, in another
process. This is how its dropdowns stay current anyway — by asking the worker
that owns the node, but only if that worker is already awake.
The problem this exists to solve¶
In vanilla ComfyUI, INPUT_TYPES() is a function of live state, re-evaluated
on every /object_info request. That is why the checkpoint dropdown updates
when you drop a new file into models/checkpoints/ and hit refresh.
An isolated node cannot work that way. Its class lives in a worker process,
so the main process holds only a proxy whose INPUT_TYPES replays a payload
captured once, by the metadata scan, and then cached on disk. Left alone, a
combo built from a directory listing would be frozen at scan time: upload a
new .obj and it never appears — not on refresh, not on restart, because the
disk cache outlives the process that produced it.
This is the modal case, not an edge case
Measured across a 493-pack third-party corpus, 236 packs (48%) have an
INPUT_TYPES that reads the filesystem.
The ladder¶
comfy-env asks the worker that owns the node to re-run the node's own
INPUT_TYPES. Not a reconstruction of it, not a recipe inferred from its
output — the real function, in the environment that owns it.
The whole design is which worker it is willing to ask
(isolation/metadata.py, _refresh_combo_options):
| # | Worker for this env | What happens |
|---|---|---|
| 1 | alive, idle or mid-call | ask it on the side lane. The pack's real INPUT_TYPES() runs on a second thread in the worker and its combo options are spliced into the cached snapshot. Answered in the running node's GIL gaps: well under a millisecond while the node is in torch or a syscall, tens of milliseconds under CPU-bound Python |
| 2 | alive, but the pack module has not been imported by a real call yet, or the side lane did not answer within a second (native code that never drops the GIL), or the reply timed out in the last five seconds | miss. Fall through. The side lane never imports a module; that is the main lane's first call |
| 3 | dead, or never started | fall through |
Rungs 2 and 3 both mean keep the cached options — which is exactly what a node with no dynamic dropdown at all does. The miss path is the old frozen behaviour, so the ladder can only ever add. Until 2026-09-12 a worker that was busy was also a miss, because the main lane has one reader and it was inside the node call; the side lane is what removed that rung.
During an /object_info pass (upstream marks one with
folder_paths.cache_helper.active) the host asks once per environment
for every class it built a proxy for, and answers the rest of the pass from
that reply. node_info calls INPUT_TYPES twice per node, so forty isolated
nodes used to be forty round trips on the event loop.
Why "spawn one" is not a rung¶
/object_info enumerates the entire node registry on every page load
(server.py), not the nodes on your canvas. So spawning there would
start every isolated environment on the machine in order to draw a dropdown —
and _get_or_create_worker runs on the asyncio event loop, holding a global
lock across the whole cold start. ComfyUI's HTTP server would freeze for the
duration: no /prompt, no /interrupt, no progress, no images.
Live options are worth a socket round trip to a process that already exists. They are not worth starting one.
What the worker sends back¶
Combo option lists, and nothing else. An input spec's first element is a list
only for a combo; "IMAGE" and ("INT", {…}) are left alone. An option list
containing anything but a primitive is refused outright rather than partially
converted.
The rest of the spec — tooltips, defaults, image_upload, and the set of
inputs itself — stays as captured at scan time. A node that changed its
shape between calls would invalidate the proxy class the parent already
built and handed to ComfyUI, so only the contents of a dropdown may move.
Two splice rules are load bearing:
- An empty listing keeps the cache. Splicing
[]would fail combo validation for every saved workflow using that node the moment a folder is empty — mid-upload, external drive unmounted. A transient state must not brick a workflow. - An unreported input is left alone. The fresh answer is additive. Silence about an input is not a claim that it disappeared.
What a pack author writes¶
Nothing.
There is no helper to import, no marker key, no declaration. A node writes the
same INPUT_TYPES it would write for vanilla ComfyUI — get_filename_list, a
hand-rolled os.walk, a glob, anything — and the ladder runs it.
That is the entire point of asking the worker rather than reconstructing the recipe in the host: there is no recipe to reconstruct.
Validation¶
A live dropdown is useless if execution then rejects the freshly-uploaded
value. ComfyUI exempts an input from its built-in combo and min/max checks
when the input's name appears in the validate function's argspec
(execution.py), so the proxy carries a synthesized VALIDATE_INPUTS
(V1) / validate_inputs (V3) naming exactly the node's combo inputs, plus
any the pack's own validate named.
Combos only, and deliberately. They are the exact set whose options the
refresh can rewrite, and they have no min/max — so numeric clamps keep
working. comfy-env never adds a **kwargs form, because that would exempt
every input on the node and silently disable those clamps. It does
reproduce one: if the pack's own validate was written (cls, **kwargs),
the synthesized signature carries **kwargs too (validate_varkw, captured
by the scan), and every built-in check is waived — exactly as it is
natively.
Only the signature is reproduced. The synthesized body is return True, so
the pack's own validation body runs in the worker, right before the node's
function, with what upstream handed the stand-in at submit — see
Caching and validation.
Never raises¶
/object_info enumerates every node, and a raise inside INPUT_TYPES makes
core omit the node entirely. A vanished node is strictly worse than a
stale dropdown.
So every failure on this path returns None and the proxy keeps its cached
options: a dead socket, a side reply that never came, a pack whose
INPUT_TYPES throws, a malformed reply. The worker logs the error on every failed call — once per
/object_info request — and the user sees a dropdown that has not moved.
Limits — read this part¶
- Cold means frozen. A dropdown only goes live once that pack's worker is running and has imported the pack through a real call, which in practice means after you have executed one of its nodes. Open ComfyUI, add a file, refresh without running anything, and you see the scan-time list.
- The pack's own
IS_CHANGEDrides the same ladder. A node that definesIS_CHANGEDorfingerprint_inputsgets its real fingerprint from its worker on rung 1, and answers changed on rungs 2 and 3, so overwriting a mesh in place re-executes as long as the pack's fingerprint says so. The miss answer is inverted on purpose: a stale dropdown is cosmetic, a stale cached result is wrong. See caching and validation. remotecombos are left alone. Both spec shapes are recognised — the hand-written(["a", "b"], {...})and the canonical("COMBO", {"options": [...]})every V3Comboinput becomes — but a combo declaredremotecarries no options for anyone to refresh: the frontend fetches them from a route, and the author's own validate exemption covers the value, as natively.- Everything else in the payload stays frozen —
RETURN_TYPES, tooltips, and any option list computed from something other than a file listing (installed backends, GPU capability probes, API queries). Those are live under rung 1 like anything else, and frozen under 2 and 3. - The side lane runs your
INPUT_TYPESwhile your node runs. That is what native ComfyUI does too (the server thread runs it for/object_infowhile the executor thread runs your node), so a pack that survives natively survives this. It does mean a CPU-heavyINPUT_TYPESsteals cycles from the running node for its own duration. - No
remotewidget. ComfyUI's frontend-fetched options widget would move the refresh onto the canvas repaint path, where comfy-env cannot control how often it fires, and its failure mode leaves the combo bound to a bare string for the rest of the session. Rejected; the ladder needs no frontend support at all.
See also¶
register_nodes()— where proxies are synthesized and the metadata scan runs- The process boundary — why the class is not in the main process to begin with
- Worker lifecycle — what "alive" means, and the idle sweep that ends it