String Formatter
Stop pasting the same tag wall into five nodes — build your prompt from named parts instead
- dict
- string
If you've ever kept three copies of "score_9, score_8_up, score_7_up" alive across your workflow and edited one and not the others, you already know why this node exists. The String Formatter from Lex-DRL's String Constructor pack lets you write a prompt template once, referencing named chunks of text like {char1_long} and {neg_common}, and have every placeholder filled from a single shared dictionary. One input, one text field, one string out. It's the "bus" for text: build your library of prompt parts once, then remix it into as many variations as you want downstream.
This is the pack's headline node, and its niche is regional prompting and multi-character scenes. When two characters share a canvas, their attributes bleed together (the regional-prompting literature is basically one long complaint about this), and the fix is having distinct, reusable chunks per subject that you swap in per region. String Formatter is exactly that without any wires going everywhere - you edit one chunk and every region that references it updates.
How it works
The mechanism is blessedly boring: under the hood it calls Python's built-in str.format_map() on your template with the dictionary as arguments. That means the syntax is Python string formatting, which is a tiny, well-documented language. You write {key_name} (no spaces inside the braces) and it gets replaced by the chunk of that name. Want fancier? Format specs like {float_value:.3f} work too, if you feed non-string values in via an "Add ANY" node.
Two toggles give it superpowers. recursive_format lets chunks reference other chunks - build a hierarchy like bad_human_long → {bad_anatomy_short}, {bad_hands_short}, {bad_face_short} and only touch the leaves to change the whole tree. That's how people build resolution-scaled detail sets or toggle-able negative prompts. safe_format (on by default) is the one beginners should keep on: if a {pattern} can't be formatted - the key's missing, or it's a JSON/CSS block with literal braces - it leaves that text alone instead of throwing an error. If you actually need literal { or } in the output, write them doubled: {{ and }}.
One honest warning about recursion: it's iterative under the hood, capped around 1000 passes, and it will raise a RecursionError if your chunks cross-reference each other in a loop. The cap keeps you safe, but it's the one way this node can genuinely bite you.
The inputs that matter
- template - the multiline text field with your
{placeholders}. This is the whole point. - recursive_format - off by default; flip it on once your chunks start referencing each other.
- safe_format - leave on unless you're deliberately building dynamic patterns and want errors.
- show_status - a preview toggle that prints the final string right on the node. Turn it off when the workflow gets busy.
- dict (optional) - the DICT input with your named chunks. Leave it unconnected and the node behaves like a plain string primitive.
The single output is string, which feeds straight into a CLIP Text Encode node (or a ConditioningCombine for regional setups).
Install
ComfyUI Manager → search "String Constructor" → Install, or:
cd ComfyUI/custom_nodes
git clone https://github.com/Lex-DRL/ComfyUI-StringConstructor
Then restart ComfyUI. The whole dependency footprint is a single pure-Python package (frozendict) - no model downloads, no torch extensions, nothing to babysit. You need ComfyUI 0.18+. The pack is genuinely minimal by design; the author's whole framing is "do one thing and do it well," and it shows.
Where people get burned
First, key names are Python identifiers: only ASCII letters, digits, and underscores, and no leading digit. 1boy is invalid; boy1 is fine. The node warns you, but it's the most common first stumble. Second, BREAK is not natively supported by Comfy's CLIP Text Encode - the README points you to CLIPTextEncode with BREAK or smZNodes' CLIP Text Encode++ if you want A1111-style region breaks. And one structural gotcha that surprises people: since v3.x, the dictionary-building nodes (Dict from Text, Add String to Dict, etc.) were split into a separate pack, ComfyUI-DictTools. This pack just formats; if you want the nodes that turn a wall of text into the dict, grab Dict Tools alongside it.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| template | STRING | Type the text template. To reference named substrings from format-dictionary, use this syntax: {substring_name}. For example: score_9, score_8_up, score_7_up, {char1_short}, standing next to {char2_short}, {char1_long} {char2_long} | |
| recursive_format | BOOLEAN | false | Do recursive format - i.e., allow the chunks from the dictionary to reference other chunks. |
| safe_format | BOOLEAN | true | If template contains an invalid {text pattern} which can't be formatted - leave it as-is (instead of throwing an error). Safe mode is recommended for templates with JSON, CSS, or other literal curly brackets. |
| show_status | BOOLEAN | true | Show the final string constructed from the text-template and format-dictionary? |
| dictopt | DICT | The dictionary to take named sub-strings from. It could be left unconnected, if the pattern doesn't reference any sub-strings - then, this node acts exactly the same as a regular string-primitive node. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| string | STRING | — |