Skip to content

Config reference: the two TOML files

Two files

Two files, two roles (ADR-0003):

  1. comfy-env-root.toml at the pack root:

    • Declares other nodepacks that this nodepack depends on or uses
    • Declares nodepack custom type serializers
  2. comfy-env.toml at nodes/ or nodes/<subdir>.

    • Gives that directory its own isolated pixi environment.
    • The file's presence is the isolation switch.

Those two locations are the only ones supported: discovery and the runtime binder deliberately match, so a config anywhere else is simply not seen rather than silently materialized-but-unused.

Example nodepack

Using ComfyUI-GeometryPack as the example:

ComfyUI/custom_nodes/
`-- ComfyUI-GeometryPack/
    +-- comfy-env-root.toml     <- FILE 1: pack-wide. [types], [node_packs].
    |                              Declares no environment.
    +-- requirements.txt        <- one line: comfy-env. The host env gets
    |                              nothing else.
    +-- prestartup_script.py       setup_env()
    +-- install.py                 install()
    +-- __init__.py                register_nodes()
    `-- nodes/
        +-- comfy-env.toml      <- FILE 2: everything below is ONE isolated
        |                          env (cgal, igl, pyvista, trimesh, ...)
        +-- boolean/
        +-- remeshing/
        +-- skeleton/, uv/, io/, analysis/, ...   (29 dirs in total)

GeometryPack puts the env file at nodes/, so the whole directory is a single environment.

The case where a pack needs two or more environments is also supported!

Nodepack authors can put one comfy-env.toml in each nodes/<subdir>/ instead -- one env per subdir, named <pack>-<subdir>.

comfy-env.toml

comfy-env.toml is open schema. comfy-env translates this .toml file into a pixi.toml config file format. Every key in comfy-env.toml falls into one of four buckets:

  • Ours: comfy-env consumes it and emits something else. Exactly four keys.
  • Passthrough: copied into the generated pixi.toml untouched.
  • Rewritten: passed through, but not unchanged. The torch family, plus the two packages whose pin comfy-env replicates from the host (comfy-aimdo, comfy-kitchen).
  • Refused: a hard error, because comfy-env generates it.
Key(s) Bucket Fate
python ours the env's interpreter pin (quoted string, 3.10 minimum)
[cuda] ours packages resolved to prebuilt wheel URLs at install time, inlined into the manifest as direct-URL pypi dependencies
[env_vars] ours env vars on this env's workers and scans -- never reaches pixi. These land before comfy-env's own spawn-time writes, and the host-derived writes (args mirror, aimdo enable/version/headroom, COMFY_CPU) are guarded not-in-env, so a value pinned here wins over the host-derived one. That makes it a lever as well as a setting: pinning COMFY_ENV_MIRROR_ARGS=0 here disables the host CLI flag mirror for this pack. A few writes are unconditional and overwrite a pack's value: COMFY_ENV_IPC_ADDR, COMFY_ENV_IPC_AUTHKEY, COMFY_ENV_PARENT_CUDA_IPC, COMFY_ENV_HOST_LOG_LEVEL, COMFYUI_BASE, COMFYUI_ISOLATION_WORKER, PYTHONNOUSERSITE, and PYTHONPATH / PYTHONHOME / PYTHONSTARTUP / PYTHONUSERBASE are removed (subprocess.py, subenv.py). Values are coerced with str(), so { N = 4 } arrives as "4"
[options] ours runtime knobs -- never reaches pixi. Exactly one exists today: health_check_timeout (seconds, per-env worker ping timeout, default 5.0); call_timeout is planned (ADR-0018)
[dependencies], [pypi-dependencies], [target.*], [activation], [tasks], [pypi-options], [system-requirements] passthrough forwarded into the generated pixi.toml at feature level ([activation] is merged with comfy-env's own KMP_DUPLICATE_LIB_OK entry; [target.*] keeps only the current platform's table)
[workspace] passthrough, partially only workspace.channels is read and merged after conda-forge; every other [workspace] key is silently dropped (the generated [workspace] is comfy-env's own)
torch / torchvision / torchaudio pins rewritten stripped and replaced with the workspace-wide pin, with a log line
comfy-aimdo / comfy-kitchen pins rewritten replaced with the host ComfyUI's exact pin (or dropped when the host has none, leaving the solver to pick); an exact == pin that disagrees with the host is a ValueError. Both are also injected into every torch env that never declared them (comfy-aimdo only on CUDA stacks)
[environments], [feature.*] refused compiler-owned: the manifest is single-feature/single-environment by design
workspace.name / .version / .platforms refused compiler-owned: env identity and host-derived platforms
[node_packs], [types] refused root-file sections -- the two files do not share a vocabulary
[serializers] refused removed 0.4.16; declare [types] in the root file instead

The 4 kinds of keys in comfy-env.toml

1. Ours — translated, never forwarded

This bucket is exactly four keys, and the list is closed: parse_config consumes python, [cuda], [env_vars] and [options], and anything it does not consume is passthrough by definition.

They do not exist in pixi at all. Pixi 0.75.0 accepts exactly seventeen top-level tables:

workspace, package, target, dependencies, host-dependencies,
build-dependencies, constraints, exclude-newer, pypi-dependencies,
pypi-exclude-newer, dev, activation, tasks, feature, environments,
pypi-options, system-requirements
and rejects anything else by name.

So [cuda] in a manifest handed straight to pixi is a hard error ('cuda' was not expected here). comfy-env consumes these itself and emits something pixi understands.

You write What it becomes When
python = "3.11" [feature.node.dependencies] python = "3.11.*" (the single feature is always named node; the pixi environment is always default) build
[cuda] packages resolved wheel URLs, inlined into the manifest as direct-URL [feature.node.pypi-dependencies] entries so they are solved and recorded in pixi.lock (see cuda-wheels) build
[env_vars] environment variables set on the worker process at spawn run
[options] the one runtime knob: health_check_timeout (seconds, default 5.0) run

The bottom two never reach pixi.toml in any form. [env_vars] in particular is not [activation.env]: it is applied when comfy-env spawns the metadata scan and the persistent worker, so it affects those processes and nothing else.

Careful with cuda: it is not a pixi table, but it is a valid key inside [system-requirements]. Different thing, and comfy-env never emits it: the only system requirements it derives from the host are glibc (Linux) and macos (macOS), written as keys on the workspace platforms entry, and only when the pack declared no [system-requirements] of its own. The CUDA major it detects is used for the torch index choice, not for this key.

2. Passthrough

Most native pixi keys like [tasks], [activation], [pypi-options], [system-requirements], [target.*], [dependencies]... are copied into the generated pixi.toml at feature level. comfy-env does not validate it; pixi does, because pixi is pinned (ADR-0002).

Use whatever pixi magic you like.

3. Rewritten on the way through

Forwarded, but not unchanged: torch-family pins. torch, torchvision and torchaudio in [dependencies] or [pypi-dependencies] are stripped and replaced with the workspace-wide pin, and you get a log line saying so. The parent and every worker ideally share one identical torch for compatibility and disk space reasons.

The same rule covers the two packages whose version comfy-env replicates from the host rather than authors: comfy-aimdo and comfy-kitchen (_HOST_DERIVED_PKGS in toml_generator.py). A pack's declaration in [pypi-dependencies] (or the current platform's [target.*] table) is replaced with the host ComfyUI's exact pin, read from the installed distribution or ComfyUI's requirements.txt; when the host has no pin the declaration is dropped and the solver picks. A wildcard or open range is normalised silently; an exact == pin that disagrees with the host raises ValueError, because a worker and its host must agree on these. Both are also injected into every env that has torch at all, even when the pack never declared them, since the host's ComfyUI imports them unguarded on the comfy.model_patcher import chain (comfy-aimdo is skipped on CPU stacks, where it has no code path). There is no operator switch for this: the host_derived parameter exists on build_env_toml, but install_workspace always leaves it at its default of on.

