ComfyUI Node

Prompt Combiner

How to jam a system prompt and a user prompt into one socket

By Rakeshcool·Created 4 days ago·Updated 2 days ago· 0
Prompt Combiner
    • FINAL_PROMPT
    system_prompt
    user_prompt
    formatplain_merge
    custom_template
    strip_whitespacetrue

    Why you'd want a node this dumb

    Most text sockets in ComfyUI hold one string. A local LLM node with a proper system field and a user field is the lucky case; a CLIP Text Encode, a lot of API-wrapper nodes, and plenty of hosted text endpoints take one blob and nothing else. So you glue, and if you glue by hand you get three different dialects of "System:" across four workflows.

    Prompt Combiner is the gluing, done with a template. It takes an instruction and a request and hands back one string, in one of five shapes. That's it.

    Know when not to use it. If your downstream node has a real system/user pair of fields, wiring a pre-labeled blob into one of them double-wraps your prompt - now the model reads "System:" as part of the text. On the diffusion side, clean separation genuinely helps on a 2026 LLM-encoded checkpoint, where your prompt is a message being read by an LLM; on a CLIP model it's a token bag and labels buy you nothing. And anything you send onward to a hosted API node leaves your machine.

    How the merge works

    Resolve a template body, then substitute with plain string replacement:

    {system_prompt} -> your system text      {user_prompt} -> your user text
    

    The author deliberately does not use Python's str.format, and that's the right call. str.format treats every stray brace as a field, so the moment your instruction contains a JSON example or a snippet of code it throws or mangles the text. With plain replacement, braces in prompt text are literal and safe. The tradeoff: no format mini-syntax. {system_prompt:>10} is not padding, it's just text.

    An unrecognised format value falls back to plain_merge rather than erroring, so an old workflow with a stale format name still runs.

    The five formats

    plain_merge is two paragraphs with a blank line between them - no labels, and honestly the one I reach for when the consumer just wants a blob. labeled writes System: / User: headers, instruct writes the wordier System prompt: / User request: pair, and xml wraps both in <system_prompt> / <user_prompt> tags, which is the shape instruction-tuned chat models are most used to seeing for section boundaries. custom takes your own body from the custom_template field.

    Small local models are the real reason labeled and instruct exist: hand a tiny 3B enhancer an unlabeled two-paragraph blob and it will happily treat your instruction as the user's request and answer it.

    Inputs, output, and the gotcha that catches everyone

    system_prompt and user_prompt are both declared as forced inputs, meaning they render as sockets, not text boxes. First time you add the node there's nowhere to type. Wire the Prompt Selector's SYSTEM_PROMPT into the first one, and use a Primitive String for the second - right-click the socket area and you'll see the wiring is the only path in.

    format defaults to plain_merge. custom_template is read only when format is custom; it must contain {system_prompt} and/or {user_prompt}, or the queue errors out. strip_whitespace defaults to true and trims the outer edges of both inputs - interior structure, blank lines and all, survives untouched.

    Out comes FINAL_PROMPT (STRING) - into a CLIP Text Encode, an LLM node's prompt field, a Preview as Text, wherever. Like the Selector, this node is an output node with an inline preview, so you can queue and read the result with nothing downstream.

    Install

    Same pack as Prompt Selector, so if you installed that one you already have this node. Otherwise: ComfyUI Manager → search ComfyUI-Universal-Prompt-Selector, or

    cd ComfyUI/custom_nodes
    git clone https://github.com/Rakeshcool/ComfyUI-Universal-Prompt-Selector.git
    # restart ComfyUI; no pip dependencies, no model downloads
    

    Published September 2026 off a single commit, so treat it as young. It imports aiohttp plus stdlib - nothing new to install.

    Where people get burned

    custom selected with an empty template. You get a runtime error naming the template input, not a silent empty string. Same if your template somehow contains neither placeholder - one of the two is mandatory.

    A system prompt that contains your own tags. Substitution is not escaping. Put </system_prompt> inside a long instruction and feed it through the xml format and you've broken your own separation. Pick braces-free, tag-free wording, or use custom.

    Your instruction is now the whole message. Combine a 300-word system prompt with a user request and that's every token the model sees before generating. Structure beats volume here - a handful of tight, specific lines outperforms a padded block on every family we've measured, and the attention cap on LLM-encoded models lands around 75-100 effective tokens for the output prompt your enhancer writes.

    Expecting it to behave like a chat template. It doesn't apply one, doesn't escape role markers, doesn't know your model. It concatenates with intent. If your downstream node has a system field, use that and skip this node.

    Categoryutils

    Inputs (5)

    NameTypeDefaultDescription
    system_promptSTRINGSystem prompt - connect the Prompt Selector output here.
    user_promptSTRINGThe user request/prompt text.
    formatCOMBOplain_mergeHow to merge the two texts.
    custom_templateSTRINGUsed when format is 'custom'. Must contain {system_prompt} and/or {user_prompt} placeholders.
    strip_whitespaceBOOLEANtrueTrim surrounding whitespace from both inputs before merging.

    Outputs (1)

    NameTypeDescription
    FINAL_PROMPTSTRING