Skip to content

Add has_intermediate_output flag for nodes with interactive UI#13048

Open
jtydhr88 wants to merge 1 commit intomasterfrom
feature/has-intermediate-output
Open

Add has_intermediate_output flag for nodes with interactive UI#13048
jtydhr88 wants to merge 1 commit intomasterfrom
feature/has-intermediate-output

Conversation

@jtydhr88
Copy link
Contributor

@jtydhr88 jtydhr88 commented Mar 18, 2026

Nodes like Image Crop have interactive UI regions that produce intermediate outputs. Previously, these outputs were lost on page refresh because cached intermediate nodes were skipped during execution list building.

The new has_intermediate_output flag behaves like OUTPUT_NODE (cached UI is resent to the frontend) but does not automatically seed the execution list, so it won't force evaluation of unevaluated branches (e.g. Switch).

Add the flag to ImageCropV2 and Painter as the first use case.

before
https://github.com/user-attachments/assets/4b6b3def-abee-49a2-b5a4-42fafafd8fd6

after

2026-03-18.15-06-16.mp4

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6aa6c6af-646f-4861-984b-7bc56985ace0

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd11fd and fbc3b0f.

📒 Files selected for processing (7)
  • comfy_api/latest/_io.py
  • comfy_execution/graph.py
  • comfy_extras/nodes_glsl.py
  • comfy_extras/nodes_images.py
  • comfy_extras/nodes_painter.py
  • execution.py
  • server.py
✅ Files skipped from review due to trivial changes (4)
  • server.py
  • comfy_extras/nodes_glsl.py
  • comfy_extras/nodes_painter.py
  • comfy_execution/graph.py

📝 Walkthrough

Walkthrough

