Evaluates
One list of strings in, one string out, your own Python in the middle
- tag
What this node is actually for
ComfyUI is good at moving values around - convert a widget to an input, wire a PrimitiveString,
and one prompt feeds six nodes. It's bad at transforming them. The moment you want to dedupe a list
of tags or join twenty captions into a paragraph, you stack join/split/replace nodes until the graph
looks like a substation.
Evaluates is the escape hatch. It's one of exactly two nodes in
alchemine/comfyui-evaluate-pack: a multiline
code box, you write real Python in it, the node runs it. Evaluate (singular) does that against
one string; Evaluates does it against a whole list and hands back a single string. That trailing
s is the whole trap - people read these as the same node and then wonder why it's complaining
about main(tag) versus main(tags).
The framing worth internalising: this is a reduce, not a map. One output, always, no matter how many strings go in.
How it works
Under the hood it's the least clever node in your install, in a good way. The class declares
INPUT_IS_LIST = True, which changes how ComfyUI schedules it: normally the engine runs a node once
per item and collects the results, but with that flag it calls the node once and hands over
everything the upstream produced. Then execute() does:
ns = {}
exec(compile(code[0], "<evaluates_code>", "exec"), ns)
main = ns.get("main")
out = main(list(tags))
Your source gets compiled and run in a fresh namespace, and the node grabs main out of it. No
imports from the pack, no wrapper class, no templating layer - plain exec, which is why the README
carries its own warning: only feed it code you trust.
Two guardrails, and they're the author's. No callable main gets you TypeError: code must define a callable main(tags: list[str]) -> str; a non-string return gets you main must return str, got <type>. Both beat silence followed by a confusing failure three nodes downstream. An IS_CHANGED
keyed on the incoming list plus your code text finishes the job: edit either and the node re-runs.
The inputs and output that matter
There are only two inputs, and you'll set both exactly once.
tags is required and set to forceInput, so there's no text box - you have to wire it, which
is the point. It's declared as a plain STRING, so ComfyUI will happily let you connect a normal
one-string output even though the node wants a list of strings. Anything that emits a string list
works: batch wildcard processors, text-file line loaders, whatever your packing nodes hand you.
code is the required multiline widget, delivered with this default:
def main(tags: list[str]) -> str:
return ", ".join(tags)
It's real CPython in there, so import re, json, itertools, pathlib all work - that's the
point over an expression evaluator like rgthree's Power Puter. A dedupe-and-sort that earns its keep:
def main(tags: list[str]) -> str:
seen, out = set(), []
for raw in tags:
for t in raw.split(","):
t = t.strip()
if t and t.lower() not in seen:
seen.add(t.lower())
out.append(t)
return ", ".join(sorted(out))
The output is one tag of type STRING, not a list. Wire it into a CLIP Text Encode, a filename
field, a save-text node - wherever you'd otherwise be typing.
Installing it
Search ComfyUI-Evaluate-Pack in ComfyUI Manager and install, or clone it:
cd ComfyUI/custom_nodes
git clone https://github.com/alchemine/comfyui-evaluate-pack
Then restart ComfyUI. The nodes land under EvaluatePack/Evaluate.
This is the rare custom node that cannot wreck your environment: dependencies = [] in its
pyproject.toml, nothing but the standard library. No pip install, no model download, no pinned
numpy to collide with anything. It does declare requires-python = ">=3.12", so on an older
3.10/3.11 environment Manager may refuse it - updating the Python your ComfyUI runs on makes it moot.
Where people get burned
You wired in a single string and got characters back. This is the number one failure. tags is
typed STRING, so nothing stops you connecting a PrimitiveString or a CLIP Text Encode text
output. The node then does main(list(tags)), and list("a, b") is ['a', ',', ' ', 'b']. If your
output looks like comma-separated letters, that's why. Check that the upstream actually produces a
list.
expected str instance, NoneType found. Classic ", ".join() bite: something in the list isn't
a string - a None from an unconnected upstream, or an empty value. Filter first:
return ", ".join(t for t in tags if isinstance(t, str) and t.strip())
"I changed it but nothing re-runs." IS_CHANGED is keyed only on the incoming list and your
code text. A body that reads something outside its arguments - random.choice, the clock, a file on
disk - hands back the cached result across runs with identical inputs. Same shape as the ancient "my
seed isn't randomising" confusion, and there's no float("NaN") escape hatch baked in. Need
freshness? Bring it in as an argument from a node that changes.
The red node with no explanation. The pack logs the traceback through its own logger
([comfyui-evaluate-pack/Evaluates]) and re-raises it, so the message you need is in the terminal
you launched ComfyUI from, not in the UI dialog. Read the console.
And the one that isn't a bug. exec is the mechanism, so the code field is arbitrary Python
with your user's privileges - same posture as the custom node you installed to get it, except the
payload now ships inside the workflow JSON. Load a stranger's workflow, hit Queue, and their code
runs. This ecosystem has already had a node pack ship credential-stealing malware; "it was in a
workflow I found" is not a safer channel than Manager.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| tags | STRING | — | |
| code | STRING | def main(tags: list[str]) -> str: return ", ".join(tags) | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| tag | STRING | — |