About comfyui-node-outputs
comfyui-node-outputs is a guide for ComfyUI node developers on returning data and interface content through NodeOutput. It covers media previews, file saving, and the distinction between temporary previews and persistent outputs.
Key features
- 01Configure multiple node output types
- 02Preview images, audio, and video with UI helpers
- 03Save image and audio files
- 04Display text and 3D model previews
- 05Distinguish temporary previews from permanent saves
Use cases
- 01Return node data alongside previews
- 02Build image or audio saving nodes
- 03Show video, text, or 3D model results
- 04Choose temporary or output folders for files
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ jtydhr88/ comfyui-custom-node-skills --skill comfyui-node-outputsAfter installing, type/comfyui-node-outputsin Claude Code to confirm it loaded.
Details
From the project READMEComfyUI Node Outputs
Nodes return data through io.NodeOutput. V3 provides built-in UI helpers for previews and file saving.
Basic Output
class SimpleNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SimpleNode",
display_name="Simple Node",
category="example",
inputs=[io.Float.Input("a"), io.Float.Input("b")],
outputs=[
io.Float.Output("SUM"),
io.Float.Output("PRODUCT"),
],
)
@classmethod
def execute(cls, a, b):
# Values must match output order
return io.NodeOutput(a + b, a * b)
Output Configuration
io.Schema(
outputs=[
io.Image.Output("IMAGE"), # basic output
io.Int.Output("COUNT"), # integer output
io.Float.Output("VALUE", display_name="Result"), # custom display name
io.String.Output("TEXT", tooltip="The processed text"),
io.Image.Output("FRAMES", is_output_list=True), # outputs a list
],
)
NodeOutput Variants
# Data only
return io.NodeOutput(image_tensor, mask_tensor)
# UI only (output node with no data outputs)
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
# Data + UI
return io.NodeOutput(image_tensor, ui=ui.PreviewImage(images, cls=cls))
# No output
return io.NodeOutput()
# Block execution
return io.NodeOutput(block_execution="Reason for blocking")
# Node expansion (positional args are outputs, not result= keyword)
return io.NodeOutput(output_ref, expand=graph.finalize())
UI Preview Helpers
Import ui from comfy_api.latest:
from comfy_api.latest import io, ui
PreviewImage
Display image previews on the node. Saves to temp directory automatically.
# Constructor: PreviewImage(image, animated=False, cls=None)
class PreviewNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="PreviewNode",
display_name="Preview Image",
category="image",
is_output_node=True,
inputs=[io.Image.Input("images")],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, images):
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
PreviewMask
# Constructor: PreviewMask(mask, animated=False, cls=None)
# Auto-converts mask to 3-channel grayscale for display
return io.NodeOutput(ui=ui.PreviewMask(masks, cls=cls))
PreviewAudio
# Constructor: PreviewAudio(audio, cls=None)
# Saves as FLAC to temp directory
return io.NodeOutput(ui=ui.PreviewAudio(audio, cls=cls))
PreviewVideo
# Constructor: PreviewVideo(values: list[SavedResult | dict])
return io.NodeOutput(ui=ui.PreviewVideo(saved_video_results))
PreviewText
Display text output:
return io.NodeOutput(ui=ui.PreviewText(value))
PreviewUI3D
Display 3D model preview:
return io.NodeOutput(ui=ui.PreviewUI3D(
model_file=saved_result, # SavedResult for the 3D file
camera_info=camera_dict, # camera position/target/zoom
bg_image=image_tensor, # optional background image (via **kwargs)
))
Saved3DModels (standard 3D output, v0.40+)
All core 3D save/preview nodes now report models as standard "3d" output items (shown in queue/assets like images):
# Constructor: Saved3DModels(results: list[SavedResult], camera_info=None, model_3d_info=None)
# -> {"3d": results, "camera_info": [camera_info], "model_3d_info": [...]}
return io.NodeOutput(ui=ui.Saved3DModels(
[ui.SavedResult("model.glb", "", io.FolderType.output)], # or FolderType.temp for previews
camera_info=camera_dict, # extended CameraInfo (quaternion/fov/near/far/frustum)
model_3d_info=transforms, # list[Model3DTransform]: position/quaternion/scale per model
))
ui.PreviewUI3DAdvanced(model_file, camera_info, model_3d_info, folder_type=None) still exists as a subclass, but model_file is now a relative path string ("subfolder/model.glb", default folder output) and it emits the "3d" format above, not {"result": [...]}.
Saving Images
Using ImageSaveHelper
The ui.ImageSaveHelper class provides static methods for various image formats:
# Save as PNG (returns list[SavedResult])
results = ui.ImageSaveHelper.save_images(
images, # tensor [B,H,W,C]
filename_prefix="ComfyUI",
folder_type=io.FolderType.output, # output, temp, or input
cls=cls, # node class (for metadata)
compress_level=4,
)
# Save and get UI object directly (saves to output folder)
saved_ui = ui.ImageSaveHelper.get_save_images_ui(images, "ComfyUI", cls=cls)
return io.NodeOutput(ui=saved_ui)
# Save animated PNG
result = ui.ImageSaveHelper.save_animated_png(
images, "anim", io.FolderType.output, cls=cls, fps=12.0, compress_level=4
)
# Save animated PNG and get UI
saved_ui = ui.ImageSaveHelper.get_save_animated_png_ui(images, "anim", cls=cls, fps=12.0, compress_level=4)
# Save animated WebP
result = ui.ImageSaveHelper.save_animated_webp(
images, "anim", io.FolderType.output, cls=cls,
fps=12.0, lossless=False, quality=80, method=4
)
# Save animated WebP and get UI
saved_ui = ui.ImageSaveHelper.get_save_animated_webp_ui(
images, "anim", cls=cls, fps=12.0, lossless=False, quality=80, method=4
)
Simple save node example:
class SaveImageNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveImageNode",
display_name="Save Image",
category="image",
is_output_node=True,
inputs=[
io.Image.Input("images"),
io.String.Input("filename_prefix", default="ComfyUI"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, images, filename_prefix):
saved = ui.ImageSaveHelper.get_save_images_ui(images, filename_prefix, cls=cls)
return io.NodeOutput(ui=saved)
Manual Image Saving
import os
import json
import numpy as np
from PIL import Image as PILImage
from PIL.PngImagePlugin import PngInfo
import folder_paths
class CustomSaveNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CustomSaveNode",
display_name="Custom Save",
category="image",
is_output_node=True,
inputs=[
io.Image.Input("images"),
io.String.Input("prefix", default="output"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, images, prefix):
output_dir = folder_paths.get_output_directory()
results = []
for i, image in enumerate(images):
# Convert tensor to PIL
img_array = np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8)
pil_image = PILImage.fromarray(img_array)
# Add metadata
metadata = PngInfo()
if cls.hidden.prompt:
metadata.add_text("prompt", json.dumps(cls.hidden.prompt))
if cls.hidden.extra_pnginfo:
for k, v in cls.hidden.extra_pnginfo.items():
metadata.add_text(k, json.dumps(v))
# Save with counter
filename = f"{prefix}_{i:05d}.png"
filepath = os.path.join(output_dir, filename)
pil_image.save(filepath, pnginfo=metadata)
results.append(ui.SavedResult(
filename=filename,
subfolder="",
type=io.FolderType.output,
))
return io.NodeOutput(ui=ui.SavedImages(results))
Saving Audio
The ui.AudioSaveHelper supports FLAC, MP3, and Opus formats:
# Save audio (returns list[SavedResult])
results = ui.AudioSaveHelper.save_audio(
audio, # {"waveform": Tensor, "sample_rate": int}
filename_prefix="audio",
folder_type=io.FolderType.output,
cls=cls,
format="flac", # "flac", "mp3", or "opus" (anything else raises ValueError)
quality="128k", # MP3: "V0","128k","320k"; Opus: "64k"-"320k"
)
# Save and get UI object
saved_ui = ui.AudioSaveHelper.get_save_audio_ui(audio, "audio", cls=cls, format="flac", quality="128k")
return io.NodeOutput(ui=saved_ui)
class SaveAudioNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SaveAudioNode",
display_name="Save Audio",
category="audio",
is_output_node=True,
inputs=[
io.Audio.Input("audio"),
io.String.Input("prefix", default="audio"),
io.Combo.Input("format", options=["flac", "mp3", "opus"], default="flac"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, audio, prefix, format):
saved = ui.AudioSaveHelper.get_save_audio_ui(audio, prefix, cls=cls, format=format)
return io.NodeOutput(ui=saved)
Temporary Previews vs Permanent Saves
- Previews (PreviewImage, etc.) save to the
tempdirectory and are ephemeral - Saves (ImageSaveHelper.save_images) save to the
outputdirectory permanently - Use
io.FolderType.temp,io.FolderType.output, orio.FolderType.input
How Core Post-Processes UI Output
After execute, every file entry ({"filename", "subfolder", "type"}) in the UI dict is enriched before it is sent/stored:
metadata: for image/video files,{"kind", "width", "height"}(+duration,fps,frame_countfor video) is added unless the entry already hasmetadata(v0.40+).- With the assets system on (default only in the Windows standalone build),
id/preview_idare added; on a cache-hit replayid/preview_idare stripped from every dict entry, and anasset_previewkey is consumed by core. - The UI dict is deep-copied each run.
So don't use id, preview_id, metadata or asset_preview as your own keys in UI entries, and keep UI values small and JSON-serializable.
V1 Output Patterns (Legacy Reference)
class V1SaveNode:
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "save"
def save(self, images, prefix):
# ... save logic ...
return {
"ui": {
"images": [
{"filename": "out.png", "subfolder": "", "type": "output"}
]
}
}
# Data + UI in V1:
class V1PreviewAndOutput:
RETURN_TYPES = ("IMAGE",)
OUTPUT_NODE = True
FUNCTION = "run"
def run(self, image):
# ... preview logic ...
return {
"ui": {"images": [...]},
"result": (processed_image,),
}
See Also
comfyui-node-basics- Node structure and Schemacomfyui-node-datatypes- Data type formatscomfyui-node-lifecycle- Execution flow