comfyui-node-datatypes
ComfyUI data types - IMAGE, LATENT, MASK, CONDITIONING, MODEL, CLIP, VAE, AUDIO, VIDEO, 3D types, widget types, and custom types. Use when working with ComfyUI tensors, model types, or defining input/output data types.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "comfyui-node-datatypes"
description: "ComfyUI data types - IMAGE, LATENT, MASK, CONDITIONING, MODEL, CLIP, VAE, AUDIO, VIDEO, 3D types, widget types, and custom types. Use when working with ComfyUI tensors, model types, or defining input/output data types."
license: "MIT"
---
# ComfyUI Data Types
ComfyUI uses specific data types for node inputs and outputs. Understanding tensor shapes and data formats is essential.
## Complete Type Reference
### Tensor/Data Types
| Type | V3 Class | Format | Description |
|---|---|---|---|
| IMAGE | `io.Image` | `torch.Tensor [B,H,W,C]` float32 0-1 | Batch of RGB images |
| MASK | `io.Mask` | `torch.Tensor [H,W]` or `[B,H,W]` float32 0-1 | Grayscale masks |
| LATENT | `io.Latent` | `{"samples": Tensor[B,C,H,W] or [B,C,T,H,W], "noise_mask"?: Tensor, "batch_index"?: list[int], "type"?: str}` | Latent space (4D image / 5D video) |
| CONDITIONING | `io.Conditioning` | `list[tuple[Tensor, PooledDict]]` | Text conditioning with pooled outputs |
| AUDIO | `io.Audio` | `{"waveform": Tensor[B,C,T], "sample_rate": int}` | Audio data |
| VIDEO | `io.Video` | `VideoInput` ABC | Video data (abstract base class) |
| SIGMAS | `io.Sigmas` | `torch.Tensor` 1D, length steps+1 | Noise schedule |
| NOISE | `io.Noise` | Object with `generate_noise()` | Noise generator |
| LORA_MODEL | `io.LoraModel` | `dict[str, torch.Tensor]` | LoRA weight deltas |
| LOSS_MAP | `io.LossMap` | `{"loss": list[torch.Tensor]}` | Loss map |
| TRACKS | `io.Tracks` | `{"track_path": Tensor, "track_visibility": Tensor}` | Motion tracking data |
| WAN_CAMERA_EMBEDDING | `io.WanCameraEmbedding` | `torch.Tensor` | WAN camera embeddings |
| LATENT_OPERATION | `io.LatentOperation` | `Callable[[Tensor], Tensor]` | Latent transform function |
| TIMESTEPS_RANGE | `io.TimestepsRange` | `tuple[int, int]` | Range 0.0-1.0 |
| DICT | `io.Dict` | `dict` | Generic dictionary |
| ARRAY | `io.Array` | `list` | Generic list/array |
### Model Types (opaque, typically pass-through)
| Type | V3 Class | Python Type |
|---|---|---|
| MODEL | `io.Model` | `ModelPatcher` |
| CLIP | `io.Clip` | `CLIP` |
| VAE | `io.Vae` | `VAE` |
| CONTROL_NET | `io.ControlNet` | `ControlNet` |
| CLIP_VISION | `io.ClipVision` | `ClipVisionModel` |
| CLIP_VISION_OUTPUT | `io.ClipVisionOutput` | `ClipVisionOutput` |
| STYLE_MODEL | `io.StyleModel` | `StyleModel` |
| GLIGEN | `io.Gligen` | `ModelPatcher` (wrapping Gligen) |
| UPSCALE_MODEL | `io.UpscaleModel` | `ImageModelDescriptor` |
| BACKGROUND_REMOVAL | `io.BackgroundRemoval` | `BackgroundRemovalModel` (e.g. BiRefNet) |
| LATENT_UPSCALE_MODEL | `io.LatentUpscaleModel` | Any |
| SAMPLER | `io.Sampler` | `Sampler` |
| GUIDER | `io.Guider` | `CFGGuider` |
| HOOKS | `io.Hooks` | `HookGroup` |
| HOOK_KEYFRAMES | `io.HookKeyframes` | `HookKeyframeGroup` |
| MODEL_PATCH | `io.ModelPatch` | Any |
| AUDIO_ENCODER | `io.AudioEncoder` | Any |
| AUDIO_ENCODER_OUTPUT | `io.AudioEncoderOutput` | Any |
| PHOTOMAKER | `io.Photomaker` | Any |
| POINT | `io.Point` | Any |
| FACE_ANALYSIS | `io.FaceAnalysis` | Any |
| BBOX | `io.BBOX` | Any |
| SEGS | `io.SEGS` | Any |
### 3D Types
| Type | V3 Class | Python Type | Description |
|---|---|---|---|
| MESH | `io.Mesh` | `MESH(vertices, faces)` | 3D mesh with vertices + faces tensors |
| VOXEL | `io.Voxel` | `VOXEL(data)` | Voxel data tensor |
| SPLAT | `io.Splat` | `SPLAT` | Gaussian splat data |
| FILE_3D | `io.File3DAny` | `File3D` | Any supported 3D format |
| FILE_3D_GLB | `io.File3DGLB` | `File3D` | Binary glTF |
| FILE_3D_GLTF | `io.File3DGLTF` | `File3D` | JSON-based glTF |
| FILE_3D_FBX | `io.File3DFBX` | `File3D` | FBX format |
| FILE_3D_OBJ | `io.File3DOBJ` | `File3D` | OBJ format |
| FILE_3D_STL | `io.File3DSTL` | `File3D` | STL format (3D printing) |
| FILE_3D_USDZ | `io.File3DUSDZ` | `File3D` | Apple AR format |
| FILE_3D_PLY | `io.File3DPLY` | `File3D` | PLY (point cloud / splat) |
| FILE_3D_SPLAT | `io.File3DSPLAT` | `File3D` | .splat gaussian splat file |
| FILE_3D_SPZ | `io.File3DSPZ` | `File3D` | Compressed splat (.spz) |
| FILE_3D_KSPLAT | `io.File3DKSPLAT` | `File3D` | .ksplat format |
| FILE_3D_SPLAT_ANY | `io.File3DSplatAny` | `File3D` | Any splat format |
| FILE_3D_POINT_CLOUD_ANY | `io.File3DPointCloudAny` | `File3D` | Any point cloud format |
| SVG | `io.SVG` | `SVG` | Scalable vector graphics |
| LOAD_3D | `io.Load3D` | `Model3DDict` (see below) | 3D model with renders |
| LOAD_3D_ANIMATION | `io.Load3DAnimation` | Same as Load3D | Animated 3D model |
| LOAD3D_CAMERA | `io.Load3DCamera` | `CameraInfo` (see below) | 3D camera info |
| LOAD3D_MODEL_INFO | `io.Load3DModelInfo` | `list[Model3DTransform]` | Per-model transforms (position/quaternion/scale) |
**Load3D.Model3DDict**: `{"image": str, "mask": str, "normal": str, "camera_info": CameraInfo, "recording"?: str, "model_3d_info"?: list[Model3DTransform]}`
**Load3DCamera.CameraInfo** (right-handed, Y-up, camera looks down -Z): required keys `position`, `target`, `zoom`, `cameraType` (`'perspective' | 'orthographic'`); optional keys `quaternion` (camera world rotation), `fov` (vertical, degrees, perspective only), `aspect`, `near`, `far`, `frustum` (orthographic only: `{left, right, top, bottom}`).
**Load3DModelInfo.Model3DTransform**: `{"position": dict, "quaternion": dict, "scale": dict}` in world space.
### Widget Types (create UI controls)
| Type | V3 Class | Python Type | Description |
|---|---|---|---|
| INT | `io.Int` | `int` | Integer with min/max/step |
| FLOAT | `io.Float` | `float` | Float with min/max/step/round |
| STRING | `io.String` | `str` | Text (single/multi-line) |
| BOOLEAN | `io.Boolean` | `bool` | Toggle with labels |
| COMBO | `io.Combo` | `str` | Dropdown selection |
| COMBO (multi) | `io.MultiCombo` | `list[str]` | Multi-select dropdown |
| COLOR | `io.Color` | `str` (hex) | Color picker, default `#ffffff` |
| COLORS | `io.Colors` | `list[str]` (hex) | Color palette (list of colors) |
| BOUNDING_BOX | `io.BoundingBox` | `{"x": int, "y": int, "width": int, "height": int}` | Rectangle region |
| BOUNDING_BOXES | `io.BoundingBoxes` | `list[{"x", "y", "width", "height", "metadata": dict}]` | Multiple labeled regions |
| CURVE | `io.Curve` | `list[tuple[float, float]]` | Spline curve points |
| RANGE | `io.Range` | `RangeInput` (min/max + optional midpoint) | Levels/range editor with gradient display |
| IMAGECOMPARE | `io.ImageCompare` | `dict` | Image comparison widget |
| WEBCAM | `io.Webcam` | `str` | Webcam capture widget |
| HISTOGRAM | `io.Histogram` | `list[int]` | Histogram bin counts |
### Special Types
| Type | V3 Class | Description |
|---|---|---|
| `*` (ANY) | `io.AnyType` | Matches any type |
| COMFY_MULTITYPED_V3 | `io.MultiType` | Accept multiple specific types on one input |
| COMFY_MATCHTYPE_V3 | `io.MatchType` | Generic type matching across inputs/outputs |
| COMFY_AUTOGROW_V3 | `io.Autogrow` | Dynamic growing inputs |
| COMFY_DYNAMICCOMBO_V3 | `io.DynamicCombo` | Combo that reveals sub-inputs per option |
| COMFY_DYNAMICSLOT_V3 | `io.DynamicSlot` | Connection slot that reveals sub-inputs when connected (not yet used by core nodes) |
| FLOW_CONTROL | `io.FlowControl` | Internal testing only |
| ACCUMULATION | `io.Accumulation` | Internal testing only |
## IMAGE Type
Images are `torch.Tensor` with shape `[B, H, W, C]`:
- **B** = batch size (1 for single image)
- **H** = height in pixels
- **W** = width in pixels
- **C** = channels (3 for RGB, values 0.0-1.0)
```python
import torch
import numpy as np
from PIL import Image as PILImage
class ImageProcessor(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageProcessor",
display_name="Image Processor",
category="image",
inputs=[io.Image.Input("image")],
outputs=[io.Image.Output("IMAGE")],
)
@classmethod
def execute(cls, image):
b, h, w, c = image.shape
result = torch.clamp(image * 1.5, 0.0, 1.0)
return io.NodeOutput(result)
```
### Loading / Saving Images
```python
from PIL import ImageOps
# Load from file → tensor
def load_image(path):
img = PILImage.open(path)
img = ImageOps.exif_transpose(img) # fix rotation from camera EXIF
if img.mode == "I": # handle 16-bit images
img = img.point(lambda i: i * (1 / 255))
img = img.convert("RGB")
return torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
# Tensor → save to file
def save_image(tensor, path):
if tensor.dim() == 4:
tensor = tensor[0]
PILImage.fromarray(np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)).save(path)
# Batch operations
batch = torch.cat([img1, img2], dim=0) # stack into batch
single = image[i] # extract from batch [H,W,C]
single_batch = image.unsqueeze(0) # add batch dim [1,H,W,C]
```
## MASK Type
`torch.Tensor` with shape `[H, W]` or `[B, H, W]`, values 0.0-1.0.
```python
# Invert mask
inverted = 1.0 - mask
# Mask ↔ Image conversion
alpha = mask.unsqueeze(0).unsqueeze(-1) # [1,H,W,1]
gray_mask = 0.299*img[:,:,:,0] + 0.587*img[:,:,:,1] + 0.114*img[:,:,:,2]
image_from_mask = mask.unsqueeze(-1).repeat(1, 1, 1, 3) # [B,H,W,3]
# Ensure batch dim
if mask.dim() == 2:
mask = mask.unsqueeze(0) # [1, H, W]
```
## LATENT Type
Dict with typed keys:
```python
class LatentDict(TypedDict):
samples: torch.Tensor # [B, C, H, W] (image) or [B, C, T, H, W] (video) - required
noise_mask: NotRequired[torch.Tensor]
batch_index: NotRequired[list[int]]
type: NotRequired[str] # only for "audio", "hunyuan3dv2"
```
**Image models** (SD1.5, SDXL, SD3, Flux): 4D `[B, C, H, W]` — SD1.5/SDXL = 4 channels, SD3/Flux = 16 channels. Latent dimensions are 1/8 of pixel dims.
**Video models** (Hunyuan Video, Wan, Cosmos, LTX Video, Mochi): 5D `[B, C, T, H, W]` — T is the temporal (frame) dimension.
```python
samples = latent["samples"]
# Check dimensionality:
if samples.ndim == 5:
B, C, T, H, W = samples.shape # video latent
else:
B, C, H, W = samples.shape # image latent
# Always preserve extra keys when modifying:
result = latent.copy()
result["samples"] = modified_samples
```
## CONDITIONING Type
`list[tuple[Tensor, PooledDict]]` — a list of (cond_tensor, metadata_dict) pairs.
The `PooledDict` contains many optional keys for different models:
```python
class PooledDict(TypedDict):
pooled_output: torch.Tensor
control: NotRequired[ControlNet]
area: NotRequired[tuple[int, ...]]
strength: NotRequired[float] # default 1.0
mask: NotRequired[torch.Tensor]
start_percent: NotRequired[float] # 0.0-1.0
end_percent: NotRequired[float] # 0.0-1.0
guidance: NotRequired[float] # Flux-like models
hooks: NotRequired[HookGroup]
# ... many more model-specific keys (SDXL, SVD, WAN, etc.)
```
Combine conditioning: `result = cond_a + cond_b` (list concatenation).
## VIDEO Type
`VideoInput` is an abstract base class with methods:
```python
class VideoInput(ABC):
def get_components(self) -> VideoComponents # images tensor + audio + frame_rate
def save_to(self, path, format, codec, metadata, bit_depth=None) # bit_depth: None keeps native depth (8 or 10)
def as_trimmed(self, start_time=None, duration=None, strict_duration=False) -> VideoInput | None
def get_stream_source(self) -> str | BytesIO
def get_dimensions(self) -> tuple[int, int] # (width, height)
def get_duration(self) -> float # seconds
def get_frame_count(self) -> int
def get_frame_rate(self) -> Fraction
def get_container_format(self) -> str
def get_bit_depth(self) -> int # 8 or 10 (default implementation returns 8)
```
10-bit video is supported end-to-end: loaders report `get_bit_depth()`, and save nodes preserve depth (`yuv420p10le` for 10-bit h264).
Concrete implementations: `VideoFromFile`, `VideoFromComponents` (available via `from comfy_api.latest import InputImpl`).
## 3D Types
### File3D
```python
from comfy_api.latest import Types
# File3D wraps a 3D file (disk path or BytesIO stream)
file_3d = Types.File3D(source="/path/to/model.glb", file_format="glb")
file_3d.format # "glb"
file_3d.is_disk_backed # True
file_3d.get_data() # BytesIO
file_3d.get_bytes() # raw bytes
file_3d.save_to("/output/model.glb")
```
### MESH, VOXEL and SPLAT
```python
from comfy_api.latest import Types
mesh = Types.MESH(vertices=torch.tensor(...), faces=torch.tensor(...))
voxel = Types.VOXEL(data=torch.tensor(...))
splat = Types.SPLAT(...) # gaussian splat data
```
## Widget Types with Special Features
### Color
```python
io.Color.Input("color", default="#ff0000", socketless=True)
# Value is a hex string like "#ff0000"
```
### Colors (palette)
```python
io.Colors.Input("palette", default=["#ff0000", "#00ff00"], socketless=True)
# Value is list[str] of hex colors
```
### BoundingBox
```python
io.BoundingBox.Input("bbox",
default={"x": 0, "y": 0, "width": 512, "height": 512},
socketless=True,
component="my_component", # optional custom UI component
)
# Value is {"x": int, "y": int, "width": int, "height": int}
```
### BoundingBoxes (multiple regions)
```python
io.BoundingBoxes.Input("regions", default=[], socketless=True)
# Value is list of {"x": int, "y": int, "width": int, "height": int, "metadata": dict}
```
### Curve
```python
from comfy_api.input import CurveInput
io.Curve.Input("curve",
default=[(0.0, 0.0), (1.0, 1.0)], # linear
socketless=True,
)
# In execute(), normalize the raw value first:
curve = CurveInput.from_raw(curve)
```
### Range (levels editor)
```python
from comfy_api.input import RangeInput
io.Range.Input("levels",
default={"min": 0.0, "max": 1.0},
display=None, # widget visualization mode
gradient_stops=None, # gradient background for the slider
show_midpoint=True, # show gamma midpoint handle
midpoint_scale=None,
value_min=0.0, value_max=1.0, # UI bounds
)
# In execute(), normalize with RangeInput.from_raw(value):
# .min_val, .max_val, .midpoint (gamma = -log2(midpoint), 0.5 = linear)
# .to_lut(size) generates a GIMP-style levels lookup table
```
### MultiCombo
```python
io.MultiCombo.Input("tags",
options=["tag1", "tag2", "tag3"],
default=["tag1"],
placeholder="Select tags...",
chip=True, # show as chips
)
# Value is list[str]
```
### Webcam
```python
io.Webcam.Input("webcam_capture")
# Value is str (captured image data)
```
### ImageCompare
```python
io.ImageCompare.Input("comparison", socketless=True)
# Value is dict
```
## Custom Types
```python
# Simple: create inline custom type
MyData = io.Custom("MY_DATA_TYPE")
# Use in inputs/outputs
io.Schema(
inputs=[MyData.Input("data")],
outputs=[MyData.Output("MY_DATA")],
)
```
### Advanced: @comfytype decorator
For custom types with type hints or custom Input/Output classes:
```python
from comfy_api.latest._io import comfytype, ComfyTypeIO
@comfytype(io_type="MY_DATA_TYPE")
class MyData(ComfyTypeIO):
Type = dict[str, Any] # type hint for the data
```
## AnyType / Wildcard
```python
# Accept any single type (always a connection input, no widget)
io.AnyType.Input("anything")
# Accept specific multiple types
io.MultiType.Input("data", types=[io.Image, io.Mask, io.Latent])
# MultiType with widget override (shows widget for first type)
io.MultiType.Input(
io.Float.Input("value", default=1.0),
types=[io.Float, io.Int],
)
```
## Imports from comfy_api.latest
```python
from comfy_api.latest import (
ComfyExtension, # extension registration
ComfyAPI, # runtime API (progress, node replacement)
io, # all io types (io.Image, io.Schema, io.ComfyNode, etc.)
ui, # UI output helpers (ui.PreviewImage, ui.SavedImages, etc.)
Input, # Input.Image (ImageInput), Input.Audio, Input.Mask, Input.Latent, Input.Video
InputImpl, # InputImpl.VideoFromFile, InputImpl.VideoFromComponents
Types, # Types.MESH, Types.VOXEL, Types.File3D, Types.VideoCodec, etc.
)
```
## Tensor Safety
When checking if a tensor exists, always use `is not None` instead of truthiness:
```python
# CORRECT
if image is not None:
process(image)
# WRONG — multi-element tensors don't support bool()
if image: # raises RuntimeError
process(image)
# For boolean conditions on tensors, use .all() or .any()
if (mask > 0.5).all():
...
```
## Type Conversion Patterns
```python
# IMAGE [B,H,W,C] → MASK [B,H,W]
mask = 0.299 * image[:,:,:,0] + 0.587 * image[:,:,:,1] + 0.114 * image[:,:,:,2]
# MASK [B,H,W] → IMAGE [B,H,W,C]
image = mask.unsqueeze(-1).repeat(1, 1, 1, 3)
# Resize image tensor
import torch.nn.functional as F
resized = F.interpolate(
image.permute(0, 3, 1, 2), # [B,C,H,W] for interpolate
size=(new_h, new_w), mode='bilinear', align_corners=False
).permute(0, 2, 3, 1) # back to [B,H,W,C]
```
## See Also
- `comfyui-node-basics` - Node class structure and registration
- `comfyui-node-inputs` - Input configuration details (widget options)
- `comfyui-node-outputs` - Output types and UI outputs
- `comfyui-node-advanced` - MatchType, MultiType, Autogrow, DynamicComboMore General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

