Nodes/Macro Nodes/Concat Text (Macro)
ComfyUI Node

Concat Text (Macro)

Ten slots, any type, and a delimiter that can actually hold a newline

By the-Macro-Man·Created 2 months ago·Updated 15 days ago· 1
Concat Text (Macro)
  • text1
  • text2
  • text3
  • text4
  • text5
  • text6
  • text7
  • text8
  • text9
  • text10
  • concat
  • block_count
◄delimiter\n\n►
◄labelsfalse►
◄trimtrue►
◄skip_emptytrue►

String concatenation is the least glamorous node in ComfyUI and the one you end up needing most. You build a prompt chain - an enhancer writes a prompt, a captioner adds a description, you bolt a fixed style suffix on the end - and suddenly those three pieces need to be one wire. Every pack ships a concat node. Almost all of them annoy you within a week.

What the existing concat nodes get wrong

They're typed STRING-only. ComfyUI's type checker won't let you drag an OLLAMA_META or OLLAMA_HISTORY output into a STRING socket, so the standard workaround is inserting a Preview as Text node purely to launder the type. In a pipeline where the metadata is the thing you want logged, you end up with a Preview node in the middle of the chain for no reason but satisfying an enum.

The delimiter can't hold a newline. This is the more subtle one. Several concat nodes special-case the exact two-character string \n and pass everything else through literally, so \n\n (blank line between blocks) or \n----\n (a visible rule) comes out as backslash-n-etc in your prompt. You wanted formatting, you got punctuation.

And there's a capacity question. The most-installed concat node people reach for tops out at five slots, which is why you find someone asking how to get "about 10" sources in, and the answer is "plug another concat node behind it, giving you nine." That works. It's also a stack of nodes for a thing that should be one.

Concat Text (Macro) answers all three complaints in one 10-slot node. It's part of the macronodes pack (publisher the-macroman, MIT), which is deliberately tiny: four quality-of-life nodes, no network code, no eval/exec/subprocess, standard library only.

How it works

The slots are declared with ComfyUI's wildcard type, * - the escape hatch in the type system the docs call ANY, where a socket will accept anything and it's up to the node to make sense of it. So text1 through text10 all accept any output, and the node serialises whatever arrives: strings pass through untouched, numbers and booleans get stringified, and anything structured is dumped as indented JSON, deliberately matching what core's PreviewAny prints so the output looks like what you're already used to seeing in a Preview node.

The delimiter is where the real work is. Escapes are expanded anywhere in the string, not just when the whole value is \n. The implementation parks a backslash on a sentinel character first, so a genuine \\n survives instead of being turned into a newline by accident.

Four toggles, all required, all with sane defaults:

  • delimiter - defaults to \n\n, a blank line between blocks. Use \n----\n if you want a visible rule between sections.
  • labels - pads each block with a === text2 === header. Off by default; first thing to switch on when a chain goes wrong and you can't tell which input did it.
  • trim and skip_empty - both on by default. Trim strips leading/trailing whitespace per block; skip_empty drops unconnected or blank slots entirely instead of leaving a stray delimiter for each one.

Two outputs. concat is your joined STRING - wire it into a CLIP Text Encode, an LLM node's prompt, or the text input on a save node if you want the exact prompt recorded beside the file. block_count is an INT you'll mostly ignore, though it's a cheap sanity check: expected four blocks, got one, so an upstream branch didn't run.

The node also logs [MacroNodes] Concat: 4 block(s), 812 chars on every run, which beats adding a Preview node when you're not sure whether the problem is your prompt or your wiring.

Installing it

Nothing to compile and nothing to pip install - the repo has no requirements file, and the only imports beyond the standard library are ComfyUI's own.

cd ComfyUI/custom_nodes
git clone https://github.com/the-Macro-Man/ComfyUI-MacroNodes

Restart ComfyUI. Or: ComfyUI Manager → search Macro Nodes, or comfy node install macronodes from the CLI. The node lands under MacroNodes/utils.

Where people get burned

Windows paths and backslashes. Because \n, \t and \r are expanded anywhere in the delimiter, a single backslash before an n is always a newline - including in a path like C:\newfolder, which becomes C: plus a line break. The tooltip says it: use \\ for a literal backslash. It's still the thing that'll bite you once.

Feeding serialised metadata into a prompt. Wire OLLAMA_HISTORY into text7 and on into a CLIP text encode and you're putting pretty-printed JSON in your conditioning. The wildcard slots are for logging and inspection. Turn labels on and look at what you're actually passing before you blame the sampler.

The target widget isn't a socket yet. Lots of text fields - including CLIP Text Encode's text - ship as widgets. Right-click the node and pick Convert widget to input before you can connect concat to it. Trips up everyone the first time.

Caching is normal. No IS_CHANGED override, so change an upstream value and it re-runs, don't and it doesn't. Cheap to leave in a big graph.

CategoryMacroNodes/utils

Inputs (14)

NameTypeDefaultDescription
delimiterSTRING\n\nText placed between blocks. \n, \t and \r are expanded anywhere in the value, so \n----\n works. Use \\ for a literal backslash.
labelsBOOLEANfalsePrint the socket name above each block. Renaming an input changes its label, so they stay meaningful.
trimBOOLEANtrueStrip leading and trailing whitespace from each block before joining.
skip_emptyBOOLEANtrueLeave unconnected or blank slots out entirely. Off means they contribute an empty block and a delimiter.
text1opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text2opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text3opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text4opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text5opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text6opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text7opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text8opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text9opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.
text10opt*Any type. Strings pass through; anything else is serialised, so OLLAMA_META and OLLAMA_HISTORY can connect straight in.

Outputs (2)

NameTypeDescription
concatSTRING—
block_countINT—