
Comfyui Workflow Builder
- 1.5k installs
- 85 repo stars
- Updated March 18, 2026
- mckruz/comfyui-expert
comfyui-workflow-builder is an agent skill that generates valid ComfyUI workflow JSON from natural language with inventory validation.
About
The comfyui-workflow- skill translates natural language requests into executable ComfyUI workflow JSON with correct class_types, connections, and output indices. It reads state/inventory.json for available checkpoints, LoRAs, ControlNet models, and custom nodes before selecting pipeline patterns for txt2img, img2img, inpainting, identity preservation, video, upscale, and FaceDetailer flows. Workflow JSON uses string node IDs with array connection format source_id and output_index. Validation checks class_type existence, model filenames, required connections, VRAM estimates, and resolution compatibility such as 1024 for FLUX and SDXL. Use when developers need ComfyUI node graphs from prompts without installation or custom node development guidance.
- Inventory-first: read state/inventory.json before generating workflows.
- Pipeline patterns: txt2img, InstantID, LoRA, Wan I2V, inpaint, upscale.
- JSON format: string node IDs with class_type and array connections.
- VRAM estimation table for FLUX, SDXL, InstantID, ControlNet, Wan.
- Validation: class_types, model files, connections, VRAM, resolution.
Comfyui Workflow Builder by the numbers
- 1,492 all-time installs (skills.sh)
- +56 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #798 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
comfyui-workflow-builder capabilities & compatibility
- Capabilities
- natural language to workflow json · inventory aware model selection · pipeline pattern selection · node graph validation
- Use cases
- orchestration · research
What comfyui-workflow-builder says it does
Translates natural language requests into executable ComfyUI workflow JSON.
Always validates against inventory before generating.
npx skills add https://github.com/mckruz/comfyui-expert --skill comfyui-workflow-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 85 |
| Security audit | 2 / 3 scanners passed |
| Last updated | March 18, 2026 |
| Repository | mckruz/comfyui-expert ↗ |
How do I build a ComfyUI workflow JSON for txt2img, img2img, or video from a prompt?
Generate valid ComfyUI workflow JSON from natural language with inventory-aware node graphs.
Who is it for?
Developers and agents generating ComfyUI pipelines for image or video generation.
Skip if: Skip for ComfyUI installation, custom node development, or model training without workflow JSON needs.
When should I use this skill?
User asks to generate ComfyUI workflow, build node graph, or create txt2img img2img pipeline.
What you get
Validated ComfyUI workflow JSON with correct nodes, connections, and model filenames from inventory.
- ComfyUI workflow JSON file
- Validated node graph with correct output indices
By the numbers
- Includes 7 evaluation test cases with 5 happy-path and 1 failure-mode case
- Targets 85% workflow correctness versus a 30% baseline without the skill
- Documents roughly 70% reduction in manual JSON correction
Files
ComfyUI Workflow Builder
Translates natural language requests into executable ComfyUI workflow JSON. Always validates against inventory before generating.
Workflow Generation Process
Step 1: Understand the Request
Parse the user's intent into:
- Output type: Image, video, or audio
- Source material: Text-only, reference image(s), existing video
- Identity method: None, zero-shot (InstantID/PuLID), LoRA, Kontext
- Quality level: Draft (fast iteration) vs production (maximum quality)
- Special requirements: ControlNet, inpainting, upscaling, lip-sync
Step 2: Check Inventory
Read state/inventory.json to determine:
- Available checkpoints → select best match for task
- Available identity models → determine which methods are possible
- Available ControlNet models → enable pose/depth control if available
- Custom nodes installed → verify all required nodes exist
- VRAM available → optimize settings accordingly
Step 3: Select Pipeline Pattern
Based on request + inventory, choose from:
| Pattern | When | Key Nodes |
|---|---|---|
| Text-to-Image | Simple generation | Checkpoint → CLIP → KSampler → VAE |
| Identity-Preserved Image | Character consistency | + InstantID/PuLID/IP-Adapter |
| LoRA Character | Trained character | + LoRA Loader |
| Image-to-Video (Wan) | High-quality video | Diffusion Model → Wan I2V → Video Combine |
| Image-to-Video (AnimateDiff) | Fast video, motion control | + AnimateDiff Loader + Motion LoRAs |
| Talking Head | Character speaks | Image → Video → Voice → Lip-Sync |
| Upscale | Enhance resolution | Image → UltimateSDUpscale → Save |
| Inpainting | Edit regions | Image + Mask → Inpaint Model → KSampler |
Step 4: Generate Workflow JSON
ComfyUI workflow format:
{
"{node_id}": {
"class_type": "{NodeClassName}",
"inputs": {
"{param_name}": "{value}",
"{connected_param}": ["{source_node_id}", {output_index}]
}
}
}Rules:
- Node IDs are strings (typically "1", "2", "3"...)
- Connected inputs use array format:
["source_node_id", output_index] - Output index is 0-based integer
- Filenames must match exactly what's in inventory
- Seed values: use random large integer or fixed for reproducibility
Step 5: Validate
Before presenting to user:
1. Every class_type exists in inventory's node list 2. Every model filename exists in inventory's model list 3. All required connections are present (no dangling inputs) 4. VRAM estimate doesn't exceed available VRAM 5. Resolution is compatible with chosen model (512 for SD1.5, 1024 for SDXL/FLUX)
Step 6: Output
If online mode: Queue via comfyui-api skill If offline mode: Save JSON to projects/{project}/workflows/ with descriptive name
Workflow Templates
Basic Text-to-Image (FLUX)
{
"1": {
"class_type": "LoadCheckpoint",
"inputs": {"ckpt_name": "flux1-dev.safetensors"}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "{positive_prompt}", "clip": ["1", 1]}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "{negative_prompt}", "clip": ["1", 1]}
},
"4": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
},
"5": {
"class_type": "KSampler",
"inputs": {
"seed": 42,
"steps": 25,
"cfg": 3.5,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["4", 0]
}
},
"6": {
"class_type": "VAEDecode",
"inputs": {"samples": ["5", 0], "vae": ["1", 2]}
},
"7": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": "output", "images": ["6", 0]}
}
}With Identity Preservation (InstantID + IP-Adapter)
Extends basic template by adding:
- Load reference image node
- InstantID Model Loader + Apply InstantID
- IPAdapter Unified Loader + Apply IPAdapter
- FaceDetailer post-processing
See references/workflows.md for complete node settings.
Video Generation (Wan I2V)
Uses different loader chain:
- Load Diffusion Model (not LoadCheckpoint)
- Wan I2V Conditioning
- EmptySD3LatentImage (with frame count)
- Video Combine (VHS)
See references/workflows.md Workflow 4 for complete settings.
VRAM Estimation
| Component | Approximate VRAM |
|---|---|
| FLUX FP16 | 16GB |
| FLUX FP8 | 8GB |
| SDXL | 6GB |
| SD1.5 | 4GB |
| InstantID | +4GB |
| IP-Adapter | +2GB |
| ControlNet (each) | +1.5GB |
| Wan 14B | 20GB |
| Wan 1.3B | 5GB |
| AnimateDiff | +3GB |
| FaceDetailer | +2GB |
Common Mistakes to Avoid
1. Wrong output index: CheckpointLoader outputs [model, clip, vae] at indices [0, 1, 2] 2. CFG too high for InstantID: Use 4-5, not default 7-8 3. Wrong resolution for model: FLUX/SDXL=1024, SD1.5=512 4. Missing VAE: FLUX needs explicit VAE (ae.safetensors) 5. Wrong model in wrong loader: Diffusion models need LoadDiffusionModel, not LoadCheckpoint
Reference Files
references/workflows.md- Detailed node-by-node templatesreferences/models.md- Model files and pathsreferences/prompt-templates.md- Model-specific promptsstate/inventory.json- Current inventory cache
skill_type: capability_uplift
baseline_expected_score: "30%"
with_skill_target_score: "85%"
token_overhead_acceptable: "25%"
manual_correction_reduction: "70%"
test_case_count: 7
happy_path_cases: 5
failure_mode_cases: 1
comparison_criteria:
- criterion: "All class_types reference real ComfyUI nodes"
weight: 0.25
assertion_types: [contains, json_valid]
anchor: "node name string match and JSON validity"
- criterion: "Output indices are correct for each node type (e.g., CheckpointLoader [0]=MODEL, [1]=CLIP, [2]=VAE)"
weight: 0.25
assertion_types: [structure_check, not_contains]
anchor: "semantic — requires JSON graph analysis; anti-pattern exclusion anchored"
- criterion: "No dangling inputs — every required connection is wired"
weight: 0.20
assertion_types: [structure_check]
anchor: "semantic — requires full graph traversal"
- criterion: "Resolution matches the selected model's training resolution"
weight: 0.15
assertion_types: [regex, contains, not_contains]
anchor: "resolution numeric value and anti-pattern exclusion"
- criterion: "VRAM estimate is reasonable for the workflow complexity"
weight: 0.10
assertion_types: [structure_check]
anchor: "semantic — requires understanding workflow complexity"
- criterion: "Model filenames reference known/real checkpoint files"
weight: 0.05
assertion_types: [structure_check]
anchor: "semantic — requires knowledge of real model filenames"
comfyui-workflow-builder — Eval Configuration
Classification
- Type: Capability Uplift
- Category: Structured workflow generation from natural language with hardware-aware validation
What "Good" Looks Like
1. Output is valid ComfyUI workflow JSON with correct node IDs, class_types, and connections 2. All class_types reference nodes that actually exist in ComfyUI (no hallucinated node names) 3. Model filenames match real checkpoint/LoRA files (e.g., juggernautXL_v9.safetensors, not invented names) 4. Connections are correct — no dangling inputs, output indices match node output slots (e.g., CheckpointLoader outputs [MODEL, CLIP, VAE] at indices [0, 1, 2]) 5. Resolution matches the selected model's training resolution (1024x1024 for SDXL, 512x512 for SD1.5, etc.) and VRAM estimate is reasonable for the hardware
Known Limitations
- Cannot verify at generation time whether the user actually has a specific model installed
- Custom node availability varies per installation — skill uses common nodes but can't guarantee all are present
- VRAM estimates are approximations based on typical configurations
Benchmark Strategy
- Without skill: Base Claude produces plausible-looking JSON but frequently hallucinates node class_types, uses wrong output indices, and ignores resolution/model compatibility
- With skill: Generates validated workflows with correct node names, proper output slot indices, model-appropriate resolutions, and VRAM estimates
- Key differentiator: Output index correctness and node class_type accuracy — the difference between a workflow that loads vs. one that errors immediately
Security — Eval Sandboxing
Eval runs use real tool access and may expose secrets in output. Results are gitignored. Use --allowedTools "Read,Glob,Grep" to prevent modification during eval runs.
Running Evals
bash eval/run-eval.sh # Full run (with-skill + baseline)
bash eval/run-eval.sh --skill-only # With-skill only
bash eval/run-eval.sh --case TC-001 # Single test caseRetirement Signal
When base Claude consistently produces ComfyUI workflows with correct output indices, valid class_types from the actual node registry, and model-appropriate resolutions without needing the skill's node/model reference data.
# Eval results may contain secrets from real config files.
*
!.gitignore
!.gitkeep
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════
# Skill Eval Runner — Shared Template v2.0
# ═══════════════════════════════════════════════════════════════════════
# Runs test cases against a skill and captures results for scoring.
# Copy this file into any skill's eval/ directory and set SKILL_NAME.
#
# Usage:
# bash eval/run-eval.sh # Full run (with-skill + baseline)
# bash eval/run-eval.sh --skill-only # Skip baseline comparison
# bash eval/run-eval.sh --case TC-001 # Single test case
# bash eval/run-eval.sh --baseline-only # Baseline only (no skill)
# bash eval/run-eval.sh --score-only # Score existing results (no new runs)
#
# Assertion types supported:
# contains(target) — response includes target (case-insensitive)
# not_contains(target) — response does NOT include target
# regex(pattern) — response matches extended regex
# question_before_code — a "?" appears before first ``` fence
# json_valid — response has a parseable JSON block
# json_fields(f1,f2,...) — JSON block contains required field names
# token_limit(N) — response under ~N tokens (estimated from words)
# range_check(expr) — evaluates numeric expression on JSON output
# word_count(field,min,max)— checks word count of a JSON field
# ═══════════════════════════════════════════════════════════════════════
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
RESULTS_DIR="$SCRIPT_DIR/results/$TIMESTAMP"
# ── CONFIGURE THIS ──────────────────────────────────────────────────
# Override by setting SKILL_NAME env var or editing this default.
SKILL_NAME="${SKILL_NAME:-$(basename "$PROJECT_DIR")}"
# ────────────────────────────────────────────────────────────────────
# Parse args
RUN_SKILL=true
RUN_BASELINE=true
SCORE_ONLY=false
SINGLE_CASE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--skill-only) RUN_BASELINE=false; shift ;;
--baseline-only) RUN_SKILL=false; shift ;;
--score-only) SCORE_ONLY=true; shift ;;
--case) SINGLE_CASE="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
mkdir -p "$RESULTS_DIR/with-skill" "$RESULTS_DIR/baseline" "$RESULTS_DIR/prompts"
echo "=== Skill Eval Runner v2.0 ==="
echo "Skill: $SKILL_NAME"
echo "Timestamp: $TIMESTAMP"
echo "Results: $RESULTS_DIR"
echo ""
# ─── Extract test cases from YAML ──────────────────────────────────
extract_cases() {
local yaml_file="$SCRIPT_DIR/test-cases.yaml"
local current_id=""
local current_prompt=""
local in_prompt=false
local ids=()
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^-\ id:\ (.+) ]]; then
if [[ -n "$current_id" ]]; then
ids+=("$current_id")
printf '%s' "$current_prompt" > "$RESULTS_DIR/prompts/${current_id}.txt"
fi
current_id="${BASH_REMATCH[1]}"
current_prompt=""
in_prompt=false
fi
if [[ "$line" =~ ^\ \ prompt:\ \"(.+)\"$ ]]; then
current_prompt="${BASH_REMATCH[1]}"
in_prompt=false
fi
if [[ "$line" =~ ^\ \ prompt:\ [\|>]$ ]]; then
in_prompt=true
current_prompt=""
continue
fi
if $in_prompt; then
if [[ "$line" =~ ^\ \ [a-z] && ! "$line" =~ ^\ \ \ \ ]]; then
in_prompt=false
else
local stripped="${line# }"
current_prompt+="${stripped}"$'\n'
fi
fi
done < "$yaml_file"
if [[ -n "$current_id" ]]; then
ids+=("$current_id")
printf '%s' "$current_prompt" > "$RESULTS_DIR/prompts/${current_id}.txt"
fi
echo "${ids[@]}"
}
CASE_IDS=($(extract_cases))
echo "Found ${#CASE_IDS[@]} test cases: ${CASE_IDS[*]}"
if [[ -n "$SINGLE_CASE" ]]; then
CASE_IDS=("$SINGLE_CASE")
echo "Filtering to: $SINGLE_CASE"
fi
echo ""
# ─── Run test cases ────────────────────────────────────────────────
run_case() {
local case_id="$1"
local mode="$2"
local prompt_file="$RESULTS_DIR/prompts/${case_id}.txt"
local output_file="$RESULTS_DIR/$mode/${case_id}.md"
local prompt_text
prompt_text=$(cat "$prompt_file")
echo " [$mode] Running $case_id..."
if [[ "$mode" == "with-skill" ]]; then
local full_prompt="Use the $SKILL_NAME skill to answer this: $prompt_text"
claude -p "$full_prompt" \
--allowedTools "Read,Glob,Grep" \
--max-turns 3 \
--output-format text \
> "$output_file" 2>/dev/null || {
echo "EVAL_ERROR: claude command failed for $case_id ($mode)" > "$output_file"
}
else
claude -p "$prompt_text" \
--allowedTools "Read,Glob,Grep" \
--max-turns 3 \
--output-format text \
> "$output_file" 2>/dev/null || {
echo "EVAL_ERROR: claude command failed for $case_id ($mode)" > "$output_file"
}
fi
local wc_out
wc_out=$(wc -w < "$output_file" | tr -d ' ')
echo " [$mode] $case_id complete ($wc_out words)"
}
if ! $SCORE_ONLY; then
if $RUN_SKILL; then
echo "── With Skill ──"
for case_id in "${CASE_IDS[@]}"; do
run_case "$case_id" "with-skill"
done
echo ""
fi
if $RUN_BASELINE; then
echo "── Baseline (no skill) ──"
for case_id in "${CASE_IDS[@]}"; do
run_case "$case_id" "baseline"
done
echo ""
fi
fi
# ─── Assertion evaluation helpers ──────────────────────────────────
# Extract first JSON block from response
extract_json() {
local text="$1"
# Try ```json fenced block first
local block
block=$(echo "$text" | sed -n '/```json/,/```/p' | sed '1d;$d')
if [[ -z "$block" ]]; then
# Try bare { ... } block
block=$(echo "$text" | grep -Pzo '\{[^{}]*(\{[^{}]*\}[^{}]*)*\}' 2>/dev/null | head -1 || true)
fi
echo "$block"
}
# Get a field value from JSON (uses python if available, else node)
json_field() {
local json="$1"
local field="$2"
if command -v python3 &>/dev/null; then
echo "$json" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
v = d.get('$field', '')
if isinstance(v, list): print(len(v))
elif isinstance(v, (int, float)): print(v)
else: print(v)
except: print('')
" 2>/dev/null
elif command -v node &>/dev/null; then
echo "$json" | node -e "
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const o=JSON.parse(d);const v=o['$field'];
if(Array.isArray(v))console.log(v.length);
else console.log(v??'');}catch(e){console.log('');}
})" 2>/dev/null
else
echo ""
fi
}
# Word count of a string
word_count() {
echo "$1" | wc -w | tr -d ' '
}
# ─── Score a single assertion ──────────────────────────────────────
eval_assertion() {
local response="$1"
local atype="$2"
local target="$3"
local json_block="$4"
case "$atype" in
contains)
echo "$response" | grep -qi "$target" && echo "PASS" || echo "FAIL"
;;
not_contains)
# Handle descriptive targets: if target contains spaces and "should not contain",
# try to extract the quoted literal(s)
if [[ "$target" =~ \'([^\']+)\' ]]; then
local found=false
while [[ "$target" =~ \'([^\']+)\' ]]; do
local literal="${BASH_REMATCH[1]}"
if echo "$response" | grep -qi "$literal"; then
found=true
break
fi
target="${target#*"'${literal}'"}"
done
$found && echo "FAIL" || echo "PASS"
else
echo "$response" | grep -qi "$target" && echo "FAIL" || echo "PASS"
fi
;;
regex)
echo "$response" | grep -qiE "$target" && echo "PASS" || echo "FAIL"
;;
question_before_code)
local q_line code_line
q_line=$(echo "$response" | grep -n '?' | head -1 | cut -d: -f1)
code_line=$(echo "$response" | grep -n '```' | head -1 | cut -d: -f1)
if [[ -z "$code_line" ]] || [[ -n "$q_line" && "$q_line" -lt "$code_line" ]]; then
echo "PASS"
else
echo "FAIL"
fi
;;
json_valid)
if [[ -n "$json_block" ]]; then
if command -v python3 &>/dev/null; then
echo "$json_block" | python3 -c "import sys,json;json.load(sys.stdin)" 2>/dev/null && echo "PASS" || echo "FAIL"
elif command -v node &>/dev/null; then
echo "$json_block" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{JSON.parse(d);console.log('PASS')}catch(e){console.log('FAIL')}})" 2>/dev/null
else
echo "SKIP"
fi
else
echo "FAIL"
fi
;;
json_schema|json_fields)
# Target is comma-separated field names
if [[ -z "$json_block" ]]; then
echo "FAIL"
return
fi
local all_found=true
IFS=',' read -ra FIELDS <<< "$target"
for field in "${FIELDS[@]}"; do
field=$(echo "$field" | xargs) # trim whitespace
if ! echo "$json_block" | grep -q "\"$field\""; then
all_found=false
break
fi
done
$all_found && echo "PASS" || echo "FAIL"
;;
token_limit)
local wc
wc=$(word_count "$response")
local est_tokens=$(( wc * 13 / 10 ))
[[ "$est_tokens" -le "${target:-99999}" ]] && echo "PASS" || echo "FAIL"
;;
range_check)
# Parse common patterns from the target expression
if [[ -z "$json_block" ]]; then
echo "FAIL"
return
fi
# Handle: bias_score >= X AND bias_score <= Y
if [[ "$target" =~ ([a-z_]+)\ *\>=\ *(-?[0-9.]+)\ +AND\ +\1\ *\<=\ *(-?[0-9.]+) ]]; then
local field="${BASH_REMATCH[1]}"
local min="${BASH_REMATCH[2]}"
local max="${BASH_REMATCH[3]}"
local val
val=$(json_field "$json_block" "$field")
if [[ -n "$val" ]] && command -v python3 &>/dev/null; then
python3 -c "v=$val; print('PASS' if $min <= v <= $max else 'FAIL')" 2>/dev/null || echo "FAIL"
else
echo "SKIP"
fi
return
fi
# Handle: abs(field) <= X
if [[ "$target" =~ abs\(([a-z_]+)\)\ *\<=\ *(-?[0-9.]+) ]]; then
local field="${BASH_REMATCH[1]}"
local limit="${BASH_REMATCH[2]}"
local val
val=$(json_field "$json_block" "$field")
if [[ -n "$val" ]] && command -v python3 &>/dev/null; then
python3 -c "v=$val; print('PASS' if abs(v) <= $limit else 'FAIL')" 2>/dev/null || echo "FAIL"
else
echo "SKIP"
fi
return
fi
# Handle: quality_score >= X
if [[ "$target" =~ ([a-z_]+)\ *\>=\ *(-?[0-9.]+)$ ]]; then
local field="${BASH_REMATCH[1]}"
local min="${BASH_REMATCH[2]}"
local val
val=$(json_field "$json_block" "$field")
if [[ -n "$val" ]] && command -v python3 &>/dev/null; then
python3 -c "v=$val; print('PASS' if v >= $min else 'FAIL')" 2>/dev/null || echo "FAIL"
else
echo "SKIP"
fi
return
fi
# Handle: len(field) >= X AND len(field) <= Y (character length)
if [[ "$target" =~ len\(([a-z_]+)\)\ *\>=\ *([0-9]+)\ +AND\ +len\(\1\)\ *\<=\ *([0-9]+) ]]; then
local field="${BASH_REMATCH[1]}"
local min="${BASH_REMATCH[2]}"
local max="${BASH_REMATCH[3]}"
local val
val=$(json_field "$json_block" "$field")
local len=${#val}
[[ "$len" -ge "$min" && "$len" -le "$max" ]] && echo "PASS" || echo "FAIL"
return
fi
# Handle: len(field) <= X
if [[ "$target" =~ len\(([a-z_]+)\)\ *\<=\ *([0-9]+) ]]; then
local field="${BASH_REMATCH[1]}"
local max="${BASH_REMATCH[2]}"
local val
val=$(json_field "$json_block" "$field")
local len=${#val}
[[ "$len" -le "$max" ]] && echo "PASS" || echo "FAIL"
return
fi
# Handle: len(field) >= X (list length)
if [[ "$target" =~ len\(([a-z_]+)\)\ *\>=\ *([0-9]+) ]]; then
local field="${BASH_REMATCH[1]}"
local min="${BASH_REMATCH[2]}"
local val
val=$(json_field "$json_block" "$field")
[[ "$val" -ge "$min" ]] 2>/dev/null && echo "PASS" || echo "FAIL"
return
fi
# Handle: word_count(field) >= X AND word_count(field) <= Y
if [[ "$target" =~ word_count\(([a-z_]+)\)\ *\>=\ *([0-9]+)\ +AND\ +word_count\(\1\)\ *\<=\ *([0-9]+) ]]; then
local field="${BASH_REMATCH[1]}"
local min="${BASH_REMATCH[2]}"
local max="${BASH_REMATCH[3]}"
local val
val=$(json_field "$json_block" "$field")
local wc
wc=$(word_count "$val")
[[ "$wc" -ge "$min" && "$wc" -le "$max" ]] && echo "PASS" || echo "FAIL"
return
fi
echo "SKIP" # Unrecognized expression
;;
# Soft assertion types — logged but always PASS (require LLM judge)
structure_check|sequence_check)
echo "SOFT"
;;
*)
echo "SKIP"
;;
esac
}
# ─── Score all assertions for a case ───────────────────────────────
score_case() {
local case_id="$1"
local mode="$2"
local output_file="$RESULTS_DIR/$mode/${case_id}.md"
local score_file="$RESULTS_DIR/$mode/${case_id}.score.txt"
local response
response=$(cat "$output_file" 2>/dev/null || echo "")
if [[ "$response" == EVAL_ERROR* ]]; then
echo "ERROR" > "$score_file"
echo "ERROR"
return
fi
local json_block
json_block=$(extract_json "$response")
local pass=0 fail=0 soft=0 skip=0 total=0
local critical_fail=false
local details=""
# Parse assertions from YAML
local in_case=false
local in_assertions=false
local assert_type="" assert_target="" assert_critical="false" assert_desc=""
process_assertion() {
if [[ -z "$assert_type" ]]; then return; fi
total=$((total + 1))
local result
result=$(eval_assertion "$response" "$assert_type" "$assert_target" "$json_block")
case "$result" in
PASS) pass=$((pass + 1)) ;;
FAIL)
fail=$((fail + 1))
[[ "$assert_critical" == "true" ]] && critical_fail=true
;;
SOFT) soft=$((soft + 1)) ;;
SKIP) skip=$((skip + 1)) ;;
esac
local label="${assert_desc:-$assert_type($assert_target)}"
local crit_marker=""
[[ "$assert_critical" == "true" ]] && crit_marker=" [CRITICAL]"
details+=" $result$crit_marker — $label"$'\n'
}
while IFS= read -r line; do
if [[ "$line" =~ ^-\ id:\ $case_id$ ]]; then
in_case=true
continue
fi
if $in_case && [[ "$line" =~ ^-\ id: ]]; then
break
fi
if $in_case && [[ "$line" =~ ^\ \ \ \ -\ type:\ (.+) ]]; then
process_assertion
assert_type="${BASH_REMATCH[1]}"
assert_target=""
assert_critical="false"
assert_desc=""
fi
if $in_case && [[ "$line" =~ ^\ \ \ \ \ \ target:\ (.+) ]]; then
assert_target="${BASH_REMATCH[1]}"
assert_target="${assert_target#\"}"
assert_target="${assert_target%\"}"
fi
if $in_case && [[ "$line" =~ ^\ \ \ \ \ \ critical:\ (.+) ]]; then
assert_critical="${BASH_REMATCH[1]}"
fi
if $in_case && [[ "$line" =~ ^\ \ \ \ \ \ description:\ (.+) ]]; then
assert_desc="${BASH_REMATCH[1]}"
assert_desc="${assert_desc#\"}"
assert_desc="${assert_desc%\"}"
fi
done < "$SCRIPT_DIR/test-cases.yaml"
process_assertion # last assertion
local anchored=$((pass + fail))
local score_line="$pass/$total (${anchored} anchored, ${soft} soft, ${skip} skipped)"
if $critical_fail; then
score_line+=" [CRITICAL FAIL]"
fi
{
echo "$score_line"
echo "$details"
} > "$score_file"
echo "$score_line"
}
# ─── Generate scorecard ───────────────────────────────────────────
generate_scorecard() {
local scorecard="$RESULTS_DIR/scorecard.md"
cat > "$scorecard" <<HEADER
# Eval Scorecard — $SKILL_NAME
**Timestamp:** $TIMESTAMP
| Test Case | With Skill | Baseline |
|-----------|-----------|----------|
HEADER
for case_id in "${CASE_IDS[@]}"; do
local skill_score="—"
local base_score="—"
if $RUN_SKILL && [[ -f "$RESULTS_DIR/with-skill/${case_id}.md" ]]; then
skill_score=$(score_case "$case_id" "with-skill")
fi
if $RUN_BASELINE && [[ -f "$RESULTS_DIR/baseline/${case_id}.md" ]]; then
base_score=$(score_case "$case_id" "baseline")
fi
echo "| $case_id | $skill_score | $base_score |" >> "$scorecard"
done
echo "" >> "$scorecard"
echo "## Assertion Details" >> "$scorecard"
for case_id in "${CASE_IDS[@]}"; do
echo "" >> "$scorecard"
echo "### $case_id" >> "$scorecard"
for mode in "with-skill" "baseline"; do
if [[ -f "$RESULTS_DIR/$mode/${case_id}.score.txt" ]]; then
echo "**${mode}:**" >> "$scorecard"
echo '```' >> "$scorecard"
cat "$RESULTS_DIR/$mode/${case_id}.score.txt" >> "$scorecard"
echo '```' >> "$scorecard"
fi
done
done
echo ""
echo "=== Scorecard ==="
cat "$scorecard"
}
generate_scorecard
echo ""
echo "=== Eval complete — $RESULTS_DIR/ ==="
- id: TC-001
name: simple-sdxl-txt2img
prompt: "Generate a ComfyUI workflow for a simple SDXL text-to-image of a sunset over mountains"
assertions:
- type: json_valid
description: "Output is valid JSON with nodes"
critical: true
- type: contains
target: "CheckpointLoaderSimple"
critical: true
- type: contains
target: "KSampler"
critical: true
- type: regex
target: "1024"
description: "Resolution is 1024 (SDXL-native)"
critical: true
- type: not_contains
target: "512x512"
description: "Does not use SD1.5 resolution for SDXL"
critical: true
expected_behavior: "Produces a valid SDXL txt2img workflow with correct resolution, valid node types, and proper connections from checkpoint loader through sampler to image output"
edge_case: false
- id: TC-002
name: checkpoint-loader-output-indices
prompt: "Build a ComfyUI workflow using SDXL with separate CLIP text encode for positive and negative prompts"
assertions:
- type: structure_check
target: "CheckpointLoaderSimple output index 0 connects to MODEL input, index 1 connects to CLIP inputs, index 2 connects to VAE input"
description: "Semantic check — requires JSON graph analysis to verify output index wiring"
critical: true
- type: not_contains
target: "CLIP output from index 0 of CheckpointLoader"
critical: true
- type: structure_check
target: "All node connections reference valid output indices for their source node type"
description: "Semantic check — requires JSON graph analysis to verify connection validity"
critical: true
expected_behavior: "Output indices are correct: CheckpointLoaderSimple outputs MODEL at 0, CLIP at 1, VAE at 2. Both CLIPTextEncode nodes receive CLIP from index 1, not index 0"
edge_case: true
- id: TC-003
name: lora-stacked-workflow
prompt: "Create a workflow that loads an SDXL checkpoint, applies two LoRAs (one for style, one for character), and generates a portrait"
assertions:
- type: contains
target: "LoraLoader"
critical: true
- type: structure_check
target: "LoRA loaders are chained: first LoRA output MODEL/CLIP feeds into second LoRA input"
description: "Semantic check — requires JSON graph analysis to verify LoRA chaining"
critical: true
- type: structure_check
target: "Second LoRA output MODEL connects to KSampler, second LoRA output CLIP connects to CLIPTextEncode"
description: "Semantic check — requires JSON graph analysis to verify final LoRA connections"
critical: true
- type: structure_check
target: "No dangling inputs — every required input on every node has a connection or value"
description: "Semantic check — requires full graph traversal to verify completeness"
critical: true
expected_behavior: "LoRA loaders are chained correctly with MODEL and CLIP passing through each loader sequentially before reaching the sampler and text encoders"
edge_case: false
- id: TC-004
name: controlnet-depth-workflow
prompt: "Make a ComfyUI workflow that uses a depth ControlNet with SDXL to generate an interior design scene from a depth map"
assertions:
- type: contains
target: "ControlNetLoader"
critical: true
- type: contains
target: "ControlNetApplyAdvanced"
critical: false
- type: regex
target: "(conditioning|positive).*ControlNet|ControlNet.*(conditioning|positive)"
description: "ControlNet conditioning applied to positive prompt conditioning"
critical: true
- type: structure_check
target: "Image loader node feeds into ControlNet apply node, not into the sampler latent input"
description: "Semantic check — requires JSON graph analysis to verify image routing"
critical: true
expected_behavior: "ControlNet is loaded and applied to conditioning before reaching the sampler. Depth map image is loaded and connected to the ControlNet apply node correctly"
edge_case: false
- id: TC-005
name: sd15-not-sdxl-resolution
prompt: "Build a workflow using Deliberate v2 (SD 1.5 model) to generate anime character art"
assertions:
- type: contains
target: "512"
description: "Resolution is 512-based (SD1.5 native)"
critical: true
- type: not_contains
target: "width: 1024"
critical: true
- type: not_contains
target: "SDXLClipTextEncode"
description: "No SDXL-specific nodes used with SD1.5 model"
critical: true
- type: contains
target: "CLIPTextEncode"
critical: true
expected_behavior: "Recognizes Deliberate v2 as an SD1.5 model and uses 512-native resolution, standard CLIPTextEncode (not SDXL variant), and SD1.5-appropriate settings"
edge_case: false
- id: TC-006
name: flux-specific-workflow
prompt: "Create a ComfyUI workflow for FLUX.1 Dev text-to-image of a portrait photo"
assertions:
- type: json_valid
critical: true
- type: not_contains
target: "CheckpointLoaderSimple"
critical: true
description: "FLUX uses LoadDiffusionModel, not CheckpointLoaderSimple"
- type: regex
target: "(LoadDiffusionModel|UNETLoader|DiffusionModelLoader)"
critical: true
description: "Must use FLUX-appropriate model loader"
- type: regex
target: "(DualCLIP|CLIPLoader.*flux|t5xxl)"
critical: false
description: "FLUX requires dual CLIP (CLIP-L + T5XXL)"
- type: regex
target: "(3\\.5|4\\.0|cfg.*[3-5])"
critical: false
description: "FLUX uses lower CFG values (3-5)"
- type: contains
target: "1024"
critical: true
description: "FLUX generates at 1024x1024 minimum"
expected_behavior: >
FLUX has a completely different architecture from SD/SDXL. It uses
LoadDiffusionModel (not CheckpointLoaderSimple), requires separate
VAE loading, uses dual CLIP (CLIP-L + T5XXL), and operates at lower
CFG values. The workflow must reflect FLUX-specific node types.
edge_case: true
- id: TC-007
name: wan-i2v-video-workflow
prompt: "Build a ComfyUI workflow for Wan 2.1 image-to-video — I have a landscape photo and want a 4-second clip with gentle camera pan"
assertions:
- type: json_valid
critical: true
- type: regex
target: "(Wan|wan|WAN)"
critical: true
description: "Must reference Wan model"
- type: regex
target: "(I2V|img2vid|image.to.video|ImageEncode)"
critical: true
description: "Must be an I2V pipeline, not txt2vid"
- type: regex
target: "(frame|frames|[0-9]+f|duration|length)"
critical: true
description: "Must specify frame count for video output"
- type: regex
target: "(camera|pan|motion|movement)"
critical: false
description: "Should address the camera pan request"
expected_behavior: >
Wan I2V workflows require specific nodes: image encoder, latent
preparation with frame count, and video-specific sampling. The workflow
must load the input image, encode it, set up the correct number of frames
for 4 seconds, and configure camera motion parameters.
edge_case: false
should_trigger:
- "Build me a ComfyUI workflow for generating anime portraits"
- "Create a workflow JSON that uses SDXL with ControlNet depth"
- "Make me an image generation pipeline in ComfyUI"
- "I need a txt2img workflow with two LoRAs stacked"
- "Generate a ComfyUI workflow for inpainting with SDXL"
- "Set up a workflow that goes from text prompt to final image using Juggernaut XL"
- "Can you make a ComfyUI pipeline for img2img with SD 1.5?"
- "I want to create a workflow with upscaling after generation"
- "Build a node graph for generating product photos with ControlNet"
- "Help me wire up a ComfyUI workflow for face detailing with ADetailer"
should_not_trigger:
- "Explain how latent diffusion models work mathematically"
- "Review this Python script for bugs"
- "What's the difference between SDXL and SD 1.5 architecturally?"
- "Help me install ComfyUI on my server"
- "Write a custom ComfyUI node in Python"
- "Debug why my ComfyUI server won't start"
- "Compare Midjourney vs Stable Diffusion quality"
- "Help me fine-tune a LoRA model"
- "What GPU should I buy for AI image generation?"
- "Convert this Automatic1111 workflow to a Python script"
optimized_description: >
Generate, build, create, or design ComfyUI workflow JSON from natural language descriptions.
Produces valid node graphs with correct class_types, connections, output indices, and
model-appropriate settings. Handles txt2img, img2img, inpainting, ControlNet, LoRA stacking,
upscaling, and face detailing pipelines. Does NOT cover ComfyUI installation, custom node
development, Python scripting, model training, hardware advice, or architectural explanations.
Related skills
How it compares
Pick comfyui-workflow-builder when you need agent-generated ComfyUI JSON graphs validated for node names and output slots, not generic image prompts or UI-only tutorials.
FAQ
Does it check available models?
Yes — it reads state/inventory.json for checkpoints, LoRAs, ControlNet, custom nodes, and VRAM before generating.
What workflow format is used?
JSON with string node IDs, class_type, inputs with values or [source_id, output_index] connections.
Is comfyui-workflow-builder safe to install?
Review the Security Audits panel on this page before installing in production.