4. Refused

Setting these is a hard error, because using them in comfy-env would not be appropriate:

Key Why
[environments], [feature.*] the per-env manifest is single-feature / single-environment by design (ADR-0007)
workspace.name, workspace.version env identity
workspace.platforms derived from the host machine

comfy-env-root.toml

comfy-env-root.toml is closed schema: it can only have two sections and nothing else. Any other top-level table is a hard parse error.

Section What it is
[node_packs] Peer nodepacks to install: git-ref-pinned table form
[types] Wire types this pack puts on sockets: "builtin" or "custom"

Example:

[node_packs]
OtherPack = { github = "https://github.com/x/OtherPack", tag = "v1.2.0" }
ComfyUI-GeometryPack = "https://github.com/PozzettiAndrea/ComfyUI-GeometryPack"

[types]
TRIMESH    = "custom"
SKELETON   = "builtin"
INTRINSICS = "builtin"

Notes:

  • install() consumes [node_packs]
  • register_nodes() validates and loads [types] (see custom wire types)

[node_packs]

We can declare nodepacks to install together with our main one in various ways, both from the registry and from github. After cloning/downloading, the peer's own install.py is run (if it has one). Its requirements.txt is not pip-installed by comfy-env (packages/node_packs.py has no pip step); a peer that needs host packages must install them from its own install.py

Spelling Example What happens
string shorthand Pack = "owner/Pack" owner/repo is normalized to https://github.com/owner/repo; full URLs pass through. Shallow clone of the default branch -- unpinned, deprecated by ADR-0016
github + tag Pack = { github = "owner/Pack", tag = "v1.2.0" } git clone --depth 1 --branch v1.2.0 -- the required shape per ADR-0016
github + branch { github = ..., branch = "dev" } shallow clone of that branch (moving ref -- unreproducible)
github + commit { github = ..., commit = "abc1234..." } full clone + git checkout <sha> (arbitrary commits cannot be shallow-cloned)
registry { registry = "pack-id" } zip download via api.comfy.org/nodes/<id>/install (latest version)
registry + version { registry = "pack-id", version = "1.2.0" } same endpoint, pinned version

repo is accepted as an alias for github. tag wins over branch for the clone (--depth 1 --branch <ref>); commit alone forces a full clone. The post-clone git checkout <commit> runs whenever commit is set, tag or branch included -- so { tag = ..., commit = ... } shallow-clones the tag and then fails the checkout unless the commit is reachable in that single-depth history.

Note:

Isolation is per-directory, not per-pack: nodes/main/ with no config imports in-process like any vanilla pack, while nodes/cgal/ with a comfy-env.toml gets its own env. Put the exotic dependencies behind a config and leave the lightweight nodes on the host runtime.