cmfy is a fast, flexible command‑line tool to run ComfyUI workflows. It loads JSON workflows from your filesystem, applies template variables and targeted overrides, submits them to a running ComfyUI server, and saves the generated outputs locally.
- One shared execution path for
run, aliases, batch jobs, durable job operations, and automation adapters: Resolve → Plan → Submit → Observe → Collect. - Durable SQLite/WAL job and provenance history with idempotent request IDs, retry, filtering, cursor pagination, cleanup, and restart recovery.
- Secure bounded HTTP transport, content-hash upload reuse, resumable output collection, containment checks, and collision-safe atomic publication.
- Versioned JSON results and JSONL progress events suitable for terminal users, agents, and Oqto Apps.
- Deterministic workflow describe/validate/plan contracts and optional server node dependency validation.
- Works with local workflow JSONs; supports both raw prompt maps and
{ "prompt": { ... } }wrappers. - Configurable
workflows_dirandoutput_dirin a TOML config stored under$XDG_CONFIG_HOME/cmfy/config.toml. - Rich templating via
${KEY}placeholders across string inputs. - Precise overrides with
--set <nodeID>.inputs.<name>=<value>(coerces ints/floats/bools; quoted strings preserved). - Asset uploads for images/masks/inputs; exposes
${IMAGE},${MASK},${INPUT}and enumerated variants. - First‑class sampler/refiner flags (
--sampler,--scheduler,--steps,--cfg,--denoise, etc.). - Music prompt convenience flags:
--tagsand--lyricsmap to${TAGS}and${LYRICS}. - Standard workflow aliases (e.g.,
txt2img,img2img, …) via[standard_workflows]and path mappings via[standard_workflows_params.<alias>].
- Go 1.23+ to build.
- A running ComfyUI server (HTTP API) reachable at
server_url(defaulthttp://127.0.0.1:8188).
# From repository root
go build -o cmfy ./cmd/cmfy
./cmfy versionOr use just:
just help
just build
just test
just checkConfig is stored at $XDG_CONFIG_HOME/cmfy/config.toml. If XDG_CONFIG_HOME is empty, falls back to ~/.config/cmfy/config.toml.
By default, output_dir is ./outputs (relative to the current working directory where you run cmfy).
Initialize a default config:
./cmfy config init
./cmfy config path # prints the pathConfig keys:
"$schema" = "https://raw.githubusercontent.com/byteowlz/schemas/refs/heads/main/cmfy/cmfy.config.schema.json"
server_url = "http://127.0.0.1:8188"
# Optional named API endpoint profiles. Credentials must remain in an external
# credential mechanism; URLs containing userinfo are rejected.
[servers.local_gpu]
url = "http://127.0.0.1:8188"
output_dir = "./outputs"
workflows_dir = "workflows"
default_workflow = "" # optional fallback when -w is omitted
default_width = 768
default_height = 768
default_steps = 28
# Positive transport/materialization bounds (bytes/count)
max_json_bytes = 8388608
max_upload_bytes = 1073741824
max_output_bytes = 536870912
max_total_output_bytes = 1073741824
max_output_files = 256
[vars]
# Global template vars available to all workflows
# MODEL = "sdxl.safetensors"
# CFG = "4.5"
[workflows.my_custom.vars]
# Per-workflow defaults (workflow name = file name without extension)
# PROMPT = "a cute cat"
# STEPS = "20"
[standard_workflows]
# Map well-known aliases to workflow names or absolute/relative JSON paths.
# If unset but a matching file exists (e.g., <workflows_dir>/txt2img.json), it is used implicitly.
txt2img = ""
img2img = ""
canny2img = ""
depth2img = ""
img2vid = ""
txt2vid = ""
txt2music = ""
txt2img_lora = ""
img2img_inpainting = ""
# Optional: precise paths for first-class flags for a given alias
[standard_workflows_params.txt2img]
# sampler_name = "12.inputs.sampler_name"
# scheduler = "12.inputs.scheduler"
# steps = "12.inputs.steps"
# cfg = "12.inputs.cfg"
# denoise = "12.inputs.denoise"
[standard_workflows_params.txt2img_lora]
# sampler_name = "28.inputs.sampler_name"
# refiner.sampler_name = "42.inputs.sampler_name"
# refiner.steps = "42.inputs.steps"
# Optional: named remote servers for SSH workflow discovery/import
[remote_servers.local_gpu]
ssh_config_host = "local-gpu"
workflows_dir = "~/ComfyUI/user/default/workflows"
# host = "192.168.1.20"
# user = "agent"
# port = 22
# key_path = "~/.ssh/id_ed25519"- Place JSON workflows under
workflows_dir(default./workflows). - Supported formats:
- Raw prompt map: a JSON object with numeric keys for nodes:
{ "0": { ... }, "1": { ... }, ... }. - Wrapper with
promptkey:{ "prompt": { "0": { ... } } }.
- Raw prompt map: a JSON object with numeric keys for nodes:
- Optional workflow metadata keys:
variables: variable defaults/descriptions.prompt_guidelines(orguidelines): authoring hints for prompt/tag writing.
Inspect a workflow to discover node IDs, class types, and inputs:
./cmfy workflows inspect txt2img
./cmfy workflows inspect txt2music --guidelines # prints optional prompting guidance
./cmfy workflows show txt2img # prints JSON with prompt map
./cmfy workflows list # lists available names in workflows_dir
./cmfy --json workflows describe txt2img
./cmfy --json workflows validate txt2img --var PROMPT="a forest owl"
# Resolve parameters/assets and verify required node classes against the target server.
./cmfy --json run --plan -w txt2img --prompt "a forest owl"
# From SSH-configured remote servers in config.toml
./cmfy workflows ssh-list local_gpu
./cmfy workflows ssh-list local_gpu flux --json
./cmfy workflows ssh-import local_gpu Flux_upscale.jsonBasic run:
# By name (resolved in workflows_dir)
./cmfy run -w txt2img --prompt "a cozy cabin in the woods" --width 768 --height 768 --steps 30
# By path (absolute or relative)
./cmfy run -w ./workflows/my_flow.json --prompt "a sketch of an owl"
# Async submit (returns immediately; prints prompt ID)
./cmfy run -w txt2img --prompt "a sketch of an owl" --async
# txt2music convenience variables
./cmfy run -w txt2music --tags "lofi chill, warm keys" --lyrics "Late night city lights"Using standard and custom aliases:
# Assign (CLI writes config.toml [standard_workflows])
./cmfy workflows assign txt2img my_txt2img
./cmfy workflows assign ltx23 ~/cmfy/workflows/image_to_video_ltx2_3_i2v_with_sound.json
# Then run directly by alias
./cmfy txt2img --prompt "sunlit woodland" --steps 28 --cfg 5.5 --sampler euler
./cmfy ltx23 --image input.png --var PROMPT="cinematic close-up" --asyncYou can also define aliases manually in config:
[standard_workflows]
ltx23 = "~/cmfy/workflows/image_to_video_ltx2_3_i2v_with_sound.json"Asset uploads:
./cmfy run -w img2img \
--image input.png \
--mask mask.png \
--input controlnet_hint.png
# Exposes variables: ${IMAGE}, ${IMAGE1}, ${MASK}, ${INPUT}, etc.
# Use these variables in your workflow node string inputs.Targeted overrides:
# Set any input on any node using <nodeID>.inputs.<name>=<value>
./cmfy run -w txt2img --set 12.inputs.steps=25 --set 12.inputs.cfg=5.0
./cmfy run -w img2img --set 8.inputs.denoise=0.6 --set 9.inputs.seed=1337
# Values are coerced when possible: ints, floats, bools; quoted strings stay stringsFirst‑class sampler/refiner flags:
./cmfy txt2img \
--sampler euler --scheduler normal \
--steps 30 --cfg 5.5 --denoise 0.2
./cmfy txt2img_lora \
--sampler dpmpp_2m --refiner-sampler euler \
--refiner-steps 15 --refiner-cfg 4.0How first‑class flags are applied:
[standard_workflows_params.<alias>]mappings write flags such assteps,cfg, andsampler_nameto exact paths.- Without a mapping, cmfy applies a parameter only when exactly one workflow node exposes that input.
- Zero matches or multiple matches fail loudly. cmfy never guesses between nodes; use
workflows inspectand add an exact mapping.
Outputs:
- Saved beneath
output_dir(default./outputs);--output/-oor--output-diroverrides it per run. - Server filenames and subfolders are validated as relative paths. Absolute paths, traversal, separator tricks, symlink destinations, oversized files, excessive output counts, and aggregate-size overflows fail closed.
- Downloads stream to same-directory partial files, resume through validated range responses, compute SHA-256, and publish complete files atomically without overwriting unrelated content.
- Duplicate content reuses its existing path. Different content receives the deterministic suffix
-<sha256-prefix>.
- Any string input in the workflow that contains
${KEY}will be replaced with the value ofKEY. - Sources (precedence):
- Global
[vars]in config [workflows.<name>.vars]in config- Convenience flags (
--prompt,--tags,--lyrics,--seed,--width,--height,--steps,--cfg) --var KEY=VALon CLI- Uploaded assets populate
${IMAGE},${MASK},${INPUT}and enumerated variants
- Global
- Numeric inputs generally require
--setunless you model them as strings in the workflow and template them.
Optional prompting guidelines in workflow JSON:
{
"prompt_guidelines": {
"summary": "Write cinematic, high-contrast prompts.",
"dos": ["Use strong nouns", "Include mood and pacing"],
"donts": ["Avoid contradictory styles"],
"examples": ["epic trailer score, dark tension, rising strings"]
}
}Show them with:
./cmfy workflows inspect txt2music --guidelinesSubmit multiple jobs from a JSONL file. Each line can target a different workflow.
# Print example JSONL to stdout
./cmfy batch run example --mode mixed-workflows
# Run batch file
./cmfy batch run --file jobs.jsonl --async
# Bound local orchestration concurrency and optionally throttle starts
./cmfy batch run --file jobs.jsonl --concurrency 4 --submit-delay 500ms
# Machine-readable result summary (one JSON array)
./cmfy --json batch run --file jobs.jsonlJSONL line schema (core fields):
workflow(required): alias, name, or workflow pathid(optional): user tracking IDvars(optional): map for${KEY}substitutionsset(optional): map for<nodeID>.inputs.<name>=valueimage/mask/input(optional): arrays of file pathsserver(optional): override server URLasync(optional): per-line async overridetimeout(optional): per-line wait timeout (e.g.30m)
Job state defaults to $CMFY_STATE_DIR/history.sqlite3, then $XDG_STATE_HOME/cmfy/history.sqlite3, then ~/.local/state/cmfy/history.sqlite3. --state-dir has highest precedence. Oqto should bind a workspace-specific directory rather than the user's global state. The database uses WAL and mode 0600; credentials are never persisted.
./cmfy --json jobs list --limit 50 --status completed
./cmfy --json jobs show <job-or-prompt-id>
./cmfy jobs watch --jsonl <job-or-prompt-id> # WebSocket, polling fallback
./cmfy jobs watch --jsonl --include-preview <id> # bounded base64 previews
./cmfy --json jobs retry <job-or-prompt-id> --request-id retry-001
./cmfy jobs prune --older-than 720h --keep-recent 1000 --dry-run
./cmfy server ping
./cmfy --json server inspect # nodes, models, digests, supported features
./cmfy --profile local_gpu server ping
./cmfy queue
./cmfy job status <id> # compatibility surface; updates durable status
./cmfy job wait <id> --download
./cmfy job cancel <id> # records the cancellation outcomejobs list is newest-first. Its opaque next_cursor can be supplied as --cursor; page size is bounded to 200.
Global --json emits exactly one versioned JSON value on stdout for automation-relevant commands. Failures emit one cmfy/error-v1 value and return non-zero. Human output is suppressed in machine mode. jobs watch --jsonl is the streaming exception: it emits one bounded cmfy/job-event-v1 object per line. --quiet suppresses human progress without changing results.
Schemas are versioned by their schema field. Additive fields may appear within a schema version; removing or changing field meaning requires a new version. Consumers should ignore unknown fields and must not parse human prose.
POST /upload/image— streams bounded form-data files as typeinput.POST /prompt— submits a prompt graph, returnsprompt_id.GET /history/<prompt_id>— tracks prompt execution and outputs.GET /queue— queue state for running/pending prompts.POST /queue— queue manipulation (used for cancel attempts).GET /view?filename=...&subfolder=...&type=...— downloads generated assets.GET /system_stats— used forserver ping.GET /object_info— server node dependency and capability inspection.GET /models[/<folder>]— bounded model inventory when supported.GET /ws?clientId=...— execution/progress/preview events with polling fallback.
- If
cmfy runreports “no workflow specified”, setdefault_workflowin config or pass-w. - If an alias is not set, cmfy will still run it implicitly if
<workflows_dir>/<alias>.jsonexists. - Use
workflows inspectto discover inputs; then use--setor add mappings under[standard_workflows_params.<alias>]for stable behavior. - The bundled TOML parser supports keys/strings/ints/floats/bools/arrays and simple sections. If you need advanced TOML features, open an issue to switch to a full TOML library.
Issues and PRs are welcome. Please keep changes focused and consistent with the existing style. If you add new flags or config keys, update this README accordingly.