Skip to main content

Three States Hold ComfyUI Together

What keeps ComfyUI coherent as work moves through its flow? The answer may be simpler than expected: three states provide a shared way to understand what happens next.

Code Cracking
15m read
#StateMachine#Workflow
Three States Hold ComfyUI Together - Featured blog post image
Mahmoud Zalt

1:1 Mentor

Are you a software engineer moving into AI?

Let's have a call. I'll help you modernize your skills and learn the tools, systems, and architecture behind reliable AI products. One session or ongoing.

Vibe Coding
with Confidence

The Vibecoder's Handbook, from idea to production

4.8

Everything you need to know about shipping software with AI, from the App idea to production.

What it covers

  • 0IntroductionWhat this book is & how to read it
  • 1Set UpGet your tools and a running app ready
  • 2PlanStructure your idea into a clear specification
  • 3ArchitectLay out a modular codebase for your AI
Start Reading Free

We’re examining how ComfyUI’s execution engine keeps graph-shaped prompts correct when nodes wait, fail, or create more work. ComfyUI executes prompts as dependency graphs rather than linear scripts, and execution.py is the file that turns those graphs into scheduled node work. Its most consequential design choice is small: every execution attempt reports SUCCESS, FAILURE, or PENDING.

I’m Mahmoud Zalt, an AI solutions architect. The interesting question is not whether a scheduler can run a ready node; it is whether it can remain truthful when a coroutine is unfinished, a lazy dependency appears, or a node expands into a subgraph. We’ll trace how ComfyUI establishes readiness, why PENDING is a correctness state rather than a convenience, and which boundaries must uphold that contract as the system grows.

From graph validation to runnable work

The protocol matters only after ComfyUI has established what may run. A prompt is a directed graph: node inputs can link to outputs produced by upstream nodes. validate_prompt starts from requested nodes whose classes declare OUTPUT_NODE = True, then recursively calls validate_inputs across their dependencies.

That pass checks registered node classes, link shapes, compatible types, required values, custom validation, cycles, and loop boundaries. Only then does PromptExecutor.execute_async create a DynamicPrompt, prepare caches, and ask ExecutionList to stage a node. get_input_data then assembles literal controls, cached upstream outputs, and hidden service values into the arguments expected by the node API.

Prompt dictionary
       │
       ▼
validate_prompt() ──► validate_inputs() ──► validate_loops()
       │
       ▼
DynamicPrompt + CacheSet + ExecutionList
       │
       ▼
stage node ──► resolve inputs ──► invoke node
       │                              │
       └──── SUCCESS / FAILURE / PENDING ◄────┘
Execution reduces a graph-shaped prompt to repeated, explicit node-state transitions.

Cycle detection shows why validation belongs before expensive execution. The visiting collection represents the current recursive path; encountering an already-visiting node proves that no valid ordering exists. ComfyUI records both the invariant violation and the concrete cycle path, rather than allowing the scheduler to appear hung.

The three-state contract

Once a node is staged, the scheduler needs one honest answer to a narrow question: did this node complete? ExecutionResult supplies that answer. It is a small state machine: SUCCESS means outputs are ready, FAILURE means the prompt must stop with structured error details, and PENDING means more work is required before completion can be claimed.

self.success = result != ExecutionResult.FAILURE
if result == ExecutionResult.FAILURE:
    self.handle_execution_error(
        prompt_id, dynamic_prompt.original_prompt,
        current_outputs, executed, error, ex
    )
    break
elif result == ExecutionResult.PENDING:
    execution_list.unstage_node_execution()
else:
    execution_list.complete_node_execution()
OutcomeScheduler actionPromise to dependents
SUCCESSComplete the staged nodeCached outputs may satisfy downstream links
PENDINGUnstage the nodeContinue waiting; no output is available yet
FAILUREReport the error and stopNo further prompt progress is allowed

The key distinction is between returning from an attempt and finishing the node. execute can begin fresh work, resume asynchronous work, insert a dynamic subgraph, and later re-enter after that subgraph finishes. The enum gives every one of those mechanisms the same scheduling language. Without it, a scheduler can accidentally release dependents based on control flow rather than actual output availability.

Why pending work cannot be completed

Asynchronous nodes make the distinction concrete. ComfyUI creates a task for coroutine node methods, yields once so immediately completed tasks can finish, and otherwise retains the unfinished task for later resolution. The wrapper keeps CurrentNodeContext attached to the prompt, node, and list index while the coroutine runs.

task = asyncio.create_task(
    async_wrapper(f, prompt_id, unique_id, index, args=inputs)
)
await asyncio.sleep(0)
if task.done():
    results.append(task.result())
