Diffusers-workflow: your GPU as something an agent can drive

I’ve been building diffusers-workflow — a declarative engine on top of Diffusers where an image or video pipeline is a JSON document rather than a Python script, with an MCP server as the primary front end so Claude Code (or any MCP client) can author, validate, run and inspect generations. There’s a web UI and a CLI/REPL too, but the agent is the first-class operator; the UI is there for looking at what it made.

Agent first. The MCP surface is ~50 tools covering the workflow catalog, the real pipeline signatures, the job queue, the gallery and the model cache. Two things I think matter for letting an LLM drive a GPU unattended:

  • validate_workflow checks the schema and the actual __call__ signature of the diffusers pipeline, without loading a model — so an agent’s typos are caught for free before it spends anything.
  • Every tool that costs GPU minutes or disk (run_workflow, download_model, delete_model, …) refuses unless called with acknowledged_cost=true. The agent has to tell you what it’s about to spend before it spends it.

A server holds several workspaces, so two agents (or an agent and you in the browser) can share one GPU without saving over each other. A small plugin adds one skill per model family (LTX-2.5, MiniMax H3, MiniMax Music3) that picks the right template and points at the vendor’s own prompt guide.

Diffable, repeatable workflows. A workflow is a list of named steps — each a diffusers pipeline or a utility task — with arguments that are references rather than paths: variable:prompt, previous_result:still, prompt:folder/name, asset:hero.png, output:ltx2/latest/still.png. That makes multi-stage work (text→image→video, chained segments with audio-driven length and frame continuity) composable, and it makes the whole thing git diff-able. Every run writes its own directory with a manifest; outputs embed the full workflow and seed, so any image in the gallery reopens as the exact recipe that made it. A step whose resolved arguments and seed haven’t changed reuses its cached result instead of re-running.

All on Diffusers. Nothing is reimplemented. Pipelines are reached by dynamic import (including ModularPipeline — the MiniMax templates already run on Modular Diffusers), so a new model in Diffusers is a new JSON file, not a new adapter. Quantization (bitsandbytes, TorchAO, GGUF, SDNQ, quanto), cache accelerators (TeaCache, FirstBlockCache, MagCache, TaylorSeer), LoRA, IP-Adapter, CUDA/MPS/CPU — all the way through. Everything in the repo runs on a single 24GB card; there’s a recipes doc for the memory configs per family.

python -m dw.serve --workspace ~/studio          # the engine
claude mcp add dw -- "$(pwd)/venv/bin/dw-mcp"   # the agent

Then: “Take my Flux workflow, swap in the portrait LoRA, render four at 1024 square, and use the best frame to seed an LTX video.”

Repo, docs and a corpus of runnable workflows: GitHub - dkackman/diffusers-workflow: MCP server, workflow wrapper and web app for Hugging Face diffusers · GitHub — happy to hear what shapes people want that it doesn’t cover yet.

The acknowledged_cost=true gate is a strong idea because it forces cost into the agent’s declared plan instead of leaving it as an invisible side effect. I would test one adversarial edge case next: a workflow that validates cheaply but expands at runtime through four variants, retries, or a model download. The acknowledgement should bind to an estimated budget or operation fingerprint, not just a boolean, otherwise the plan can change after consent while the flag remains true.

The run manifest and embedded workflow/seed make this unusually inspectable. That is probably the feature I would trust most before letting an agent drive the GPU unattended.

great suggestion. there a number of currently hidden costs which aren’t being surfaced or gated. queuing up a fix.

For now, I think I may have found one candidate workflow shape:


The shape I have in mind is roughly:

generate / fan out N candidates
        ↓
gather them
        ↓
evaluate / select one
        ↓
send only that result into an expensive downstream step

For example, very close to the use case already in the README:

4 Flux stills → choose one → LTX video

I do not mean that this use case is currently impossible. The existing agent path already handles the semantic version nicely: the agent can inspect the generated images, decide that “the third frame is the one”, keep_output it, and use that asset in the next workflow. That is probably the right boundary when “best” is a genuinely visual or subjective judgment.

The part that seems potentially interesting as a workflow shape is the deterministic version of the same pattern.

My default split would probably be:

  • semantic / subjective selection → keep it at the agent boundary, as today;
  • deterministic N→1 selection → possibly a small selector/reducer task inside the workflow;
  • actual conditional control flow → only introduce that if there are use cases that cannot be expressed cleanly by the first two.

That seems smaller than adding a general when/branch language immediately, and it fits the existing for_eachgather: → task/pipeline structure pretty naturally.

