AgentOS markAgentOS

Image Flows


An Image Flow is a small node graph that makes images: a prompt goes in, optionally a prompt writer or an image description in between, one or more image model calls, and a Save node that writes PNG files. You build flows in the desktop app (Flows in the sidebar, after Jobs); the gateway stores and runs them, so the same flow can be run again from the editor, from the terminal (agentos flows run) or by an agent (flow_run).

Two things make a flow worth having over a one-off image_generate call:

  • It is reusable. A flow keeps its prompt, model, aspect, count and the wiring between steps. Its public inputs (for example prompt and count) are the values a run may override; everything else stays as you set it.
  • It is cached. Every node's result is cached by what went into it. Run a flow again with one input changed and only the nodes downstream of that input call a model again; the rest are reported as cached and cost nothing.

Concepts

  • Links carry one of two types: TEXT or IMAGE (a list of 1..n images). An input that has a type can be linked; when it is, the link wins over the input's own value. Numbers, choices and toggles are values only.
  • A run has targets: the node you asked for (--to NODE) or else every node with no outgoing link. Only the targets and what they need run.
  • Run status is queued, running, done, failed or cancelled. Each node is queued, running, cached, done, error or skipped. When a node fails, nodes already calling a model finish (and are cached), nothing new starts, the rest are skipped, and the run is failed.
  • Runs queue first in, first out; [flows].max_concurrent_runs (default
    1. run at once, at most 2 calls per image provider. A run still queued or running when the gateway stops ends failed with flows.interrupted.

Nodes

NodeInputsOutputNotes
texttextTEXTA fixed piece of text.
load_imageimage (an uploaded image)IMAGEUpload in the editor or drop a PNG.
join_texta, b (optional), template (default {a}, {b})TEXTA missing b drops the dangling , .
prompt_writeridea, context (optional, link only), styleTEXTTurns a short idea into a detailed image prompt with the auxiliary model (task flow_prompt). Paid.
describe_imageimage, focusTEXTDescribes an image with the vision model the image tool uses (task vision). Paid.
imageprompt, images (optional, several links), model, aspect / resolution / size / quality / background (shown when the model has them), count (1-4), variationIMAGEText to image with nothing linked to images; edit / multi-reference with 1..n images linked. variation = new always calls the model again; keep reuses the cached result for the same inputs. Paid.
saveimages (several links), name (default {flow}_{run}_{n})noneWrites PNG files that carry the flow inside them (an agentos-flow text chunk with the flow id, revision, run id and graph; never a key).

agentos flows nodes lists them from the running gateway.

Models and keys

agentos flows models lists every image model with whether it can be used, why not, and its price per image. A model is available when its provider has an API key; [image_generation].enabled only gates the image_generate agent tool and does not affect flows. Keys and base URLs come from [image_generation.providers.*]:

ProviderKey (default env)Models
Surplus IntelligenceSURPLUS_API_KEY ([image_generation.providers.surplus])Nano Banana 2 / 2.1 / Pro, GPT Image 2, Seedream, FLUX.2 Pro, Qwen Image (from Surplus's public catalog, refreshed every 6 h, with a bundled snapshot as fallback).
OpenRouterOPENROUTER_API_KEYNano Banana 2 / 2.1 / Pro, GPT-5 Image Mini.
OpenAIOPENAI_API_KEYGPT Image (openai/gpt-image-1): sizes, qualities, transparent background.

Set a key with agentos env set SURPLUS_API_KEY (or the desktop's Settings). A node's model = auto means [image_generation].primary when that names an available flow model (the built-in default openai/gpt-image-1 does not count as a choice), else the first available model in the order above (Surplus first), else none: an image node then fails validation with "No image model is available" before anything is called.

Costs

Prices are the best known public per-image prices and are marked approximate (~). An estimate is per node: per_image × count + per_ref × linked images; the run's total adds only the nodes that will run and are not cached. A node's recorded cost is what the provider reported when it reports one, else the estimate. Costs are booked in the usage tracker under the scope flow:<run_id> (see agentos cost).

Running flows from the terminal

agentos flows list                                   # saved flows, newest first
agentos flows show "Lighthouse study"                # nodes, links, public inputs
agentos flows nodes                                  # node types
agentos flows models                                 # image models, keys, prices
agentos flows run "Lighthouse study" --input prompt="a red fox in snow" --input count=2
agentos flows run flw_2af4f4b5 --to n2 --json        # only up to node n2
agentos flows run flw_2af4f4b5 --no-wait             # queue and return the run id
agentos flows runs "Lighthouse study"                # recent runs of one flow
agentos flows stop run_0123456789ab

run resolves the flow by id, or by exact name (ids win; two flows with the same name are refused, pass the id). It waits for the run (polling flows.run.get every second, --timeout seconds, default 600) and prints the status, node progress and cached count, the cost against the estimate (written like the editor: free, ~$0.067, ~$0.0034–$0.10, unknown), and the saved files:

run_0123456789ab  Lighthouse study  done  3/3 nodes, 1 cached
cost $0.050 (estimate ~$0.067)
1 image(s):
  ~/.agentos/flows/outputs/flw_2af4f4b5/Lighthouse-study_run_0123456789ab_1.png  1024x1024

Exit codes: 0 when the run is done; 1 when it failed, was cancelled or is still going at the timeout (it keeps running; check agentos flows runs); 2 for an input the engine refuses (unknown flow, a name that is not a public input, a graph that does not validate: each node error is printed). With --json, stdout is one summary object (run_id, status, images[] with path, sha256, width, height, cost_usd, est_usd, cached, nodes {done,total}, estimate (the total Price), error, node_errors, timed_out) and errors are a JSON envelope on stderr. Runs started here carry origin: "cli" and show up live in the editor.

Like every gateway-backed command, agentos flows connects to AGENTOS_GATEWAY_URL, else the port in the file AGENTOS_GATEWAY_CONFIG_PATH names, else ws://localhost:18791/ws; AGENTOS_STATE_DIR alone does not move it (point AGENTOS_GATEWAY_URL at a gateway on another port).

Running a flow spends money, so flows run and flows stop are the user's actions: from an agent's shell (an agent-bound connection) the gateway refuses them with flows.operator_required. Agents use flow_run instead.

Agents: flow_list and flow_run

Two built-in tools let an agent use the flows you built (three more let it build and edit them: see Agents can build flows). Both are in the deferred set (the model loads them with tool_search), and both are offered only while [flows].enabled is on and at least one image model has a key.

  • flow_list() returns each flow's id, name, node count and public inputs with their current values, plus the default model and the agent cost limit.
  • flow_run(flow, inputs?, wait_seconds?) runs a flow by id or exact name, with inputs overriding public inputs only. The tool descriptions steer the model to flow_run when you ask to run or reuse a flow you built, and to image_generate for a one-off picture. When image_generate is not available (off, or no key) but flows are, the system prompt tells the model that it can make the picture you ask for by running one of your flows, and that it should ask you to create one in the Flows editor when none fits; the tools never create a flow themselves. Their descriptions carry the words image, picture, draw, photo and illustration, so tool_search finds them for an image request.

The cost gate. flow_run asks the engine for the estimate before anything is queued. A known estimate at or under [flows].agent_cost_limit_usd (default $0.50) runs at once. A higher estimate, or one that cannot be known, goes through the approval queue: you see "Runs the Image Flow 'Lighthouse study' for an estimated ~$0.27, above the $0.50 agents may spend per run without asking" in the same prompt that guards shell commands, and the turn waits for you. The approval is bound to that flow, its revision and those inputs; the run is then capped at the approved amount (if the estimate rose in between, nothing is queued and the agent is told). The prompt has its own approval namespace, flow, and offers only allow once or deny: no "Always Allow" and no "Stop Asking" (the gateway ignores both for this namespace), so approving one run never unlocks the next, and a session bypass of shell prompts never approves a spend.

A "no" sticks. Denied, the agent gets a refusal that tells it plainly not to retry or work around it (fewer images, other inputs, a cheaper model, another flow, image_generate) unless you ask, and nothing is spent. For the next 30 minutes every flow_run of that flow in that session needs a fresh approval, even one under the limit; the prompt says "You declined a run of the Image Flow ... N min ago". A prompt the agent stopped waiting for counts when you decline it later. Approving such a prompt lifts the block. Other sessions and other flows are not affected; the memory is in the gateway process, so a gateway restart clears it. (A new chat message is not taken as consent on its own: whether it "asks again" cannot be told reliably, so the fresh approval prompt is the consent.)

The approval mode applies as usual (auto-approve runs without a prompt, auto-deny refuses and counts as a "no"); an unattended run (cron, heartbeat) refuses instead of waiting. Direct tool calls over HTTP (/tools/invoke) never run flow_run.

One-off variants. flow_run(flow, overrides={"<image node>": {...}}) changes model, count, aspect, resolution, size, quality or background of an image node for that run only; the saved flow stays as it is. The cost gate uses the overridden estimate, and an approval is bound to those overrides (approving 2 images does not approve 4). The agent uses this for "make 4 of these with GPT Image 2" instead of saving that configuration. count stays at most 4 per image node: for more, the agent runs again (each run cost-gated on its own) or asks whether 4 will do.

The tool waits for the run (wait_seconds, default 300, at most 900). A run still going then keeps running; the tool says so and the card updates when it ends. Stopping the turn stops its run.

What lands in the chat. Each PNG a Save node writes is published into the session as an image artifact while the run executes (channels deliver them like any generated image), and a run card is published ahead of them:

  • Run card (application/vnd.agentos.flow-run+json): the flow name, the status, node progress n/m with a bar, the cost (or the estimate while it runs), the cached count and thumbnails. The card re-reads the run with flows.run.get whenever it is drawn and follows the flows.run / flows.node events while the run is going, so a reload or a re-render never shows a stale snapshot. Thumbnails come from flows.media (256 px). It is read-only. On the desktop it has Open in Flows, which opens the flow in the editor; the web console has no editor and no button.

The tool's result (what the model reads) carries status, run_id, flow {id, name}, images[] (path, sha256, width, height, artifact_id), cost_usd, est_usd, cached, nodes {done, total}, error, node_errors and the published artifacts.

The card payload

{
  "version": 1,
  "kind": "flow_run",
  "run_id": "run_0123456789ab",
  "flow_id": "flw_2af4f4b5",
  "flow_name": "Lighthouse study",
  "status": "done",
  "origin": "agent",
  "progress": {"done": 3, "total": 3},
  "cached": 1,
  "est_usd": 0.0672,
  "cost_usd": 0.05,
  "images": [{"sha256": "…", "mime": "image/png", "width": 1024, "height": 1024,
              "name": "Lighthouse-study_run_0123456789ab_1.png"}],
  "error": null,
  "created": "2026-10-29T10:00:00Z",
  "ended": "2026-10-29T10:00:21Z",
  "fetchedAt": "2026-10-29T10:00:21Z"
}

images lists what the Save nodes wrote, or the generated images when the flow has no Save node (name is then null). progress.done counts done and cached nodes. A client that has the run (flows.run.get) derives the same fields from the RunRecord.

Agents can build flows

An agent can also create and edit your flows from the chat, with three more tools in the deferred set (found by tool_search for "create flow", "build workflow", "pipeline", "automate images", "edit flow", "add node"). Building a flow costs nothing, so there is no approval step; running it is still flow_run, with its cost gate. No tool deletes a flow, and every save, yours or the agent's, keeps the previous version: the editor's version history lists each revision with its author (you or the agent, and the chat it came from) and restores any of them.

  • flow_get() returns the node catalog (types, inputs with their widgets, options and defaults, outputs), the image models (availability, edit support, how many reference images, ratios, prices) and the spec format. flow_get(flow) returns that flow as a spec, its revision, who created it, and its public inputs.
  • flow_create(spec) validates the spec, lays the nodes out and saves a new flow marked as created by the agent.
  • flow_update(flow, spec, base_rev) replaces the graph of an existing flow (yours or the agent's). base_rev is required: the revision the agent read with flow_get (or got back from its last save). If the flow changed since (you edited it, or restored an older version), nothing is saved and the agent is told the current revision and what changed, so it reads the flow again and re-applies only the change you asked for; it never writes a graph from memory over yours. Nodes keep their place by id, new ones are placed for them; the name stays unless the spec renames it. The result says what changed in one line.

A spec has no positions and writes links as pairs:

{"name": "AgentOS infographic",
 "nodes": [{"id": "ref", "type": "load_image", "title": "Style reference",
            "values": {"image": {"attachment": "latest"}}},
           {"id": "content", "type": "text", "values": {"text": "..."}},
           {"id": "gen", "type": "image", "values": {"model": "surplus/gpt-image-2", "aspect": "21:9"}},
           {"id": "out", "type": "save"}],
 "links": [["ref.image", "gen.images"], ["content.text", "gen.prompt"], ["gen.image", "out.images"]],
 "inputs": {"content": "content.text"}}

A load_image value may be an image already in the Flows media store, or:

  • {"attachment": "latest"}: the image you attached most recently in this chat ("2" is the one before, and so on), or its file name, or {"attachment": "<sha256>"};
  • {"attachment": "<artifact id>"}: an image published in this chat (for example a flow run's saved PNG, whose artifact_id the agent sees);
  • {"path": "<file>"}: a PNG, JPEG, GIF or WebP inside the agent's workspace. Anything outside it (an absolute path elsewhere, .., a symlink that leads out) is refused.

Chat images are read from this chat's transcript, where the gateway records each message you send with its attachments: an image up to 2 MB is stored in the message itself, a larger upload in the session's attachment store. An image from another chat cannot be reached, and the agent is told never to search the disk for your images; if it cannot find one, it asks you to attach it again. A spec that does not validate is not saved: the agent gets each problem by node (an unknown node type, a missing prompt, a model without edit support for a reference image, an image it could not resolve) and fixes it. A model whose provider has no key yet only matters at run time, so that saves with a warning.

After a save the chat shows a change card (application/vnd.agentos.flow-change+json): the flow's name, created or updated, the one-line summary, the node count, the estimate per run and the revision; on the desktop it has Open in Flows. The agent then says what the flow does and offers to run it; it runs it straight away only when you asked for the image now.

{"version": 1, "kind": "flow_change", "flow_id": "flw_2af4f4b5",
 "name": "AgentOS infographic", "rev": 1, "change": "created",
 "summary": "created with 4 nodes and 3 links", "node_count": 4,
 "estimate": {"kind": "usd", "usd": 0.27, "approximate": true},
 "fetchedAt": "2026-10-30T10:00:00Z"}

Direct tool calls over HTTP (/tools/invoke) never run flow_create or flow_update; flow_get is read-only.

Configuration

[flows]
enabled = true              # off: runs are refused (editor, CLI, agent tools) and the tools are hidden
max_concurrent_runs = 1     # 1-8; further runs wait in a FIFO queue
agent_cost_limit_usd = 0.50 # an agent's flow_run above this (or unknown) needs approval
cache_max_mb = 500          # node cache size, least recently used evicted first

Environment overrides: AGENTOS_FLOWS_ENABLED, AGENTOS_FLOWS_MAX_CONCURRENT_RUNS, AGENTOS_FLOWS_AGENT_COST_LIMIT_USD, AGENTOS_FLOWS_CACHE_MAX_MB. For example agentos config set flows.agent_cost_limit_usd 1.00.

State lives under <state_dir>/flows/ (~/.agentos/flows/ by default): <flow_id>.json flow documents, flows.db (runs and the node cache), media/ content-addressed images (and their thumbnails), surplus_catalog.json, and outputs/<flow_id>/ the PNG files Save writes.

RPC and events

Control surface (flows.*). Reads are open to every control connection; everything that writes or spends (create, save, delete, run, stop, upload, import_png) is refused on an agent-bound connection with flows.operator_required.

MethodParamsResult
flows.nodes–node catalog (types, inputs, outputs, prices)
flows.models–{default, models[]} with availability and prices
flows.list–{flows: [{id, name, rev, updated, node_count, preview}]}
flows.create{name, template?} (blank, text_to_image, edit_image, describe_remix){flow}
flows.get{id}{flow}
flows.save{flow, base_rev}{flow}; flows.conflict when the stored revision moved
flows.delete{id}{ok}
flows.validate{flow, to_node?}{ok, node_errors}
flows.estimate{flow, to_node?, inputs?}{nodes, total, will_run, cached}
flows.run{flow_id, flow?, inputs?, to_node?, origin?} (canvas or cli){run_id, status: "queued"}
flows.stop{run_id}{ok}
flows.runs{flow_id?, limit?}{runs: [RunSummary]} newest first
flows.run.get{run_id}{run: RunRecord} (summary + inputs, graph, nodes, outputs, to_node, estimate, seq)
flows.media{sha256, max_side?}{mime, data_b64, width, height}
flows.upload{name, mime, data_b64}{image}
flows.import_png{data_b64}{image, flow} (the graph a saved PNG carries, not saved)

Error codes: flows.not_found, flows.conflict, flows.invalid (details.node_errors), flows.busy, flows.unavailable, flows.cost_gate (details.estimate, details.limit_usd), flows.operator_required.

Events go to control connections; each carries run_id, flow_id and a per-run increasing seq. Clients that reconnect read flows.run.get.

  • flows.run {run_id, flow_id, seq, status, est_usd?, cost_usd?, error?}
  • flows.node {run_id, flow_id, seq, node_id, state, progress?, ms?, cost_usd?, outputs?, error?}
  • flows.changed {flow_id, rev, deleted?} after a create, save or delete.