else:
    results.append(task)

An unfinished task is stored in pending_async_nodes, and ExecutionList receives an external block. A completion task gathers the outstanding work and removes that block. This barrier is the operational meaning of PENDING: downstream execution cannot observe a partial result merely because the parent node was invoked.

The same state covers different sources of incompleteness. A lazy node can discover that an unresolved input is required and strengthen that dependency. A node can return a dynamically expanded subgraph that must be inserted into DynamicPrompt before the parent is finalized. These are distinct mechanisms, but the scheduler should make the same claim in each case: this node is not done.

List mapping increases the need for an explicit pending state

_async_map_node_over_list may call a node once per element up to the largest input-list length. Its orchestration cost is approximately O(m × i), where m is that maximum length and i is the number of input fields, excluding the node’s own work. One logical node can therefore create many tasks, making partial completion especially dangerous.

There is an important operational limit: this file defines no per-node or per-prompt deadline for such tasks. Cancellation exists through the queue and global node interruption, but a plugin coroutine that never returns can retain its external block indefinitely. A configurable deadline with structured timeout cleanup would strengthen the same truthfulness guarantee; automatic retries would not, because node execution is not guaranteed to be idempotent.

Protecting the contract at system boundaries

A precise internal protocol still depends on honest boundaries around it. Error serialization is the clearest example. Hidden inputs can include auth_token_comfy_org and api_key_comfy_org, and the module identifies those names in SENSITIVE_EXTRA_DATA_KEYS. Yet the general exception path formats resolved inputs into current_inputs, which can then cross the server boundary in an execution_error event.

The focused correction is to redact at formatting time, then test both returned error details and fake-server messages. A complete policy must also handle secrets nested in objects or carried under V3 names, and it should bound formatted output. Error payloads are output APIs; they require the same schema, redaction, size limits, and transport tests as successful responses.

Ownership boundaries matter too. Mutable dictionaries and lists used as public-call defaults can be shared across calls because Python creates them once, not once per invocation. Replacing them with None and allocating fresh values inside the function prevents prompt state from leaking across calls.

Finally, observability should expose the protocol rather than only CPU time. Track prompt duration by workflow class and cache type, cache-hit ratio, pending asynchronous node count, queue depth, and queue wait time. Trace validation, cache priming, scheduler waits, node execution, asynchronous waits, subgraph expansion, and cleanup—but retain safe attributes such as node ID, class_type, cache-hit status, and outcome rather than raw inputs.

Design the protocol first

ComfyUI’s execution core coordinates graphs, plugins, caches, queues, server events, PyTorch, and memory policy. Its durable design lesson is simpler: complicated orchestration remains manageable when workers and schedulers share a small, honest outcome protocol. The three states work because completion, failure, and incompleteness produce different scheduler actions.

We can apply that lesson directly:

  1. Represent incompleteness explicitly. If work can wait on dependencies, pause in a coroutine, or expand into more graph work, distinguish pending from both success and failure.
  2. Make release conditional on real completion. Keep execution-list state and cached outputs consistent, and test that dependents cannot run before those outputs exist.
  3. Harden and observe the edges. Redact error payloads, eliminate shared mutable defaults, bound unfinished asynchronous work, and instrument queueing, cache behavior, and pending duration.

The primary lesson is that a scheduler must never lie about node state. ComfyUI demonstrates how an explicit SUCCESS/FAILURE/PENDING contract prevents that lie across asynchronous tasks, lazy dependencies, and dynamic subgraphs. Once that protocol is trustworthy, refactoring, security hardening, and scaling become safer engineering work instead of guesses about hidden execution state.

Full Source Code

Direct source from the upstream repository. Preview it inline or open it on GitHub.

heads/master/execution.py

Comfy-Org/ComfyUI • refs

Read Code on GitHub

Thanks for reading! I hope this was useful. If you have questions or thoughts, feel free to reach out.

Content Creation Process: This article was generated via a semi-automated workflow using AI tools. I prepared the strategic framework, including specific prompts and data sources. From there, the automation system conducted the research, analysis, and writing. The content passed through automated verification steps before being finalized and published without manual intervention.

Mahmoud Zalt

About the Author

I’m Zalt, a technologist with 16+ years of experience, passionate about designing and building AI systems that move us closer to a world where machines handle everything and humans reclaim wonder.

Let's connect if you're working on interesting AI projects, looking for technical advice or want to discuss anything.

Support this content

Share this article

Stay in touch

An occasional note when I build or write something new. Leave anytime.

Hire AI Employees

Hire AI Employees that work 24/7. No code.