Introduces a schema-level boolean has_intermediate_output and adds it to Schema and NodeInfoV1. Node classes expose this via a HAS_INTERMEDIATE_OUTPUT classproperty populated from GET_SCHEMA(). TopologicalSort gains an is_intermediate_output(node_id) helper. Several nodes (ImageCropV2, PainterNode, GLSLShader) mark their schema with has_intermediate_output=True. The server’s node_info now includes has_intermediate_output. Execution logic adds a _send_cached_ui helper and a pre-loop pass in PromptExecutor.execute_async that resends cached UI for intermediate-output prompt nodes.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introducing a has_intermediate_output flag for nodes with interactive UI, which is the core feature described in both the PR description and the changeset across multiple files.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem being solved (cached intermediate outputs lost on refresh), the solution mechanism (has_intermediate_output flag behavior), and naming two concrete use cases (ImageCropV2, Painter).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@comfy_api/latest/_io.py`:
- Around line 1469-1478: The new has_intermediate_output field in IO.Schema
shifts positional parameters and breaks source-compatibility for third-party
nodes using positional IO.Schema(...) calls; to fix, make
has_intermediate_output keyword-only or move it to the end of the
dataclass/fields list so existing positional arguments keep their
meanings—specifically update the IO.Schema declaration to declare
has_intermediate_output with field(kw_only=True) (or place it after
is_output_node and other existing fields) so existing callers of IO.Schema and
parameters like is_output_node remain unaffected.

In `@server.py`:
- Around line 712-713: The branch that sets info['has_intermediate_output'] only
when obj_class.HAS_INTERMEDIATE_OUTPUT is True causes inconsistent shapes
between legacy nodes and GET_NODE_INFO_V1; change the logic so info always
contains the key (set info['has_intermediate_output'] = bool(getattr(obj_class,
'HAS_INTERMEDIATE_OUTPUT', False))) or initialize
info['has_intermediate_output'] = False then set True when the attribute exists
on obj_class, ensuring every node returned by /object_info includes the
has_intermediate_output field consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 487a4d40-f012-4775-a855-3829410f7b4c

📥 Commits

Reviewing files that changed from the base of the PR and between 0695702 and cb385b4.

📒 Files selected for processing (5)
  • comfy_api/latest/_io.py
  • comfy_execution/graph.py
  • comfy_extras/nodes_images.py
  • comfy_extras/nodes_painter.py
  • server.py

Comment on lines +1469 to +1478
has_intermediate_output: bool=False
"""Flags this node as having intermediate output that should persist across page refreshes.
Nodes with this flag behave like output nodes (their UI results are cached and resent
to the frontend) but do NOT automatically get added to the execution list. This means
they will only execute if they are on the dependency path of a real output node.
Use this for nodes with interactive/operable UI regions that produce intermediate outputs
(e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image).
"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

#!/bin/bash
python - <<'PY'
import ast
from pathlib import Path

class Visitor(ast.NodeVisitor):
    def __init__(self, path: Path):
        self.path = path

    def visit_Call(self, node: ast.Call):
        func = node.func
        is_schema = (
            isinstance(func, ast.Name) and func.id == "Schema"
        ) or (
            isinstance(func, ast.Attribute) and func.attr == "Schema"
        )
        if is_schema and node.args:
            print(f"{self.path}:{node.lineno}: positional_args={len(node.args)}")
        self.generic_visit(node)

for path in Path(".").rglob("*.py"):
    if any(part in {".git", "__pycache__", ".venv", "venv", "node_modules"} for part in path.parts):
        continue
    try:
        tree = ast.parse(path.read_text(encoding="utf-8"))
    except Exception:
        continue
    Visitor(path).visit(tree)
PY

Repository: Comfy-Org/ComfyUI


Repository: Comfy-Org/ComfyUI
Exit code: 0


Script executed:

# Find the Schema class definition and inspect field order
grep -n "class Schema" comfy_api/latest/_io.py

Repository: Comfy-Org/ComfyUI


Repository: Comfy-Org/ComfyUI
Exit code: 0

stdout:

1431:class Schema:

Script executed:

# Examine the Schema class fields around line 1469-1478 and surrounding context
sed -n '1350,1490p' comfy_api/latest/_io.py | cat -n

Repository: Comfy-Org/ComfyUI


Repository: Comfy-Org/ComfyUI
Exit code: 0

stdout:

     1	    name: str=None
     2	    display_name: str=None
     3	    description: str=None
     4	    python_module: Any=None
     5	    category: str=None
     6	    output_node: bool=None
     7	    has_intermediate_output: bool=None
     8	    deprecated: bool=None
     9	    experimental: bool=None
    10	    dev_only: bool=None
    11	    api_node: bool=None
    12	    price_badge: dict | None = None
    13	    search_aliases: list[str]=None
    14	    essentials_category: str=None
    15	
    16	
    17	`@dataclass`
    18	class PriceBadgeDepends:
    19	    widgets: list[str] = field(default_factory=list)
    20	    inputs: list[str] = field(default_factory=list)
    21	    input_groups: list[str] = field(default_factory=list)
    22	
    23	    def validate(self) -> None:
    24	        if not isinstance(self.widgets, list) or any(not isinstance(x, str) for x in self.widgets):
    25	            raise ValueError("PriceBadgeDepends.widgets must be a list[str].")
    26	        if not isinstance(self.inputs, list) or any(not isinstance(x, str) for x in self.inputs):
    27	            raise ValueError("PriceBadgeDepends.inputs must be a list[str].")
    28	        if not isinstance(self.input_groups, list) or any(not isinstance(x, str) for x in self.input_groups):
    29	            raise ValueError("PriceBadgeDepends.input_groups must be a list[str].")
    30	
    31	    def as_dict(self, schema_inputs: list["Input"]) -> dict[str, Any]:
    32	        # Build lookup: widget_id -> io_type
    33	        input_types: dict[str, str] = {}
    34	        for inp in schema_inputs:
    35	            all_inputs = inp.get_all()
    36	            input_types[inp.id] = inp.get_io_type()  # First input is always the parent itself
    37	            for nested_inp in all_inputs[1:]:
    38	                # For DynamicCombo/DynamicSlot, nested inputs are prefixed with parent ID
    39	                # to match frontend naming convention (e.g., "should_texture.enable_pbr")
    40	                prefixed_id = f"{inp.id}.{nested_inp.id}"
    41	                input_types[prefixed_id] = nested_inp.get_io_type()
    42	
    43	        # Enrich widgets with type information, raising error for unknown widgets
    44	        widgets_data: list[dict[str, str]] = []
    45	        for w in self.widgets:
    46	            if w not in input_types:
    47	                raise ValueError(
    48	                    f"PriceBadge depends_on.widgets references unknown widget '{w}'. "
    49	                    f"Available widgets: {list(input_types.keys())}"
    50	                )
    51	            widgets_data.append({"name": w, "type": input_types[w]})
    52	
    53	        return {
    54	            "widgets": widgets_data,
    55	            "inputs": self.inputs,
    56	            "input_groups": self.input_groups,
    57	        }
    58	
    59	
    60	`@dataclass`
    61	class PriceBadge:
    62	    expr: str
    63	    depends_on: PriceBadgeDepends = field(default_factory=PriceBadgeDepends)
    64	    engine: str = field(default="jsonata")
    65	
    66	    def validate(self) -> None:
    67	        if self.engine != "jsonata":
    68	            raise ValueError(f"Unsupported PriceBadge.engine '{self.engine}'. Only 'jsonata' is supported.")
    69	        if not isinstance(self.expr, str) or not self.expr.strip():
    70	            raise ValueError("PriceBadge.expr must be a non-empty string.")
    71	        self.depends_on.validate()
    72	
    73	    def as_dict(self, schema_inputs: list["Input"]) -> dict[str, Any]:
    74	        return {
    75	            "engine": self.engine,
    76	            "depends_on": self.depends_on.as_dict(schema_inputs),
    77	            "expr": self.expr,
    78	        }
    79	
    80	
    81	`@dataclass`
    82	class Schema:
    83	    """Definition of V3 node properties."""
    84	
    85	    node_id: str
    86	    """ID of node - should be globally unique. If this is a custom node, add a prefix or postfix to avoid name clashes."""
    87	    display_name: str = None
    88	    """Display name of node."""
    89	    category: str = "sd"
    90	    """The category of the node, as per the "Add Node" menu."""
    91	    inputs: list[Input] = field(default_factory=list)
    92	    outputs: list[Output] = field(default_factory=list)
    93	    hidden: list[Hidden] = field(default_factory=list)
    94	    description: str=""
    95	    """Node description, shown as a tooltip when hovering over the node."""
    96	    search_aliases: list[str] = field(default_factory=list)
    97	    """Alternative names for search. Useful for synonyms, abbreviations, or old names after renaming."""
    98	    is_input_list: bool = False
    99	    """A flag indicating if this node implements the additional code necessary to deal with OUTPUT_IS_LIST nodes.
   100	
   101	    All inputs of ``type`` will become ``list[type]``, regardless of how many items are passed in.  This also affects ``check_lazy_status``.
   102	
   103	    From the docs:
   104	
   105	    A node can also override the default input behaviour and receive the whole list in a single call. This is done by setting a class attribute `INPUT_IS_LIST` to ``True``.
   106	
   107	    Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lists#list-processing
   108	    """
   109	    is_output_node: bool=False
   110	    """Flags this node as an output node, causing any inputs it requires to be executed.
   111	
   112	    If a node is not connected to any output nodes, that node will not be executed.  Usage::
   113	
   114	    From the docs:
   115	
   116	    By default, a node is not considered an output. Set ``OUTPUT_NODE = True`` to specify that it is.
   117	
   118	    Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#output-node
   119	    """
   120	    has_intermediate_output: bool=False
   121	    """Flags this node as having intermediate output that should persist across page refreshes.
   122	
   123	    Nodes with this flag behave like output nodes (their UI results are cached and resent
   124	    to the frontend) but do NOT automatically get added to the execution list. This means
   125	    they will only execute if they are on the dependency path of a real output node.
   126	
   127	    Use this for nodes with interactive/operable UI regions that produce intermediate outputs
   128	    (e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image).
   129	    """
   130	    is_deprecated: bool=False
   131	    """Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
   132	    is_experimental: bool=False
   133	    """Flags a node as experimental, informing users that it may change or not work as expected."""
   134	    is_dev_only: bool=False
   135	    """Flags a node as dev-only, hiding it from search/menus unless dev mode is enabled."""
   136	    is_api_node: bool=False
   137	    """Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview."""
   138	    price_badge: PriceBadge | None = None
   139	    """Optional client-evaluated pricing badge declaration for this node."""
   140	    not_idempotent: bool=False
   141	    """Flags a node as not idempotent; when True, the node will run and not reuse the cached outputs when identical inputs are provided on a different node in the graph."""

Keep IO.Schema positional construction source-compatible.

Schema is a public node API. Inserting has_intermediate_output shifts every positional argument after is_output_node, so third-party nodes that call IO.Schema(...) positionally will silently bind arguments to the wrong parameters.

Consider appending has_intermediate_output at the end of the field list, or make it keyword-only via field(kw_only=True), to preserve source compatibility with external custom nodes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@comfy_api/latest/_io.py` around lines 1469 - 1478, The new
has_intermediate_output field in IO.Schema shifts positional parameters and
breaks source-compatibility for third-party nodes using positional
IO.Schema(...) calls; to fix, make has_intermediate_output keyword-only or move
it to the end of the dataclass/fields list so existing positional arguments keep
their meanings—specifically update the IO.Schema declaration to declare
has_intermediate_output with field(kw_only=True) (or place it after
is_output_node and other existing fields) so existing callers of IO.Schema and
parameters like is_output_node remain unaffected.

@christian-byrne christian-byrne removed their assignment Mar 18, 2026
@jtydhr88 jtydhr88 changed the title Add has_intermediate_output flag for nodes with interactive UI Add has_intermediate_output flag for nodes with interactive UI Mar 19, 2026
@jtydhr88 jtydhr88 force-pushed the feature/has-intermediate-output branch from 5cd11fd to fbc3b0f Compare March 23, 2026 03:23
@jtydhr88 jtydhr88 assigned christian-byrne and unassigned jtydhr88 Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants