[![Actions Status](https://github.com/m-doughty/ComfyUI-API/actions/workflows/test.yml/badge.svg)](https://github.com/m-doughty/ComfyUI-API/actions)

NAME
====

ComfyUI::API - Raku client for the ComfyUI image generation API with workflow templating

SYNOPSIS
========

```raku
use ComfyUI::API;

# Load and template a workflow
my $wf = ComfyUI::API::Workflow.from-file('my-workflow.json'.IO);
my $rendered = $wf.render(%(
    positive_prompt => 'a portrait of a knight in shining armor',
    negative_prompt => 'blurry, low quality',
    model_name      => 'sd_xl_base_1.0.safetensors',
));

# Or modify nodes directly
$wf.set('3', 'seed', 12345);
$wf.set-by-title('KSampler', 'steps', 30);

# Submit and wait for result
my $client = ComfyUI::API::Client.new(:base-url('http://localhost:8188'));
my $result = $client.submit-and-wait($rendered);

# Download generated images
for $result.image-filenames -> $filename {
    my Buf $image = $client.download($filename);
    "output/$filename".IO.spurt($image, :bin);
}
```

DESCRIPTION
===========

ComfyUI::API provides a Raku client for ComfyUI's REST API. Load workflow JSONs, template them with open-ended `{{variable}}` substitution, submit to a ComfyUI server, and download generated images.

ComfyUI::API::Workflow
----------------------

Load, template, and modify ComfyUI workflow JSONs.

```raku
# Load
my $wf = ComfyUI::API::Workflow.from-file($path);
my $wf = ComfyUI::API::Workflow.from-json($json-string);
my $wf = ComfyUI::API::Workflow.from-hash(%data);

# Template — replaces {{var}} in ALL string values throughout the workflow
my $rendered = $wf.render(%(key => 'value', ...));

# Direct node manipulation
$wf.set('3', 'seed', 42);                           # By node ID
$wf.set-by-title('KSampler', 'steps', 30);          # By node title
my @ids = $wf.find-nodes-by-class('CLIPTextEncode'); # Find by class

# Serialize
$wf.to-json;   # JSON string
$wf.to-hash;   # Raw Hash
```

ComfyUI::API::Client
--------------------

HTTP client for ComfyUI server. Blocking by default.

```raku
my $client = ComfyUI::API::Client.new(
    :base-url('http://localhost:8188'),  # Default
    :client-id('my-app'),               # Optional, auto-generated UUID
);

# Submit and poll
my $prompt-id = $client.submit($workflow);
my $result = $client.poll($prompt-id, :timeout(300e0), :interval(1e0));

# Or one-step
my $result = $client.submit-and-wait($workflow, :timeout(300e0));

# Download images (use type 'temp' for PreviewImage, 'output' for SaveImage)
my Buf $image = $client.download($filename, :type<temp>);

# Server info
my %queue = $client.queue-status;
my %nodes = $client.node-info;
```

### Health probe

`health` hits `/system_stats` with a short timeout and returns whether the server responded with 200. Useful as a connection check from a settings UI.

```raku
if $client.health(:timeout(2e0)) {
    say "ComfyUI is up";
}
else {
    say "Can't reach ComfyUI at {$client.base-url}";
}
```

### Parsed object_info: list-checkpoints / list-loras / list-samplers / list-schedulers / list-vaes

These walk the `/object_info` introspection tree and return sorted, de-duplicated filename or enum lists. Each accepts an optional pre-fetched `:%info` hash so a caller populating multiple dropdowns can fetch `/object_info` once and reuse.

```raku
# One HTTP call shared across helpers
my %info       = $client.node-info;
my @checkpoints = $client.list-checkpoints(:%info);
my @loras       = $client.list-loras(:%info);
my @samplers    = $client.list-samplers(:%info);
my @schedulers  = $client.list-schedulers(:%info);
my @vaes        = $client.list-vaes(:%info);

# Or each call fetches independently
my @ckpts = $client.list-checkpoints;
```

If a required node class (e.g. `LoraLoader`) isn't registered on the server — typically because a custom node pack is missing — these throw [X::ComfyUI::IntrospectionMissing](#x-comfyui-introspectionmissing) with the class name and field that couldn't be found.

### Progress events: progress-supply

`progress-supply` returns a hot [Supply](Supply) of typed progress events from ComfyUI's WebSocket interface (`/ws`). Each tap subscribes to all subsequent events; the underlying connection is shared across taps and reconnects automatically on socket drop. Filter by `prompt-id` to get events for a specific submission.

```raku
my $tap = $client.progress-supply.tap(-> %ev {
    given %ev<type> {
        when 'queue'           { say "queue pending: %ev<pending>" }
        when 'execution-start' { say "started %ev<prompt-id>" }
        when 'executing'       { say "running node %ev<node>" }
        when 'progress'        { say "step %ev<value>/%ev<max> on %ev<node>" }
        when 'executed'        { say "node %ev<node> finished" }
        when 'execution-end'   { say "done %ev<prompt-id>" }
        when 'error'           { say "error: %ev<message> at %ev<node>" }
    }
});

# ... later
$tap.close;
```

Emitted variants:

    { type => 'queue',           pending => Int, running => Int }
    { type => 'execution-start', prompt-id => Str }
    { type => 'executing',       prompt-id => Str, node => Str }
    { type => 'progress',        prompt-id => Str, value => Int, max => Int, node => Str }
    { type => 'executed',        prompt-id => Str, node => Str, output => Hash }
    { type => 'execution-end',   prompt-id => Str }
    { type => 'error',           prompt-id => Str, message => Str, node => Str }

### parse-progress-message

The pure parser is exposed for callers who need to drive their own WebSocket loop or test against canned message hashes. Pass a decoded ComfyUI message hash (the raw JSON payload after `from-json`) and get back the same typed event hash that `progress-supply` would emit, or an empty hash for unrecognized messages and binary preview frames.

```raku
my %ev = $client.parse-progress-message(%(
    type => 'progress',
    data => %( value => 12, max => 30, prompt_id => 'abc', node => '7' ),
));
# %ev = { type => 'progress', prompt-id => 'abc', value => 12, max => 30, node => '7' }
```

X::ComfyUI::IntrospectionMissing
--------------------------------

Thrown by `list-checkpoints`, `list-loras`, `list-samplers`, `list-schedulers`, and `list-vaes` when a required node class or field is missing from the server's `/object_info`. Carries the `node-class` and `field` on the exception so callers can surface a helpful "your ComfyUI install is missing X" message.

```raku
use ComfyUI::API::Exception;

CATCH {
    when X::ComfyUI::IntrospectionMissing {
        warn "Missing introspection: {.node-class}.{.field}";
    }
}
```

ComfyUI::API::Result
--------------------

Parsed result from a completed generation.

```raku
$result.prompt-id;                 # The prompt ID
$result.images;                    # List of image info hashes
$result.image-filenames;           # List of filename strings
$result.output-for-node('9');      # Output for a specific node
$result.raw;                       # Raw history data
```

AUTHOR
======

Matt Doughty <matt@apogee.guru>

COPYRIGHT AND LICENSE
=====================

Copyright 2026 Matt Doughty

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

