Monday, April 6, 2026

Image Generation with ComfyUI From Bash

The best tool I've found to run AI image and video-generation models is ComfyUI. It supports a lot of different models and has a dataflow programming architecture that allows fairly sophisticated use cases. The problem for me, however, is that it's awkward to use it just to generate an image or two. It's also hard to use the web interface to iterate over different prompts or parameters. So this post describes a workflow for calling ComfyUI from Bash and previewing images directly from a terminal.

ComfyUI has a REST interface that can be accessed from the command line via curl. This is used by the web interface, so you get the full power of the program without having to click on little boxes to change parameters.

I won't cover installing ComfyUI. You'll have to figure that out yourself. But let's assume you have ComfyUI installed and running, and you've downloaded the model files for Z-Image Turbo and can generate images in your browser. Now you want to do the same from a command line.

First, you'll need a JSON file for your workflow. Load up the ComfyUI web interface and open up your workflow. As of this writing, you can right-click on the workflow tab at the top of the window and select "Export (API)" to create this file. So for example, if you do this with the default Z-Image Turbo workflow, you'll get something like this:

{
  "9": {
    "inputs": {
      "filename_prefix": "z-image-turbo",
      "images": [
        "57:8",
        0
      ]
    },
    "class_type": "SaveImage",
    "_meta": {
      "title": "Save Image"
    }
  },
  "57:30": {
    "inputs": {
      "clip_name": "qwen_3_4b.safetensors",
      "type": "lumina2",
      "device": "default"
    },
    "class_type": "CLIPLoader",
    "_meta": {
      "title": "Load CLIP"
    }
  },
  "57:33": {
    "inputs": {
      "conditioning": [
        "57:27",
        0
      ]
    },
    "class_type": "ConditioningZeroOut",
    "_meta": {
      "title": "ConditioningZeroOut"
    }
  },
  "57:8": {
    "inputs": {
      "samples": [
        "57:3",
        0
      ],
      "vae": [
        "57:29",
        0
      ]
    },
    "class_type": "VAEDecode",
    "_meta": {
      "title": "VAE Decode"
    }
  },
  "57:28": {
    "inputs": {
      "unet_name": "z_image_turbo_bf16.safetensors",
      "weight_dtype": "default"
    },
    "class_type": "UNETLoader",
    "_meta": {
      "title": "Load Diffusion Model"
    }
  },
  "57:27": {
    "inputs": {
      "text": "A sea lion on a beach, holding a sign that says, \"Command-Line Interfaces Rock!\"",
      "clip": [
        "57:30",
        0
      ]
    },
    "class_type": "CLIPTextEncode",
    "_meta": {
      "title": "CLIP Text Encode (Prompt)"
    }
  },
  "57:13": {
    "inputs": {
      "width": 1024,
      "height": 1024,
      "batch_size": 1
    },
    "class_type": "EmptySD3LatentImage",
    "_meta": {
      "title": "EmptySD3LatentImage"
    }
  },
  "57:11": {
    "inputs": {
      "shift": 3,
      "model": [
        "57:28",
        0
      ]
    },
    "class_type": "ModelSamplingAuraFlow",
    "_meta": {
      "title": "ModelSamplingAuraFlow"
    }
  },
  "57:3": {
    "inputs": {
      "seed": 277911290314474,
      "steps": 8,
      "cfg": 1,
      "sampler_name": "res_multistep",
      "scheduler": "simple",
      "denoise": 1,
      "model": [
        "57:11",
        0
      ],
      "positive": [
        "57:27",
        0
      ],
      "negative": [
        "57:33",
        0
      ],
      "latent_image": [
        "57:13",
        0
      ]
    },
    "class_type": "KSampler",
    "_meta": {
      "title": "KSampler"
    }
  },
  "57:29": {
    "inputs": {
      "vae_name": "ae.safetensors"
    },
    "class_type": "VAELoader",
    "_meta": {
      "title": "Load VAE"
    }
  }
}

This JSON file describes the graph that's used to generate the image. You'll need to wrap this in a larger JSON object and send that to ComfyUI to get an image back. So create a file named zimageturbo.json and put the following in it:

{
  "client_id": "e70c5721-5e9f-43b4-bcb3-23bf05fec938",
  "prompt_id": "5719972c-88d3-45b7-b441-ef06f0b1a011",
  "prompt":
    /* insert your workflow JSON from above here */
}

The long HEX numbers are just UUIDs I generated with uuidgen. You'll need to generate a new prompt_id UUID one for each prompt you submit and a new client_id from each computer you want to connect from.

Now you can send this to the ComfyUI instance with curl. Assuming your server is at the default http://127.0.0.1:8188, you can do:

$ curl -s --url "http://127.0.0.1:8188/prompt" --json @zimageturbo.json

Now you wait for the image to be generated. You can submit requests to the history/[prompt_id] endpoint (filling in the prompt_id to match your JSON file) to see when the image has completed and where to find it:

$ curl -s --url "http://127.0.0.1:8188/history/5719972c-88d3-45b7-b441-ef06f0b1a011"

Before the image is completed, this will return nothing. After the image is completed, this will return some JSON:

{
  "5719972c-88d3-45b7-b441-ef06f0b1a011": {
    "prompt": [
      7,
      "5719972c-88d3-45b7-b441-ef06f0b1a011",
      {
        "9": {
          "inputs": {
            "filename_prefix": "z-image-turbo",
            "images": [
              "57:8",
              0
            ]
          },
          "class_type": "SaveImage",
          "_meta": {
            "title": "Save Image"
          }
        },
        "57:30": {
          "inputs": {
            "clip_name": "qwen_3_4b.safetensors",
            "type": "lumina2",
            "device": "default"
          },
          "class_type": "CLIPLoader",
          "_meta": {
            "title": "Load CLIP"
          }
        },
        "57:33": {
          "inputs": {
            "conditioning": [
              "57:27",
              0
            ]
          },
          "class_type": "ConditioningZeroOut",
          "_meta": {
            "title": "ConditioningZeroOut"
          }
        },
        "57:8": {
          "inputs": {
            "samples": [
              "57:3",
              0
            ],
            "vae": [
              "57:29",
              0
            ]
          },
          "class_type": "VAEDecode",
          "_meta": {
            "title": "VAE Decode"
          }
        },
        "57:28": {
          "inputs": {
            "unet_name": "z_image_turbo_bf16.safetensors",
            "weight_dtype": "default"
          },
          "class_type": "UNETLoader",
          "_meta": {
            "title": "Load Diffusion Model"
          }
        },
        "57:27": {
          "inputs": {
            "text": "A sea lion on a beach, holding a sign that says, \"Command-Line Interfaces Rock!\"",
            "clip": [
              "57:30",
              0
            ]
          },
          "class_type": "CLIPTextEncode",
          "_meta": {
            "title": "CLIP Text Encode (Prompt)"
          }
        },
        "57:13": {
          "inputs": {
            "width": 1024,
            "height": 1024,
            "batch_size": 1
          },
          "class_type": "EmptySD3LatentImage",
          "_meta": {
            "title": "EmptySD3LatentImage"
          }
        },
        "57:11": {
          "inputs": {
            "shift": 3.0,
            "model": [
              "57:28",
              0
            ]
          },
          "class_type": "ModelSamplingAuraFlow",
          "_meta": {
            "title": "ModelSamplingAuraFlow"
          }
        },
        "57:3": {
          "inputs": {
            "seed": 277911290314474,
            "steps": 8,
            "cfg": 1.0,
            "sampler_name": "res_multistep",
            "scheduler": "simple",
            "denoise": 1.0,
            "model": [
              "57:11",
              0
            ],
            "positive": [
              "57:27",
              0
            ],
            "negative": [
              "57:33",
              0
            ],
            "latent_image": [
              "57:13",
              0
            ]
          },
          "class_type": "KSampler",
          "_meta": {
            "title": "KSampler"
          }
        },
        "57:29": {
          "inputs": {
            "vae_name": "ae.safetensors"
          },
          "class_type": "VAELoader",
          "_meta": {
            "title": "Load VAE"
          }
        }
      },
      {
        "client_id": "e70c5721-5e9f-43b4-bcb3-23bf05fec938",
        "create_time": 1775495893332
      },
      [
        "9"
      ]
    ],
    "outputs": {
      "9": {
        "images": [
          {
            "filename": "z-image-turbo_00000_.png",
            "subfolder": "",
            "type": "output"
          }
        ]
      }
    },
    "status": {
      "status_str": "success",
      "completed": true,
      "messages": [
        [
          "execution_start",
          {
            "prompt_id": "5719972c-88d3-45b7-b441-ef06f0b1a011",
            "timestamp": 1775495893332
          }
        ],
        [
          "execution_cached",
          {
            "nodes": [],
            "prompt_id": "5719972c-88d3-45b7-b441-ef06f0b1a011",
            "timestamp": 1775495893333
          }
        ],
        [
          "execution_success",
          {
            "prompt_id": "5719972c-88d3-45b7-b441-ef06f0b1a011",
            "timestamp": 1775495906934
          }
        ]
      ]
    },
    "meta": {
      "9": {
        "node_id": "9",
        "display_node": "9",
        "parent_node": null,
        "real_node_id": "9"
      }
    }
  }
}

You need to look at the contents of [prompt_id]["outputs"]["9"]. You can extract this with the jq tool:

$ curl -s --url "http://127.0.0.1:8188/history/5719972c-88d3-45b7-b441-ef06f0b1a011" | 
jq '.["5719972c-88d3-45b7-b441-ef06f0b1a011"].outputs.["9"]'

Which will display:

{
  "images": [
    {
      "filename": "z-image-turbo_00000_.png",
      "subfolder": "",
      "type": "output"
    }
  ]
}

With that information, we can use the view endpoint to fetch the image:

$ curl -s --get --url "http://127.0.0.1:8188/view" -d 'filename=z-image-turbo_00000_.png' \
-d 'subfolder=' -d 'type=output' -o z-image-turbo_00000_.png

You can view the resulting image with a tool like timg or chafa. If you have a new enough terminal emulator, this can even display the full-resolution image.

Sea lions can't read or write, so this might be the best you can hope for.

And that's all there is to it. You can modify the JSON file to edit the prompt, image width and height, random seed, diffusion steps, or anything else you want to change. You can also export other workflows and use any ComfyUI-supported model from the command line this way.