For example, the deterministic case could be something like:

generate 4 candidates
→ gather
→ select argmax(score)
→ run the expensive video/upscale stage only for that candidate

The selector could be as simple as “highest score”, “first above threshold”, “lowest artifact score”, or “use the index returned by another task”. A similar N→1 shape shows up in ComfyUI’s official ImageSelector custom-node walkthrough, so it seems like a reasonably real workflow pattern rather than a hypothetical one.

If selection remains semantic, though, I would not try to force it into the workflow DSL just for completeness. The current agent-mediated two-job flow may actually be the cleaner abstraction.

Separately, I found one small preflight edge case that seems especially relevant when an agent is authoring the JSON.

On commit 80caec73..., I tried the project’s own workflow validator against a few tiny cases. The result was roughly:

Case Validation
normal task step valid
unknown step property when valid
unknown step property retry valid
unknown step property select valid
typo relase_pipeline valid
known release_pipeline with the wrong type invalid
missing required step name invalid
unknown pipeline.configuration property invalid

So this does not look like “validation is generally permissive”. It looks more specifically like step-level unknown properties are currently open, while several other layers are checked much more strictly.

That creates an interesting agent-facing failure mode: an agent could invent a plausible-looking property such as when or mistype a real property, get a successful preflight, and then assume semantics that the runtime does not actually implement.

Given that one of the nice properties of this project is catching agent mistakes before model loading / GPU work, I wonder whether step-level unknown properties might deserve at least a warning.

I would not automatically make them hard errors, though. There seem to be a few reasonable policies depending on what you want the workflow format to become:

unknown step fields are intentional extension points
    → keep them open, maybe warn

forward compatibility matters, but typos should be visible
    → warning by default + optional strict mode

the step vocabulary is intended to be closed
    → schema error is probably simplest

JSON Schema itself permits additional properties by default, so the current behavior is not surprising by itself. The question is more whether that default is desirable for an agent-authored DSL.

There is a fairly close failure shape in ComfyUI issue #15669: programmatically supplied unknown inputs can be silently ignored while validation succeeds and the generation still runs. That is a different codebase and not necessarily the same root cause, but I thought it was a useful example of why silent unknown fields become more noticeable once software rather than a human is constructing the workflow.

Why I think selector/reducer is a useful boundary

The current workflow machinery already seems to have most of the surrounding pieces:

  • for_each expands work over multiple inputs;
  • gather: collects the resulting members;
  • utility tasks already provide a place for non-pipeline transformations;
  • previous_result: carries the selected/transformed result into the next stage.

So there are at least three increasingly powerful things that could be kept distinct.

1. Semantic selection

Examples:

  • which composition looks best;
  • which image follows the prompt best;
  • which result has the best character consistency;
  • which frame the user simply prefers.

For these, I think the existing flow is strong:

job A
→ agent inspects outputs
→ choose
→ keep_output
→ job B

The agent is doing something it is actually useful for, and the workflow engine does not need to grow a control-flow language.

2. Deterministic reduction

Examples:

argmax(scores)
first(candidate.score >= threshold)
min(artifact_score)
candidate[index_returned_by_previous_task]

These do not necessarily require an agent decision at all.

If this lived as a normal task/reducer, the workflow could potentially preserve the selection rule itself alongside the rest of the recipe, while still avoiding general branching semantics.

It also gives a useful place to put cheap filtering before an expensive stage:

cheap candidate generation
→ cheap evaluation
→ reduce N → 1
→ expensive video/upscale/etc.

That is the “shape” I was mainly thinking of.

3. General conditional execution

Only after that do we get to things like:

if no candidate passes:
    generate another batch
else:
    continue to video

That is qualitatively different. Now the DSL is not only moving data between steps; it is choosing which steps exist in the execution path.

I would probably avoid conflating this with simple selection unless a concrete workflow needs it.

A little more detail on the preflight observation

The current workflow schema lists the recognized step fields, but the step object is not closed against additional properties.

That explains why a property such as:

{
  "name": "something",
  "when": "previous_result:judge.pass",
  "...": "..."
}

can be structurally accepted even though when is not part of the documented step vocabulary.

As a counter-check, adding additionalProperties: false only to the step schema caused the tested unknown properties and the relase_pipeline typo to fail validation while the normal baseline still passed. I do not take that as evidence that strict mode is necessarily the right policy; it just shows that the distinction is mechanically straightforward if a closed vocabulary is desired.

There is also some useful precedent inside the project itself: other layers already distinguish unknown or unused inputs rather than silently treating everything as meaningful. That is why a warning feels like a particularly low-risk option here.

A warning also has a nice property for agents: it can say something concrete such as:

Step "video": unknown property "when"; it will not affect execution.

That makes the contract much clearer than either silently accepting it or introducing an immediate compatibility-breaking error.

The ComfyUI issue I mentioned above is useful mainly because it demonstrates the operational failure mode. Their reported problem was not “the program crashed”; it was almost the opposite — the workflow looked valid enough to run, the expensive work happened, but an intended input had silently had no effect.

That seems like exactly the sort of thing a strong preflight layer is well positioned to prevent.

If general branching eventually becomes useful

If you ever do decide that the workflow itself should contain real conditions, I think the difficult part is less the syntax of when than the semantics around skipped steps.

For example:

A → maybe skipped
B → previous_result:A

What does B receive if A was skipped?

Some possibilities are:

  • the reference is absent and B also skips;
  • the reference is absent and validation/runtime fails;
  • B supplies a default;
  • a fallback producer is selected;
  • skipped steps produce an explicit sentinel value.

General workflow engines have to define this fairly carefully. For example, Argo’s variable/output rules explicitly deal with outputs from skipped/omitted producers rather than treating a condition as only a boolean attached to a step.

For this project there would also be questions around things that are already useful features:

  • what constitutes the cache key after a branch;
  • whether a skipped step has any cache/manifest record;
  • whether the branch decision itself is recorded for replay;
  • how previous_result: behaves across skipped producers;
  • whether changing only the selector/condition can reuse upstream cached candidates.

Retry has a similar distinction.

A technical retry:

request failed transiently
→ retry with bounded backoff

is different from a quality retry:

generation succeeded
→ nothing passed the quality threshold
→ spend more GPU time generating another batch

The second one is really an optimization/search policy, not just failure recovery. If it ever becomes part of the workflow language, I think it would be worth keeping those concepts separate.

None of this is an argument that general branching is needed now — actually, it is part of why the smaller gather → deterministic reducer shape looks attractive to me as a first boundary.

So if I had to reduce this to two concrete pieces of feedback:

  1. A possible missing shape: deterministic N→1 selection/reduction between candidate generation and an expensive downstream stage, while leaving subjective “best” decisions with the agent.

  2. A small preflight hardening opportunity: make unknown step-level properties visible somehow — warning, strict mode, or hard error depending on the intended extensibility of the workflow format — so a plausible agent-invented field cannot silently look meaningful.

Both seem compatible with the existing agent-first design rather than requiring it to become a general-purpose workflow engine.

Before: acknowledged_cost=true was checked in the MCP layer but tied to nothing. The quoted number came from catalog defaults, not actual args, so a plan could grow after you consented and the flag would still read true.

Now:

- validate_workflow / POST /api/validate returns a plan block: a fingerprint of the realized workflow (real args, expanded for_each lists), the actual step/iteration count, required downloads, and a cost estimate with a basis flag (catalog, derived, per_entry, or unknown) so you know how much to trust the number.

- acknowledged_cost can now take {fingerprint, minutes, downloads} instead of a bare boolean. If the workflow’s shape changes after you bind to a plan — different args, longer list, whatever — the server 409s and re-quotes instead of letting it run over. Bare `true` still works for backward compat, but the job now logs which form it got (none / boolean / bound), so you can require the bound form for unattended agents.

- Your test case — a fan-out list changing real cost while the quote stayed flat — was the actual bug: the estimate wasn’t re-pricing per list length. Fixed. A 2-shot vs 10-shot run now gets genuinely different numbers, with the basis flag telling you if it’s measured or estimated.

Thanks for the suggestion!

BTW the validation message now has this shape and includes the case where a lengthy model download is needed

{
  "valid": true,
  "errors": [],
  "plan": {
    "fingerprint": "sha256:7c4f7cc3a91e8b2d4f0c6a5e9b1d3f2a8c7e6d5b4a3928170f6e5d4c3b2a1908",
    "steps": 13,
    "list_entries": { "shots": 10 },
    "cached_steps": 0,
    "downloads_required": [
      { "repo": "black-forest-labs/FLUX.1-dev", "gb": 23.8 }
    ],
    "estimate": {
      "minutes": 84.0,
      "basis": "derived",
      "device": "cuda",
      "measured_on": "RTX 3090",
      "partial": false
    }
  }
}

Thanks for this - both points landed, and you were right about the split.

**Unknown step properties.** Confirmed, and your table is exact. The step object is the one place in the schema that is open where it should be closed: `pipeline_configuration` and `chain` already reject unknown keys, the engine reads only the documented step keys, and nothing in the shipped catalog carries a stray one - so closing it costs nothing today.

I’m going with a hard error rather than a warning. The forward-compatibility case doesn’t arise here: an agent validates against the server it is about to run on, and the catalog ships with the engine, so there is no older-engine-reads-newer-workflow path to protect. And a warning is precisely the thing an agent skims on its way to `run_workflow`. The error will name the step and the key (`steps[2] “video”: unknown property “when”`), the same shape the other reference errors take, so the agent can act on it. The other open objects (`pipeline`, `task`, `workflow`) get the same treatment once each has had its own stray-key sweep.

Your ComfyUI #15669 reference is apt - “it ran, the expensive part happened, the input did nothing” is exactly the failure the preflight exists to prevent, and it’s a worse failure than a crash.

Ticket: validate_workflow accepts unknown step-level properties (`when`, `retry`, a typo'd `relase_pipeline`) — close the step object · Issue #118 · dkackman/diffusers-workflow · GitHub

**Deterministic N→1 selection.** Agreed on the boundary: subjective choice stays with the agent, and `keep_output` + `asset:` is the sanctioned way to carry it into the next job. The deterministic reducer is a real gap, and the part I’d push on is the score - the engine has nothing today that emits a number per candidate, so `select` on its own reduces over nothing but an index. The useful version is a scorer task (a VLM judge with a rubric first, since `image_to_text` already loads the model; aesthetic/CLIP second) plus a `select` reducer with a closed rule set (`argmax`, `argmin`, `first_above`, `index`) that returns one artifact and records the winner in the manifest so the recipe replays.

Writing it up turned up one thing you couldn’t have seen from the outside: there is no reference that hands a task *all* of one step’s artifacts - every `previous_result:` iterates - so `gather:` over a `for_each` group is the only whole-list reference. The proposal builds on that (fan out with `for_each`, score with a second group over the same list, reduce over the two gathers), which turns out to be the better shape anyway: members are named, cached individually, and the winner carries an entry name rather than an index.

Your skipped-step questions are the reason I’d rather ship the reducer than `when`: every step runs, so `previous_result:` never means “maybe absent”; the winning index goes in the manifest and the realized `workflow.json`; and the step cache already gives a selector-only change a free rerun of the candidates - the property you asked about holds by construction. “Nothing passed the threshold” is an error at the reducer, with every candidate and score in the manifest, and the agent decides whether to draw a new seed - which keeps quality-retry where you put it, as a search policy rather than failure recovery. Technical retry, if it ever lands, belongs to the job manager, not the workflow.

Proposal: diffusers-workflow/docs/proposals/score-and-select.md at master · dkackman/diffusers-workflow · GitHub

Ticket: Deterministic N→1 selection between a fan-out and an expensive stage (`select` reducer + `judge` scorer) · Issue #119 · dkackman/diffusers-workflow · GitHub

Wouldn’t it be easier to just have an agent automate Comfy graphs? I think there’s already tooling for that. I know Blender supports it. I bet Comfy, Divinci, Autocad, etc, all have it too these days. It’s been a while, but my recollection is that Comfy with plugins can run just about any diffuser workflow imaginable, has a vast ecosystem, ongoing support, and an excellent gui.

Anyhow, not putting down on your cool project. Just genuinely curious if you considered it (I’m sure you did).

Early on (SD 1.4 times, pre-comfy) I started playing with diffusers automation and never really stopped. So easier? Probably. As much fun for me? Definalty not!

Where I think this project might offer something different, is in the workflows themselves and their legibility to agents. I haven’t played with comfy via MCP so maybe it can do this but, I’m getting this thing to the point where an agent can author, direct, narrate, score, shoot and post produce complex workflows from simple prompts. Deepseek 4.1 flash authored the workflow below. My prompt was “make a dead-serious BBC-style nature documentary about the ecosystem inside a kitchen sponge, macro cinematography, hushed narration, the works.”

https://huggingface.co/buckets/dkackman/examples/tree/InnerSpaceEdit-film.14-0.0.mp4
This is the node graph it came up with. It picked the models (H3) and even auditioned multiple narrators, asking which I preferred. My input was purely editorial. The MCP offers templates, tools (including the ability to go get different models or author new templates), and tasks that describe their inputs, what they do and their outputs, and the agent assembles things from there.