Power Prompt
ComfyUI node for building rich, conditional prompts with a variable system, weighted random selection, tag-based filtering, and Jinja2 templating.
Power Prompt
Note: The
mainbranch tracks active development and may be ahead of the latest release. Features documented here might not be available in an older installed version — check the releases page or the CHANGELOG to see what's in the version you have.
A note from me: I built this mostly for myself - I kept running into the same frustration with other prompt nodes: either too little control or too much friction to use comfortably. I wanted a clean UI that made it fun to experiment with options, and I may have gotten a little carried away. It's a hobby project, but I'm genuinely open to feature requests and bug reports.
A ComfyUI node for building rich, dynamic prompts from a simple YAML definition. Define named variables with weighted options, conditional filtering, and Jinja2 templating — then let the node assemble your prompt at generation time.
Features
- Variables — define named
select,text, orinputvariables and reference them in a Jinja2 prompt template - Weighted options — give options a
weightto control how often they're randomly chosen - Count ranges — pick exactly N, a random range (
1-3), or any number (any) of options - Option-level
when/unless— Jinja2 expressions that control which options are available for a given generation - Variable-level
when/unless— skip an entire variable (resolve it to empty) when a condition fails; works on all variable types includinginput - Option-level tags — attach semantic labels to specific option values; downstream
when/unlessexpressions can checkstyle_tags,season_tags, etc. - Variable-level tags — tags scoped to the variable itself, emitted for any resolved value; option-level tags are an inner scope emitted only for the specific value chosen
- Multi-value options — a single option entry can expand to multiple values sharing the same weight, tags, and
when/unless - UI controls —
label:sets a custom display name,group:organises variables into named sections,hidden:hides a variable from the UI while still resolving it - Prompt fragments — define a
fragments:mapping of named Jinja2 sub-templates rendered after variables resolve; reference them as{{ fragment.name }}in any prompt - Partials — wire one or more Power Prompt Partial nodes into include slots to share
variables:andfragments:across compositions - Inline includes — declare file-based partial dependencies directly in any YAML with
includes:; resolved transitively and merged before the prompt runs, no extra nodes required - Global
tags— a synthetictagsvariable accumulates every tag emitted so far across all variables; branch on any tag without knowing which variable produced it
Installation
- Clone or copy this folder into
ComfyUI/custom_nodes/comfyui-power-prompt - Install Python dependencies:
pip install -r requirements.txt - Restart ComfyUI — the Power Prompt node appears under the
promptcategory
Local Development
Prerequisites: Node.js 18+
npm install # also installs the Husky pre-commit hook
There are two JavaScript bundles, both built with esbuild:
| Script | Input | Output | When to rebuild |
|---|---|---|---|
| npm run build-editor | scripts/cm-entry.mjs | web/js/vendor/codemirror-bundle.js | CodeMirror version bump or new editor extensions |
| npm run build-node | src/index.js | web/js/power-prompt-node.js | Any change under src/ |
| npm run build | both | both | — |
The Husky pre-commit hook runs build-node and stages the output automatically, so you never need to remember to rebuild before committing frontend changes. Rebuild build-editor manually if you touch the CodeMirror setup, and commit the result alongside your other changes.
YAML Reference
A Power Prompt YAML has three top-level keys:
variables: # required — named inputs resolved at generation time
...
fragments: # optional — named sub-templates composed from resolved variables
...
prompt: | # required — Jinja2 template rendered after all variables resolve
...
Variables
Each entry under variables: becomes a UI control and a template variable.
Variable fields
| Field | Type | Default | Description |
|---|---|---|---|
| type | select | text | input | — | Required. select picks from a list; text is a free-text input; input creates a node socket. Aliases: choice (= select, count 1), multiselect (= select, count any) |
| when | Jinja2 expression | (always resolves) | Skip the entire variable when this evaluates to false — the value becomes "" and no tags are emitted. References variables declared above this one |
| unless | Jinja2 expression | (never skipped) | Skip the entire variable when this evaluates to true. Checked after when; both can be combined |
| tags | string | list | [] | Tags emitted whenever this variable resolves to a non-empty value, regardless of which option was chosen. Added to <varname>_tags and the global tags list |
| count | int | range | any | 1 | How many options to pick (select only). 1 = single dropdown; 0-3 = random range; any = user checks all they want |
| options | list | — | Required for select. The pool of values to choose from |
| label | string | key name | Custom display name shown in the UI panel. Underscores in the key name are replaced with spaces when no label is set |
| group | string | — | Groups this variable under a named section header in the UI. All variables sharing the same string are rendered together, before ungrouped variables |
| hidden | bool | false | When true, the variable is not shown in the UI. It is still resolved randomly and available in the prompt and when/unless expressions |
type: select
Picks one or more values from the options list:
variables:
season:
type: select
count: 1 # exactly one — renders as a dropdown
options:
- spring
- summer
- autumn
- winter
accessories:
type: select
count: 0-2 # zero to two — renders as a chip group
options:
- scarf
- sunglasses
- umbrella
style:
type: multiselect # alias: count is implicitly "any" — user checks what they want
options:
- anime
- watercolor
- oil painting
type: text
A free-text input field. The value is whatever the user types:
variables:
notes:
type: text
label: "Extra notes" # shown in the UI as "Extra notes" instead of "notes"
type: input
Adds a named input socket to the ComfyUI node (accepts any output type). The connected node's value is passed as a string into the template. If nothing is connected the variable resolves to "".
No UI control is rendered in the panel — the socket is the interface. Variable-level when, unless, and tags all work normally:
variables:
character:
type: select
count: 1
options: [warrior, mage]
character_lora:
type: input
when: "character == 'mage'" # only use the socket value when relevant
tags: [lora_active] # downstream vars can check 'lora_active' in tags
prompt: |
{{ character }},
{% if character_lora %}{{ character_lora }},{% endif %}
masterpiece
Variable-level when, unless, and tags
These fields apply to the variable as a whole, not to individual options. They work on every type — select, text, and input.
when / unless — skip the entire variable when the condition fails. The resolved value becomes "" (or [] for multi-pick), no tags are emitted, and subsequent variables can still reference it safely:
variables:
season:
type: select
count: 1
options: [winter, summer]
winter_mood:
type: select
count: 1
when: "season == 'winter'" # whole variable skipped unless it's winter
options: [cozy, frosty, serene]
tags — emitted to the global tags list and <varname>_tags whenever the variable resolves to any non-empty value. Combined with option-level tags, not instead of them:
variables:
style:
type: select
count: 1
tags: [has_style] # always added when style resolves non-empty
options:
- value: anime
tags: [japanese] # added only when anime is chosen
- value: watercolor
tags: [traditional]
artist:
type: select
count: 1
options:
- value: generic artist
- value: Makoto Shinkai
when: "'japanese' in style_tags"
- value: special artist
when: "'has_style' in tags" # gates on variable-level tag
label, group, and hidden
variables:
internal_season:
type: select
hidden: true # resolved randomly but not shown in the UI panel
options: [spring, summer, autumn, winter]
outfit:
type: select
label: "Outfit Style" # displayed as "Outfit Style" instead of "outfit"
group: "Character" # appears under a "Character" section header in the UI
options:
- casual
- formal
- fantasy
setting:
type: select
group: "Character" # shares the "Character" group with outfit above
options:
- city street
- forest path
- rooftop
Options
Each entry in an options: list is either a plain string (shorthand) or a mapping with any of the following fields:
| Field | Type | Default | Description |
|---|---|---|---|
| value | string | list | — | The option text. A list expands to multiple independent entries sharing the same weight, when, and tags |
| weight | float | 1.0 | Relative selection probability. Higher = chosen more often |
| when | Jinja2 expression | (always included) | Option is added to the random pool only when this evaluates to true |
| unless | Jinja2 expression | (never excluded) | Option is removed from the random pool when this evaluates to true |
| tags | string | list | [] | Semantic labels attached to this value, available as {varname}_tags in downstream when/unless expressions |
options:
- simple string option # shorthand — weight 1, no conditions
- value: weighted option
weight: 3 # 3× more likely than weight-1 options
- value: conditional option
when: "season == 'winter'" # only available when season is winter
- value: excluded option
unless: "'rainy' in weather_tags" # excluded when the weather has a rainy tag
- value: # multi-value: share weight, when, tags
- option variant a
- option variant b
weight: 2
tags: [shared_tag]
when and unless expressions
Expressions are evaluated using the Jinja2 sandbox — attribute access on unsafe objects is blocked. Variables resolved above the current one in the YAML are available by name.
when and unless can appear at two levels:
- Option level — filters which options are included in the random pool for that generation
- Variable level — skips the entire variable (value becomes
"", no tags emitted) when the condition fails
when: "season == 'winter'"
when: "'cold' in season_tags"
when: "'student' in character_tags and 'cold' not in season_tags"
when: "time_of_day in ('dusk', 'night')"
when: "len(accessories) > 0"
unless: "season == 'summer'"
unless: "'rainy' in weather_tags"
Available names in expressions:
| Name | Value |
|---|---|
| <varname> | Resolved string (single-pick) or list (multi-pick) |
| <varname>_tags | List of tags from the resolved value(s), deduplicated in encounter order |
| tags | Accumulated list of every tag emitted by all variables resolved so far, deduplicated in encounter order. Useful when you need to branch on any tag regardless of which variable produced it |
Available functions: len, any, all, min, max, str, int, float, bool, abs, round
Jinja2 notes:
- Use lists
[x, y]instead of set literals{x, y}— sets are not supported - Generator expressions (
any(x for x in y)) are not supported — useinmembership checks instead - Both
whenandunlesscan appear on the same option — the option is included only ifwhenis true andunlessis false - Variables must be declared above any variable whose
when/unlessreferences them tagsand all<varname>_tagslists are also available infragments:and theprompt:template after all variables resolve
Prompt fragments
Define named Jinja2 sub-templates that compose from resolved variables. Reference them in the prompt (or in later fragments) as {{ fragment.name }}.
fragments:
location: "{{ city }} at {{ time_of_day }}"
full_scene: "{{ fragment.location }}, {{ weather }}" # earlier fragment available here
prompt: |
{{ character }}, {{ fragment.full_scene }}, masterpiece
Fragments are rendered in declaration order after all variables resolve. Fragments from connected Partials are merged in — the main YAML wins on name collisions.
Partials
Partials contribute variables: and fragments: into a main node — useful for reusable character, location, or style libraries. There are two ways to bring a partial into a composition:
Wired partials — connect Power Prompt Partial or Power Prompt File Partial nodes into a main node's include_1, include_2, … input slots in the ComfyUI graph.
Inline includes — declare file dependencies directly inside any YAML with a top-level includes: list. Files are resolved from the registered power-prompt partials folder (the same folder that Power Prompt File Partial uses).
includes:
- characters/alice.yaml
- styles/anime.yaml
variables:
subject:
type: select
options: [1girl, 1boy]
prompt: |
{{ subject }}, {{ fragment.style_block }}
Any YAML — main prompt, wired partial, or included file — can declare includes:. Imports are resolved transitively: if an included file itself has an includes: list, those files are loaded too. The same file is never loaded more than once regardless of how many times it appears in the graph (cycles are safe).
Merge order and priority
All sources are merged in a fixed order before the prompt renders. The rightmost source wins on any name collision:
included files → wired partials (include_1 … include_N) → main YAML
| Source | Priority | Notes |
|---|---|---|
| Included files | Lowest | Declared via includes:; earlier entries in the list lose to later ones |
| Wired partials | Middle | include_1 < include_2 < … (higher number wins) |
| Main YAML | Highest | Always overrides everything |
This applies to both variables: and fragments:.
Variable resolution order and cross-partial when/unless
Variables are resolved in the same order as the merge order above — wired-partial variables are evaluated before imported variables. This matters for cross-partial when/unless expressions:
# character.yaml (wired as include_1) — defines char_archetype with tags
variables:
char_archetype:
type: select
options:
- value: student
tags: [student]
# action-pose.yaml (imported) — options filtered by char_archetype_tags
variables:
action:
type: select
options:
- value: studying, surrounded by books
when: "'student' in char_archetype_tags" # ← works because wired partials
- value: gazing into the distance # resolve before includes
Because wired partials resolve first, their tags are already in eval_context when imported variables are processed. If you need a variable defined in a wired partial to gate options in an included file, always supply that variable via a wired partial, not another import.
The prompt: key is not meaningful in a partial and is ignored.
Node outputs
| Socket | Content |
|---|---|
| prompt | Normalized — newlines collapsed, whitespace trimmed, commas cleaned |
| raw_prompt | Unprocessed rendered output from the Jinja2 template |
See examples/ for worked examples and docs/prompt-schema.yaml for the full annotated field reference.
Examples
See the examples/ folder:
basic.yaml— two variables, minimal promptstandard.yaml— weighted options, tags,when/unless, count ranges, text variableadvanced.yaml— full feature showcase includingfragments:action-pose.yaml— reusable partial: shot type, pose, and action variables with tag-driven conditionals; exposes{{ fragment.pose_description }}includes-demo.yaml— demonstrates inlineincludes:, pullingaction-pose.yamlinto a main prompt without wiring any extra nodesPower Prompt - Example.json— full ComfyUI workflow with wired partials and a file partial
License
GPL-3.0-or-later — see LICENSE