Conditions
A 'do it once after N runs' gate for your ComfyUI graph
- count
- emit_result
Conditions (class AsyncOutputEmitter) is the counter that decides when a batch is "done" in the AsyncOutput pack. Each time you nudge it, it counts. When the count hits the number you asked for, it fires emit_result = true exactly once and re-arms itself. It's the Promise.all orchestrator in this family: the author's README describes it as an EventEmitter that keeps emitting false until the Nth task finally flips the signal.
Where it earns its keep: you have a loop that generates N images, each run touching a AsyncOutputCollect node to stash a string. You don't want the "combine all N results" step to run until the loop is genuinely finished. Drop an Emitter in the loop, wire its emit_result into AsyncOutputCallback, and downstream only executes on the run where the count lands on your condition.
How it works
State lives in a process-wide dict (ASYNC_OUTPUT_COUNTER) keyed by key_id. When touch is true the counter advances by step. When the counter matches (or exceeds, depending on mode), it returns emit_result = true and deletes the counter record - that delete is what makes it one-shot instead of firing on every later pass. IS_CHANGED returns nan, so the node always executes; the author needs it to run every pass to notice the count.
The inputs that matter
touch(BOOLEAN, forced input) - the pulse. Wire a boolean from your loop here; a false touch just reports the current count, it doesn't advance it.key_id(STRING) - must match the Collection/Callback key.conditions(INT, default 1) - the target count. This is the number you actually think about.mode-exact(fire only when count == conditions) orgreater_than_or_equal(fire at the first pass that reaches it).step(INT, default 1) - how much each touch adds. Set it higher if one run represents several units of work.reset(BOOLEAN, optional) - zeroes the counter without firing.
Outputs: count (INT) tells you the current count, and emit_result (BOOLEAN) is the one you wire into the Callback. The tooltips in the schema describe them as "Current Count Value" and "if count == conditions, return true. then remove this record" - the removal part is the behavior that catches people off guard.
Installing it
The pack installs clean with zero dependencies - nothing to pip install, no model files. In ComfyUI Manager search "AsyncOutput", or:
cd ComfyUI/custom_nodes
git clone https://github.com/teddy1565/ComfyUI-AsyncOutput
Restart and it appears under AsyncOutput/Deprecated(ComfyUI NOT Turing complete)/String.
Where people get burned
The category name isn't sarcasm - the author flags this family as deprecated because ComfyUI doesn't truly support mutable state across a workflow's execution, and it shows in the sharp edges. The whole memory store is auto-wiped at the start of every run unless you disable that with AsyncOutputGlobalAutoReset, and it all lives in process memory, so restart = empty. Also note the record is deleted the moment it fires: if the condition is 1 and you touch it once, that's it for the run - a second trigger on a later run starts a fresh count. If your emitter never fires, the first thing to check is whether auto-reset is eating the counter between touches.
The maintained sibling family has a near-identical node, BatchIteratorStringEmitter - same idea, fewer modes (== / >= only), and it doesn't delete its record after firing, which makes it more forgiving in loops.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| touch | BOOLEAN | — | |
| mode | COMBO | exact | 2 options: exact, greater_than_or_equal |
| key_id | STRING | — | |
| step | INT | 1 | — |
| conditions | INT | 1 | — |
| resetopt | BOOLEAN | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| count | INT | Current Count Value |
| emit_result | BOOLEAN | if count == conditions, return true. then remove this record |