API Reference
The backend serves a REST API and an execution WebSocket. All endpoints use the same origin as the app (http://localhost:8000 by default). The Auth column uses the five values defined under Authentication. Each table links to the relevant usage page.
The server also serves FastAPI's generated schema: GET /openapi.json (OpenAPI 3.1, every HTTP route on this page; the WebSocket is not in it), GET /docs (Swagger UI, with its helper page /docs/oauth2-redirect) and GET /redoc. All four are open. /docs and /redoc load their scripts from cdn.jsdelivr.net, so the browser needs internet access to show them; /openapi.json does not. When the prebuilt frontend is present, a GET for any other path outside /api/ and /ws/ returns the file at that path in the frontend build when there is one (the /assets/... bundle, /build-info.json, and in a release /LICENSE, /NOTICE, /THIRD_PARTY_NOTICES.md, /COMMERCIAL-LICENSE.md and /CONTRIBUTING.md), else index.html. A missing file under /assets/ and an unmatched path under /api/ or /ws/ are 404s, never the editor page.
Authentication
| Auth | Meaning |
|---|---|
| open | No credential. Most GET / HEAD / OPTIONS requests use this; the published-app and API-key rows below name the exceptions. |
| token | The X-CodefyUI-Token session header. The generic middleware requires it for every POST / PUT / PATCH / DELETE under /api/ except /api/apps and /api/keys, whose routes enforce one of the policies in this table. It is also the credential for every app-management route (GET /api/apps and GET /api/apps/{slug}/versions included) and all of /api/keys. Missing or wrong: 403 {"detail": "Missing or invalid X-CodefyUI-Token header"}. Where the token file lives and why it rotates on every restart: Graph as a Function, section 2. |
| token+loopback | The token, and the server must be bound to a loopback address (127.0.0.1, localhost, ::1) — otherwise 403 — unless CODEFYUI_ALLOW_REMOTE_PACK_INSTALL=1 (packs) or CODEFYUI_ALLOW_REMOTE_PLUGIN_INSTALL=1 (plugins) was exported first. The gate reads CODEFYUI_HOST, never the socket. |
| API key | Only Authorization: Bearer cdui_... is accepted. A session token returns an error that states that an API key is required. Authentication failures use status 401 inside the run envelope and include WWW-Authenticate: Bearer. Keys are created through /api/keys; see Publish. |
| worker token | The long-lived credential a worker agent gets from POST /api/workers/enroll in exchange for a one-time enrollment token, sent in the X-CodefyUI-Token header (or ?token=) on /ws/worker, and in the header on POST /api/runs/{run_id}/artifacts/upload for a run on that worker. Accepted from any peer; it names one worker, whose hello must use that name. DELETE /api/workers/{id} revokes it. The server stores only its sha256 hash. See Remote Workers. |
| enrollment token | abcdef.0123456789abcdef, generated with POST /api/workers/enrollments. Only POST /api/workers/enroll takes it, in the JSON body, without a session token; that route is rate-limited per client address (10 attempts, then 10 a minute; 429 with Retry-After). |
| key-or-token | Either credential is accepted. These metadata reads are another exception to the general rule that GET requests are open. A request with neither credential returns 401 {"detail": "..."}. |
The Host guard runs before all other checks on every request, including the SPA page and WebSocket. A Host header outside the allowlist returns 421 {"detail": "Misdirected Request (Host not allowed)"}. The allowlist is derived from CODEFYUI_HOST and CODEFYUI_PORT and extended by CODEFYUI_EXTRA_ALLOWED_HOSTS. An SSH worker's tunnel never reaches this port, and no Host marks a request as tunnelled: the tunnel lands on a listener the server opens for that worker alone (a Unix socket in a private directory; an ephemeral 127.0.0.1 port on Windows), which serves /ws/worker (worker token; the session token is refused there), POST /api/workers/enroll (enrollment token) and, with a worker token (403 without), the transfer backend's input downloads and POST /api/runs/{run_id}/artifacts/upload. Every other path there answers 404, GET /api/auth/bootstrap included, whatever the Host (Remote Workers, Connecting over SSH); see Publish, section 6 and Deployment. The token middleware exempts GET /api/auth/bootstrap so the frontend can obtain the token. Any request with an allowed Host can use this endpoint, including a remote client when the server is bound to a LAN address.
Health and system
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/health | GET | open | Health probe with status, version, boot_id (a process identifier that lets clients detect a server restart), nodes_loaded, presets_loaded, caches (current bytes in each in-memory store and its budget; see Training Memory), and project (the absolute project directory) when the server runs with --project. |
/api/auth/bootstrap | GET | open | {"token": "..."} for any allowed-Host request — how the frontend gets the session token. |
/api/system/devices | GET | open | Compute devices for graph execution: the best-available default plus a labelled devices list that tells NVIDIA CUDA, AMD ROCm and Apple MPS apart. Each device also carries memory: {total_bytes, free_bytes, source} read at request time (source is nvml, torch.cuda, torch.mps or psutil), or null when nothing could read it. NVIDIA cards are read through NVML (nvidia-ml-py), falling back to torch. Backs the editor's device selector, which shows free / total GiB for accelerators. |
/api/system/fingerprint | GET | open | This server's environment fingerprint and its digest: {fingerprint, digest}. See Run environment. |
/api/system/fingerprints/{digest} | GET | open | A fingerprint some run recorded, by its digest; 404 when no run recorded it. Keeps resolving after the runs are deleted. |
Nodes and presets
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/nodes | GET | open | List all node definitions. Each node carries details (its longer description, empty when description says it all), provider (builtin, custom, or plugin:<id>) and requires_pack (the pack the node declares, or null), and each SELECT param option_packs (option value to pack id). The editor keeps an unavailable current value selectable with a warning, greys out other unavailable options, and offers the install. Neither field stops a run; a node that must not run without its pack refuses in execute, as the built-in nodes do (see Custom Nodes). |
/api/nodes/{node_name} | GET | open | Get a single node definition. |
/api/nodes/reload | POST | token | Re-discover every node and preset source: custom nodes and plugins are re-imported from disk, built-ins are re-registered (not re-imported), presets are re-scanned. Returns {builtin, custom, plugins, presets, total}; identical to POST /api/plugins/reload. |
/api/nodes/script/validate | POST | token | Check one PythonScript body ({"code"}) against the Tier-0 policy while it is being typed: {ok, error, line, defines_run, allowed_modules}. ok: false is a normal 200, not an error. |
/api/presets | GET | open | List preset definitions. |
/api/presets/{name} | GET | open | Get a single preset definition. |
/api/presets/create | POST | token | Create a new preset from the request's complete nodes and edges; the editor submits the whole current canvas. |
Graphs
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/graph/validate | POST | token | Validate a graph: {valid, errors, issues}. errors lists each problem as an English sentence; issues has one {message, code, node_id, params} per problem, in the same order: message is that sentence, code names the check (null for a check that has no code yet), node_id is the node the problem is about, and params are the values the sentence names. The editor uses them to say each problem in the UI language and name the node by its title. |
/api/graph/save | POST | token | Save a graph. The body is the graph plus two optional fields: file, the address (filename stem) to write to, and overwrite. Omitting file means "derive the address from name", which is a Save As. Send overwrite: false with such a request to opt into the collision guard: one that lands on an address something already occupies then answers 409 with {"detail": {"error": "graph_exists", "file", "name"}} instead of writing, where file is the stem the server resolved and name is the title of the graph already there — re-send with overwrite: true to replace it. Leaving overwrite out entirely keeps the behaviour this route has always had: the save is written, occupied address or not, so existing clients and scripts are unaffected. A request that carries file is saving in place and is never refused this way. The response is {message, path, file}: path is the file written and file is the address the server derived, which later saves, loads and renames use. |
/api/graph/load/{name} | GET | open | Load a saved graph. |
/api/graph/list | GET | open | List saved graphs as [{name, file, modified}]: name is the title stored in the file, file is the address to load, rename or delete it by, and modified is the file's modification time in epoch seconds (in a project directory, the .graph.json file's, not the layout file's). In a project directory, 409 when a name exists both as <name>.graph.json and as a legacy <name>.json. |
/api/graph/rename | POST | token | Rename a saved graph: {"from": "<file>", "to": "<new name>"}, both non-empty; any other key is 422. Moves the file (and, in a project directory, its layout/ file) and writes to into it as its name. Returns {message, name, file}, where file is the sanitized address to use from then on. 404 when from does not exist; 409 when to is taken, when a layout file for to exists without its graph, or when a name exists in both file forms; 400 when the file is not readable JSON or, in a project directory, when to ends in .graph or .layout. Backs Rename in the sidebar's Graphs tab (Tabs & Persistence). |
/api/graph/{name} | DELETE | token | Delete a saved graph, with its layout file in a project directory. Returns {message, removed}, the list of deleted paths. 404 when nothing is there; 409 when the name exists in both file forms. Backs Delete in the same tab. |
/api/graph/export | POST | token | Export a single-file headless Python runner. It embeds the graph and requires a compatible CodefyUI backend environment, but no running server. The response is {script, warnings}, where warnings lists the file params whose absolute path ties the script to this computer. |
/api/graph/contract/{name} | GET | open | The saved graph's derived function signature — inputs, outputs, problems — for scripting against it. See Graph as a Function. |
/api/graph/run/{name} | POST | token | Run the latest saved file as a function: {inputs, timeout_s, device, record_outputs} in, the 9-key envelope out on every outcome. See Graph as a Function. |
/api/examples/list | GET | open | List the built-in example graphs and those of enabled plugins: [{name, description, category, path, source, node_count, edge_count, section, family, order}]. source is builtin or plugin:<id>, node_count leaves out notes, and section, family and order place the example in the gallery (see Examples Gallery, Adding an example). |
/api/examples/load | GET | open | Load an example graph; ?path= is a list row's path. |
Runs and sweeps
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/runs | POST | token | Submit a run to the queue and return immediately: {run_id, status: "running" | "queued"}. options.target ({worker, devices}, plus pool, memory_gib, memory_source and wait) names where it executes: local, a connected worker, or any (Run Queue, Workers). A worker name this server has not seen since it started, a worker that does not offer the device, or a degraded worker that lacks a node type the graph uses (the message names the types) is 400; a known worker that is away queues the run until it reconnects. |
/api/runs | GET | open | Newest-first list; ?status= repeats, ?limit= has a maximum of 500, and ?offset= selects the starting row. Every row includes queue_position, queue_reason (why a queued run is not starting beyond its place in line, such as waiting for worker gpu-box to reconnect, else null), queue_reason_code and queue_reason_params (the same reason as a code and its parameters for a client to word in its own language: worker_reconnect, worker_draining, worker_no_mount, any_no_worker, any_no_worker_mount, any_unavailable, and for memory admission and one-shot workers memory_wait, memory_no_budget, one_shot_start, one_shot_taken, one_shot_run_id, one_shot_pool, budget_raised (a raised estimate beside another reason is that reason's params.raised); null with no reason), active, final_metrics, fingerprint_digest, execution_facts, worker_id (local, a worker's name, or any until an any run is placed), devices (the requested device set or null; for a run on a worker, the devices it got), metric_producers ({name: [node ids]}, null for a run-level series), and worker_status (for an active run on a worker, {worker_id, state, disconnected_at, resume_deadline} with state connected or disconnected and Unix-second times while disconnected; else null); GET /api/runs/{id} also has late_result ({status, error, worker_id, at} when a worker reported the run's end after it had ended here, else null); the response also includes the unpaged total. |
/api/runs/{run_id} | GET | open | Return one run (the same fields as a list row, including worker_id and devices) and last_cursor, which clients use to start event polling, plus fingerprint: the environment document its fingerprint_digest names (null before the run starts), and provenance: the run's node provenance records in the order they were recorded ([] for a run without the provenance option; see Node provenance). |
/api/workers | GET | open | Every enrolled worker agent and every other agent this server has seen since it started: {workers: [{id, name, status, draining, same_host, version, fingerprint_digest, platform, mounts, devices[{value, kind, index, name, memory, slots, shared_with}], running, connected_at, last_heartbeat_at, enrolled, credential, plain_http, node_catalog, degraded_reasons}], local, rejections}. status is online, degraded (a heartbeat is late, or its node catalog differs from this server's; degraded_reasons lists heartbeat and/or node_catalog), draining (connected, takes no new runs; draining is also a boolean of its own), disconnected, or offline (enrolled, not connected since the server started; connected_at is null, and last_heartbeat_at is null for one never seen); enrolled says the worker holds a revocable credential and credential how its connection authenticated (worker_token, session_token, or null); same_host is true for an agent on this machine; plain_http is true when its connection is plain ws:// from another machine (the "Plain HTTP" badge: not wss as uvicorn reports the scheme, and not a loopback peer such as a same-host agent or an SSH tunnel's local end; see Remote Workers, Trust); memory is {total_bytes, free_bytes, source} from the latest heartbeat, or null; slots is {limit, taken} for that (worker, device) slot, or null; shared_with is "local" when the device is physically one of this server's, whose slot it then shares (count it once), else null. local lists this server's own devices as {id: "local", devices[{value, kind, slots}]}, or is null without a run service. mounts maps each storage label the worker declared to its path there; a worker on this machine also shows this server's label with this server's path, and local.mounts is this server's own label and path (Storage: shared filesystem). fingerprint_digest is the worker's environment fingerprint digest, resolvable with GET /api/system/fingerprints/{digest} (null when its last connection reported none). node_catalog is {status, hash, missing, extra}: match, mismatch (with the node types this server has and the worker lacks, and the other way round) or unknown (the worker reported none) (Remote Workers, Keeping workers in sync). Each worker also has ssh_enabled (whether this server's SSH worker of that name is enabled; null when it is not one), memory_budget (its agent enforces memory budgets), one_shot ({run_id, took} for a one-shot agent, else null) and instance_of / instance (an instance of a shared credential, else null); each of its devices also has policy (exclusive or shared), admission (a shared card's ledger, else null), same_card (the other offerers of the same physical device, [{worker, device, policy, online, memory_budget}]) and, in slots, limited_by ({worker, device}) when another offerer's limit applies. rejections is each worker name's last refused handshake from an agent that did not get in, newest first, [{name, reason, at, peer}] (at in epoch seconds, peer the client address; kept in memory, cleared when that name connects); a refused attempt to take the name of a connected worker is listed there and never on that worker's card. 503 when the worker registry is not available. |
/api/workers/{id}/drain | POST | token | Stop placing new runs on the worker; runs it is executing continue. Returns the worker entry; 404 for an unknown worker. |
/api/workers/{id}/undrain | POST | token | Place runs on the worker again. Returns the worker entry; 404 for an unknown worker. |
/api/workers/{id} | DELETE | token | Revoke the worker's credential, disconnect it (its running runs end interrupted) and forget it; the runs still queued for it fail at once with worker <id> was removed. Returns {id, revoked, failed_runs}. Its agent is refused from then on. 404 for an unknown worker. |
/api/workers/enrollments | POST | token | A new enrollment token: body {ttl_s?, uses?, label?, worker?, control_url?} (ttl_s 60 to 604800, default 86400; uses 1 to 100, default 1). worker makes a re-key token bound to that known worker (404 for an unknown one): the only kind of token that can enroll a name already taken, and it can enroll that name only. Returns {id, token, expires_at, uses_left, state, label, worker, created_at, control_url, command}; command is cdui agent start --control <control_url> --token <token> (plus --name <worker> for a re-key token, and --ca-fingerprint <tls_fingerprint> over https when the server knows the fingerprint), where control_url is the body's (an http(s)://host[:port][/path] address without shell characters, else 422) or this request's scheme and Host. The token is in this response only. |
/api/workers/enrollments | GET | token | The enrollment tokens, newest first, without their secrets: {enrollments: [{id, label, worker, created_at, expires_at, uses_left, state}], control_url, other_machine_urls, listening_on_loopback, tls_fingerprint, fingerprint_command}. state is pending, expired or used; expired and used tokens stay listed until deleted. other_machine_urls are the allowlisted addresses another machine could dial. tls_fingerprint is sha256:<hex> of the public key of the first certificate in CODEFYUI_TLS_CERT_FILE, or null when that is unset or unreadable; fingerprint_command is the openssl pipeline that computes the same value on the TLS proxy's host. |
/api/workers/enrollments/{id} | DELETE | token | Revoke (or clear away) an enrollment token. 404 for an unknown id. |
/api/workers/agent-command | POST | token | The agent command for an enrollment token at another address, built by the same function as the command above: body {control_url, token, worker?}, returns {control_url, command}. The token is in the body only (never a URL), and is neither checked against the store nor kept. 422 for a malformed address, token or worker name. |
/api/workers/enroll | POST | enrollment token | {token, name, platform?, devices?, mounts?} -> {worker_id, worker_token}. Spends one use of the token and creates the worker. A name that is already enrolled, or held by a connected same-host agent, is 409 ("worker X is already enrolled; remove it or create a re-key token for it") unless the token is a re-key token for it, which gives it a new credential (the old one stops working and its connection closes). 401 for an unknown, expired or used-up token, with the reason; 403 for a re-key token used for another name; 400 for a bad or reserved name (local, any, enroll, enrollments); 429 when rate-limited. A refused enrollment spends no use. Only POST on this path skips the session token. Through an SSH worker's tunnel the rate limit is kept per SSH worker rather than per client address. |
/api/workers/ssh | GET | token | The SSH workers this server starts itself (Remote Workers, Connecting over SSH): {ssh_workers: [{id, target, port, identity, remote_cdui, remote_port, enabled, state, last_error, exit_code, tail, attempts, retry_at, up_since, gave_up, pid, remote_pid}], ssh_available, port_range, default_remote_cdui}. state is connecting (ssh runs, the agent has not connected), up, failed (ssh exited; last_error holds its last lines, retry_at the next attempt) or stopped (disabled). remote_port is the worker's slot from CODEFYUI_SSH_REMOTE_PORT_RANGE: its state directory on the remote host is ~/.codefyui-ssh/<id>-<slot>/ (<id>: this server's instance id), where the tunnel's Unix socket is. ssh_available says whether this machine has an ssh on PATH. A GET that needs the session token. |
/api/workers/ssh | POST | token | Add an SSH worker and start it: {target, port?, identity?, remote_cdui?, name?}. target is user@host, host or an ssh Host alias; identity a key file on this machine (it must exist); remote_cdui the launcher on the remote host (default ~/CodefyUI/cdui); name defaults to the target's host name. 400 for a value that cannot be used or when ssh is missing, 409 for a name an SSH worker or another worker already has. Returns the entry. |
/api/workers/ssh/{id}/enable | POST | token | Undrain the worker and start its ssh session again; on an enabled entry that gave up (gave_up: true, forwarding refused), retry it. 404 for an unknown entry. |
/api/workers/ssh/{id}/disable | POST | token | Drain the worker, then end its ssh session: the remote agent stops and a run still on it ends interrupted. 404 for an unknown entry. |
/api/workers/ssh/{id} | DELETE | token | Stop the SSH worker and delete its entry; its enrollment stays (DELETE /api/workers/{id} revokes it, and removes the SSH entry too). |
/api/runs/{run_id} | DELETE | token | Drop a finished run with its events, metrics, artifact rows and captured outputs; 409 while it is queued or running. Artifact files stay on disk, except what a worker uploaded for the run: its folder here, and its objects in the S3 bucket but output/ (An S3 or MinIO bucket); when those objects cannot be removed, the answer carries a warning saying why. |
/api/runs/{run_id}/cancel | POST | token | Cooperative stop — {run_id, status, cancelled}; cancelled: false (still 200) for a run that had already finished. |
/api/runs/{run_id}/events | GET | open | Events after ?cursor=; ?wait= long-polls for up to 60 s, and ?limit= accepts up to 2000. Returns {run_id, status, active, events[{cursor, type, payload, ts}], cursor}. A page can contain fewer than limit events; use the returned cursor for the next request. |
/api/runs/{run_id}/metrics | GET | open | Recorded scalar series in (name, step) order; ?name= filters, ?format=csv downloads (UTF-8 BOM, formula-safe cells). The JSON form also lists names. |
/api/runs/{run_id}/artifacts | GET | open | Files the run recorded (checkpoints, exports, images), oldest first; ?kind= filters, and an unknown kind is an empty list. Each entry is {id, kind, path, meta, created_at, backend, mount_label, relative_path, worker_id, bucket}: path is where the file is on this server, or null for a mount row whose mount this server does not hold (then only mount_label and relative_path name it); backend is local (an absolute path, every row before mounts existed) mount (a file a worker wrote on a shared mount, stored as relative_path under mount_label and translated to this server's path) transfer (a file a worker without the mount uploaded here, stored at relative_path under this server's data folder) or s3 (a file a worker without the mount uploaded to the S3 bucket, path being s3://<bucket>/<key>, relative_path the object key and bucket the bucket); worker_id is the worker whose run wrote it (local: this server); bucket is null on every row but s3, and mount_label on every row but mount. |
/api/runs/{run_id}/artifacts/upload | POST | worker token | A worker's output for a run executing on it (Storage: no shared filesystem). Multipart, form fields first: kind, name (a relative path), place (run, the default, or data) and optional sha256 (checked), then file; the body is streamed to disk. run stores at <data folder>/runs/<run_id>/<kind>/<name>; data (a writer node's file, name under models/, media/ or output/) stores at that relative path in this server's data folders, never over a different file (the newer one gets a -1, -2 ... suffix). The same content again answers 200 with the same key. Answers {path, bytes, sha256}, path being the key the worker then reports. The session token may upload for any worker run; a worker token only for a run on that worker (409 otherwise, and when the run is not executing on a worker); 403 without either. Capped by CODEFYUI_MAX_UPLOAD_SIZE (413). |
/api/runs/{run_id}/artifacts/{artifact_id}/download | GET | open | The file of a transfer row (in the run's folder, or a writer's file in the data folders), as an attachment. The object of an s3 row (An S3 or MinIO bucket), streamed from the bucket, or with CODEFYUI_S3_DOWNLOAD=presign a 307 to a pre-signed URL; 503 when the bucket cannot be reached. 400 for any other row (its file is at its path) and for a directory (a TensorBoard folder); 404 when the row or its file is gone. |
/api/sweeps | POST | token | Compile a parameter sweep into variant runs and queue them all — 201 with the sweep_id and one run_id per variant. |
/api/sweeps/{sweep_id} | GET | open | The ranked comparison table — variants best first, best, counts, and objective_warning when there is one; ?format=csv downloads it. |
/api/sweeps/{sweep_id}/cancel | POST | token | Cancel every queued or running variant: {sweep_id, state, cancelled, already_finished, variants[]}, one entry per variant in index order. |
/api/studies | POST | token | Submit a study: variants x repetitions of queued child runs on one exact objective. 201 with the study; with an Idempotency-Key header (or idempotency_key), a retry of the same request is 200 with the same study and creates nothing, and the key reused for a different request is 409. |
/api/studies | GET | open | Newest-first list with per-status counts of the current attempts; ?status= takes one status or several separated by |, plus ?limit= (up to 500) and ?offset=. |
/api/studies/{study_id} | GET | open | The study's status, revision, owner (the plugin that submitted it, or null), halt (the budget that halted it, or null), budget_usage (what each budget has used and which one ran out), every attempt with its run_id, stop_reason and copied evidence, per-variant aggregate (count, mean, std, values), eligibility, rankings with exact ties marked, and last_cursor. |
/api/studies/{study_id}/events | GET | open | The study's audit log after ?cursor=; ?wait= long-polls up to 60 s while the study is active. Returns {study_id, status, revision, events[{cursor, type, payload, ts}], cursor}. |
/api/studies/{study_id}/cancel | POST | token | Stop scheduling and cancel the active children; completed attempts are kept. Optional body {expected_revision}. 409 for a succeeded study, and for an expected_revision the study has moved past. |
/api/studies/{study_id}/resume | POST | token | Continue an interrupted, cancelled, failed or halted study: completed repetitions are skipped, a cancelled, interrupted or lost one gets a new attempt. Optional body {budget, expected_revision}: budget replaces the study's budget. 409 for any other status, for a budget that is already used up, and for a stale expected_revision. |
/api/studies/{study_id}/variants/{index}/graph | GET | open | The graph one variant ran, as stored (SECRET values blank): {study_id, variant_index, label, graph}. |
Runs API. POST /api/runs accepts {"graph": {...}, "options": {...}, "name": "..."}. The graph uses the saved-graph JSON format (nodes, edges, and optional presets, subgraphs and settings). The endpoint returns 422 when the body is not JSON or has no graph object, 400 for an invalid graph (for example empty nodes or a bad settings.device) or option, and 503 when the run service is unavailable or an interactive-lane submission exceeds the limit. Option keys are a closed set: device, seed, deterministic, record_outputs, lane, the canvas flags verbose, graph_id, weights_persistent, backward_mode, auto_backward, the engine error policy error_mode, max_retries, target, and provenance ("identity" or "content", see Node provenance; "off" or no key records nothing and stores no key). The run's device is the first entry of options.target.devices when given, else options.device when given, else the graph's settings.device (see the graph settings object), else cpu. target is {"worker", "devices"}: worker is local (the default), the name of a connected worker agent, or any, and devices is a set of concrete devices of one kind. pool: "one-shot" places the run on a one-shot worker (Remote Workers, One-shot workers); memory_gib, memory_source and wait apply to a worker card shared by memory, and memory_gib is 400 with local (Multi-GPU, Sharing a card). A worker name that has not connected since the server started, a worker that does not offer the devices, an any run that neither this server nor a worker it has seen could serve, a worker target combined with the interactive lane, and record_outputs on a named worker whose agent does not support it are 400; a known worker that is away queues the run until it reconnects. On this server a set run holds a slot on every device and executes on the first; on a worker it uses them all (see Run Queue, Workers and Execution Architecture). A run's status is one of queued, running, succeeded, failed, cancelled, or interrupted. Two limits apply to /events: a payload over CODEFYUI_RUN_EVENT_PAYLOAD_CAP_BYTES (128 KB) is stored with its outputs replaced by an elision marker, and a response ends once it exceeds CODEFYUI_RUN_EVENTS_RESPONSE_CAP_BYTES (4 MB). See Run Queue for queue order, lanes, retention, and cdui run.
Sweeps. POST /api/sweeps accepts base_graph, a sweep_spec (method grid or random, seed, samples, and params[{node_id, param, values | range}]), a required objective (metric, direction minimize or maximize, and an optional node_id naming the node whose series ranks the sweep), the same options, a name, and seed_variants. It creates at most CODEFYUI_MAX_SWEEP_RUNS (32) variants. Each variant is an ordinary /api/runs row whose /events endpoint can be followed separately. See Run Queue — Sweeps for the specification, validation errors, and cancellation behavior.
Studies. POST /api/studies accepts name, a required objective (node_id, metric and direction, all three required), base_graph, variants[{label, graph | ops[{node_id, param, value}]}], repetitions (1 to 32), seed, options (as for a sweep, without seed or the interactive lane), budget (max_runs, max_parallel_runs, max_run_seconds, max_active_seconds), resume_policy (explicit or auto) and idempotency_key (a key starting with plugin: is refused: that prefix scopes plugin jobs). A study's status is one of queued, running, cancel_requested, succeeded, failed, cancelled, interrupted or halted (a budget ran out); an attempt's is pending, submitting, queued, running, succeeded, failed, cancelled, interrupted, lost or timed_out (stopped for max_run_seconds). The budgets are enforced by the server; see Studies, Budgets. Only variants whose scheduled repetitions all succeeded and recorded the objective are ranked. A sweep table ranks a failed run on the value it logged; in a study, that run makes its variant ineligible. Each child is an ordinary /api/runs row carrying study_id and study_attempt_id. See Run Queue — Studies for seeds, ranking, cancel, resume and the restart policy.
Run environment
A run records two things about how it was submitted, and since #344 two about where it executed.
- At submission:
options(device, seed, determinism and the rest, after the graph'ssettingsfill in what the request left out), andgit_commit,git_dirtyandplugin_pins(the user's project commit and the enabled plugins in the lockfile). These describe the request and the project, not the CodefyUI code that runs it. - At execution start:
fingerprint_digestandexecution_facts, written when the run leaves the queue and begins. A queued run that waited through a server update therefore records the environment that actually executed it, while its submission fields still say what they said. For a run on a worker,fingerprint_digestdescribes the worker. When the run starts it is the fingerprint the worker's agent reported when it connected (#781). The run subprocess then reports its own, collected after it loaded the code, plugins and custom nodes the run executes with, and that one replaces it;execution_facts.fingerprint_sourcesays which of the two the digest is. The subprocess takes the device facts (accelerators,devices,CUDA_VISIBLE_DEVICESandHIP_VISIBLE_DEVICES) from the agent's document, because the agent narrows each run to its own cards, so on a machine where nothing changed since the agent started the two digests are equal.execution_facts.worker_idnames the worker, and the run's own cards are inexecution_facts.visible_devicesanddevice_aliases.
GET /api/system/fingerprint returns the fingerprint document for the server as it is now, with schema_version 2. Every key is present in every document, and a fact that cannot be read is null:
| Key | Contents |
|---|---|
codefyui | version, commit, dirty and commit_source: git when the server runs from a checkout, build-info when only frontend/dist/build-info.json names a commit, else null. Read once per server process, because the code that runs is the code that was loaded at startup. |
python, platform | Python version and implementation; operating system, kernel release, machine and macOS version. |
libraries, torch_build | Installed torch, torchvision, numpy and mlx versions; torch's build commit, CUDA, HIP and cuDNN versions. |
accelerators, devices | CUDA, ROCm, MPS and MLX availability, each CUDA card's name and compute capability, the NVIDIA driver version when pynvml can read it; the server's default device and the devices it offers. |
plugins | Each enabled pack: source_kind, source, ref, version, revision, revision_kind, immutable and content_sha256. A pack installed from a repository has its commit (git-sha, immutable: true). A builtin pack ships inside the checkout, so its revision is the CodefyUI commit (codefyui-commit) and its source is plugins/<id>; it is immutable only while the checkout is clean (codefyui.dirty is false). A linked local pack has no immutable revision: its revision is the digest of its files when the fingerprint was collected (content-sha256, immutable: false), which identifies the files but cannot recover them, and its path is withheld. content_sha256 is that digest for every pack whose directory can be read (the sha256 of each file's bytes over the sorted relative paths, leaving out directories named .git, .hg, .svn, __pycache__, node_modules, .mypy_cache, .pytest_cache, .ruff_cache, .ipynb_checkpoints, .venv, venv, env, .tox, .nox, build, dist and *.egg-info, and files named .DS_Store, Thumbs.db, *.pyc and *.pyo, which are version control, caches, virtual environments, build output and file-manager metadata; a pack directory inside a git checkout is listed by git ls-files instead, so its tracked files and the untracked files its .gitignore does not exclude count, and it is walked when git is not installed, the directory is not in a checkout, or git takes longer than 10 seconds), so a pack edited in place after it was installed changes the digest; it is null when the directory cannot be read. |
custom_nodes, optional_packages | The sha256 of each enabled custom node file; the installed version of each Package Center pip distribution (null when not installed). |
env | The values of a fixed list of variables that change numerics or device selection, such as CUBLAS_WORKSPACE_CONFIG, CUDA_VISIBLE_DEVICES and ONEDNN_MAX_CPU_ISA. No other environment variable is read. |
The document carries no hostname, username, home-directory path or secret. The digest is the sha256 of the document's canonical JSON (sorted keys, no whitespace, UTF-8) and covers every key, so the same environment gives the same digest and a change to any recorded fact gives a different one. Each distinct document is stored once; retention and DELETE /api/runs/{run_id} leave it in place, so /api/system/fingerprints/{digest} still resolves a digest after its runs are gone.
A stored document keeps the schema_version it was written under. In version 1 (CodefyUI 3.1) a plugins entry has no immutable or content_sha256, and a linked local pack's revision is null; version 2 adds both keys and the content revision. A version 1 and a version 2 document never share a digest, even for the same machine.
execution_facts sets the request next to what the run got. requested holds device, seed and deterministic from the stored options, plus graph_settings_device and graph_settings_seed from the graph. resolved holds the device the scheduler resolved (auto becomes, for example, mps:0), the seed and determinism flag handed to the engine, and numerics: torch's default dtype, float32 matmul precision, TF32 and cuDNN benchmark switches, and thread count when the run started. There is no run-level precision option; mixed precision is a parameter of the training nodes and is recorded in the graph snapshot. A node with its own device parameter can override the run's device for that node. fingerprint_source says which process fingerprint_digest describes: process (this server, which executed the run), worker-hello (the worker's agent as it connected, while the run subprocess has not reported, or when it could not), worker-run (the worker's run subprocess, which also fills resolved.numerics), or null when no fingerprint was recorded. A run on a worker has no resolved.numerics until its subprocess reports. execution_facts has schema_version 3; rows written before it have no fingerprint_source.
When a run that executed finishes, execution_facts also gets peak_memory, whatever the outcome. A CUDA run records one entry per card the server process holds memory on, the run's own card first, so a node pinned to another card is counted too: {"cuda:0": {"allocated_bytes": N, "reserved_bytes": M, "exact": false}}, the allocator's high-water marks (max_memory_allocated, max_memory_reserved) over the run. An MPS run records {"mps": {"held_bytes": N, "exact": false}}: what the Metal driver holds for the server process when the run ends, its cache included. MPS has no peak counter, so this is not a peak. A CPU run is measured the same way, so a node that pinned a CUDA card puts that card in the map. A run with no card in use (a plain CPU run), or in a server where torch is not loaded or CUDA was never initialised, records null. Reserved memory with no allocation during the run is not counted on a card whose counters the run reset, because there it is cache an earlier run left behind. A run that stopped before it executed, or whose executor failed without reporting, records nothing (the key is absent). exact is false for every run that executes inside the server process, because those runs share one CUDA context. A run on a worker executes in a child process that holds that run alone, so its CUDA entries are exact: true and never shared; the MPS figure stays exact: false everywhere because it includes the driver's cache. The peak counters belong to the process, so a run resets them at its start only when no other in-process run is on that card. Otherwise it leaves them alone, so it does not wipe the other run's peak, and marks the card "shared": true: the number then also covers the other run. The Runs panel's detail header shows the largest CUDA value as "Peak memory", or the MPS value as "Memory held at end", with every device listed on hover.
A matching digest and seed do not guarantee identical numbers. Nondeterministic kernels and thread scheduling can still change results, and the same seed on different hardware or library versions can produce different values. The digest tells you whether the two environments are the same.
Node provenance
The fingerprint says which code executed a run; it does not say which data or weights the graph read. With the run option provenance, the nodes that read a dataset or load a model record that too, one record per node (#344). The Queue on... dialog offers the option as Data and model provenance.
"identity": where the files are, how many, how large, when they were last modified, and a stat identity: each file's path, size and modification time hashed together (content.method: "stat"). No file content is read, so it costs about as much as listing the files."content": the same, with a content hash (content.method: "sha256"): the sha256 of every file's bytes, combined over the sorted relative paths. The digest of a single file is the valuesha256sumprints. Hashing is incremental: each file's hash is kept in<user data>/cache/content-hashes/under its path, size, modification time and inode, and a file whose values are unchanged is not read again, so a large dataset is read in full on the first run only. The cache never sits inside the dataset, and losing it only means the files are read again. The same rule means a file rewritten in place with the same size, modification time and inode keeps its earlier hash.
A stat identity is not a content hash: an edit that keeps the size and lands within one timestamp tick, or a copy that resets modification times, can make it disagree with the content. Compare content.digest values only between records with the same content.method.
Both identities walk the same files. A walk follows directory symlinks, so a dataset folder whose class folders link to another disk is identified by the linked files; a link back into a directory the walk is already inside (a loop) is not followed, and a note says so. A walk skips only version control directories (.git, .hg, .svn), __pycache__, tool caches (node_modules, .mypy_cache, .pytest_cache, .ruff_cache, .ipynb_checkpoints) and *.pyc / *.pyo files; every other file is hashed as the node's loader reads it, so a class folder named build or env is part of the dataset. The longer list of generated content applies to pack directories only (see plugins under Run environment).
Without the option nothing is recorded, the stored options have no provenance key, and the run's events and results do not change. The records are collected after each node finishes, from its parameters and outputs, without changing what the node does. On a worker the run subprocess collects them and the control plane stores them.
The node types that record provenance:
| Node | kind | What is recorded |
|---|---|---|
CSVReader | dataset | The file; rows is the number of rows in the node's output tensor. |
Dataset | dataset | The torchvision dataset name and split, and the dataset's own directory under data_dir (MNIST/, cifar-10-batches-py/); rows is the dataset's length. torchvision datasets carry no revision. |
ImageFolderDataset | dataset | The split's folder; rows is the number of images. |
HuggingFaceDataset | dataset | The repository id, subset and split. revision.resolved is the commit of the dataset repository the cached files came from (hf-commit, read from the datasets cache path, else from the local hub cache's refs/main), the content identity covers the Arrow cache files, and rows is the dataset's length. The node pins no revision, so revision.requested is null (the default branch). |
KaggleDataset | dataset | The dataset slug. revision.resolved is the version kagglehub downloaded (kaggle-version), revision.requested the version the slug pins, if any; rows is the number of images. |
ModelLoader, CheckpointLoader | model | The weights file. A local checkpoint has no revision, so its content identity is its identity. |
HFTextGenerate | model | The model repository. revision.requested is the revision the Package Center catalog pins, revision.resolved the commit of the downloaded snapshot (hf-commit), and the content identity covers the snapshot's files. |
Every record (schema_version 1) has every key below; a fact that is not known is null, and notes says why when the reason is not obvious:
{
"schema_version": 1, "node_id": "csv", "node_type": "CSVReader",
"kind": "dataset", "mode": "content",
"source": {"type": "file", "name": null, "location": "files",
"path": "iris.csv", "subset": null},
"split": null,
"revision": {"requested": null, "resolved": null, "kind": null},
"content": {"method": "sha256", "digest": "e036f888...", "exists": true,
"files": 1, "bytes": 4551, "latest_mtime": "2026-10-21T11:46:52Z",
"truncated": false, "unreadable": 0, "reused": 1, "hashed": 0},
"rows": 150, "notes": ["rows counts the rows the node kept"]
}
source.path is never absolute. It is relative to the CodefyUI directory the file was found under, which source.location names (models, files, data, project, working-dir, hf-cache, packs); a file outside all of them is recorded by its name only, with location: "external". A filesystem root (/, C:\) or the home directory never counts as one of those directories, so a server started from / (a systemd unit's default working directory, a container's WORKDIR /) also records an outside file as external. content is null when the node named no file. content.digest is null when any input is missing, a file or directory is unreadable (unreadable counts them), or the walk stopped at 200,000 files (truncated); notes names what was missing, unreadable or not followed. A provider that a plugin registers gets the same shape: the keys it leaves out are null. reused and hashed (content mode only) count the files whose hash came from the cache and the files read for this run. GET /api/runs/{run_id} returns the records as provenance, the Runs panel shows them in the run detail under Data and models, and they are deleted with the run.
Execution outputs and state
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/execution/outputs/{run_id} | GET | open | List ports captured for a run. |
/api/execution/outputs/{run_id} | DELETE | token | Clear a captured run. |
/api/execution/outputs/{run_id}/{node_id}/{port} | GET | open | Fetch a captured tensor (supports ?slice=0,:,: and ?max_elements=, default 4096, at most 1,000,000); 413 when the slice is still larger. ?preview=true (not combined with slice) returns a tensor larger than max_elements as a leading slice the server picks from its shape, with slice, sliced_shape and truncated: true in the response; a tensor within the limit comes back whole. 204 when the node ran and this port produced no value (None); 404 when the run, or the port, has nothing recorded. |
/api/execution/outputs/{run_id}/{node_id}/{port}/stats | GET | open | Server-side summary statistics for one captured port: a fixed set of scalars and a 64-bin histogram, or value counts for label tensors. The response is typically one or two kilobytes regardless of tensor size. Tensors above CODEFYUI_STATS_SAMPLE_THRESHOLD (4,000,000 elements) are sampled. 204 when the node ran and this port produced no value (None); 404 when the run, or the port, has nothing recorded. |
/api/execution/outputs/{run_id}/{node_id}/__steps_index | GET | open | Step-trace metadata for a node (Inspector → Steps tab). |
/api/execution/outputs/{run_id}/{node_id}/__grad_index | GET | open | Captured gradient metadata (Inspector → Backward tab). With ?inner=true, either index lists what the nodes inside a block or preset card recorded, each entry naming its node in node_id. |
/api/execution/outputs/{run_id}/value?node_id=&port= | GET | open | Same as the {node_id}/{port} path form, for a node id the path cannot carry: one inside a block (<instance>/<node>). Takes the same slice, max_elements and preview. |
/api/execution/outputs/{run_id}/stats?node_id=&port= | GET | open | Same as the /stats path form, for such an id. |
/api/execution/outputs/{run_id}/steps?node_id= | GET | open | Same as __steps_index, for such an id. |
/api/execution/outputs/{run_id}/grads?node_id= | GET | open | Same as __grad_index, for such an id. |
/api/execution/state/reset | POST | token | Reset persisted layer weights (per-node or per-graph). |
/api/execution/state/list | GET | open | List how many modules are persisted (diagnostic). |
Published apps and API keys
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/apps | GET | token | List apps — slug, graph_name, active_version, versions_count, record_io. |
/api/apps/{slug}/publish | POST | token | Freeze the named saved graph as the slug's next version and activate it ("create": true for a new slug); runs the /run pre-flight first. |
/api/apps/{slug}/versions | GET | token | Every version with its note, provenance (git_commit, git_dirty) and active flag. |
/api/apps/{slug}/activate | POST | token | Point the slug at any existing version ({"version": n}) — also the rollback path. |
/api/apps/{slug}/unpublish | POST | token | active_version = null; versions and runs are kept. |
/api/apps/{slug} | PATCH | token | Flip record_io without republishing. |
/api/apps/{slug} | DELETE | token | Remove the app, all its versions and all its run records — there is no undo. |
/api/apps/{slug}/invoke | POST | API key | Execute the active version: the same body and envelope as /api/graph/run/{name}, with app and version filled in. |
/api/apps/{slug}/openapi.json | GET | key-or-token | A standalone OpenAPI 3.1 document for the active version. |
/api/apps/{slug}/runs | GET | key-or-token | Newest-first run records, metadata only; page with ?before= and ?before_id=. |
/api/apps/{slug}/runs/{run_id} | GET | key-or-token | One record with its inputs, outputs and node timings. |
/api/keys | POST | token | Mint a key ({"name"}); the full cdui_... token appears in this response only. |
/api/keys | GET | token | List keys — id, name, prefix, timestamps, never secrets; revoked rows stay listed. |
/api/keys/{key_id}/revoke | POST | token | Soft revoke; 404 key_not_found. |
The lifecycle, run records, pagination and the per-app OpenAPI document are on Publish.
Custom nodes and plugins
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/custom-nodes | GET | open | List custom nodes. |
/api/custom-nodes/upload | POST | token | Upload a custom node. |
/api/custom-nodes/toggle | POST | token | Enable/disable a custom node. |
/api/custom-nodes/{filename} | DELETE | token | Delete a custom node. |
/api/plugins | GET | open | List installed plugin packs. |
/api/plugins/catalog | GET | open | List catalog and installed plugins. Each row includes its state: installed, disabled, explicitly removed, or missing files. |
/api/plugins/generation | GET | open | Return the reload counter, which every rediscovery of nodes and plugins increments. The editor polls it only when an enabled linked plugin was installed at page load, and re-activates plugin frontends when it changes; see Plugins. |
/api/plugins/{id} | GET | open | Get a plugin's manifest + README. |
/api/plugins/reload | POST | token | Same as POST /api/nodes/reload. |
/api/plugins/inspect | POST | token+loopback | Inspect a catalog name, owner/repo, or URL at one resolved commit. Returns the installation requirements under an inspection_id without installing the plugin. |
/api/plugins/install | POST | token+loopback | Install the manifest identified by inspection_id; returns 202 with a job_id. The server uses the inspected manifest rather than accepting installation metadata in this request. |
/api/plugins/jobs/{job_id}/events | GET | open | Return an install job's log and progress after ?cursor=. ?wait= long-polls for up to 60 s. A job can end with needs_restart and include the command to run after stopping the server. |
/api/plugins/jobs/{job_id}/cancel | POST | token+loopback | Cancel a running install and remove partial writes. |
/api/plugins/{id}/update | POST | token+loopback | Check the plugin's GitHub repository for an update. Returns 202 {"job_id"}, 200 {"status": "up_to_date", "sha"}, or 200 {"status": "needs_consent", "inspection", "capabilities_added", "allowed_modules_added"} when the update requires additional permission. Complete a consent-required update with POST /api/plugins/install {"inspection_id", "accept_capabilities", "trust_author"} and no force. Updates preserve the enabled state. Built-in and linked plugins, and repositories whose manifest now declares another plugin id, return 400 not_updatable. |
/api/plugins/{id} | DELETE | token+loopback | Uninstall a plugin. A built-in plugin keeps its files and is recorded as removed; a linked directory remains unchanged. Python packages remain installed; the response names the ones nothing else still needs, with the command that removes them. |
/api/plugins/{id}/enable | POST | token | Turn an installed plugin on and re-discover. |
/api/plugins/{id}/disable | POST | token | Turn it off without uninstalling it. |
/plugins/{id}/frontend/{path} | GET | open | Serve a file from an enabled plugin's frontend/ directory when its manifest declares [frontend]; otherwise return 404. The route reads the lockfile on every request, so install, enable, disable, and uninstall changes apply without a restart. Cache-Control: no-cache requires browser revalidation after updates. |
/plugins/{id}/assets/{path} | GET, HEAD | open | Serve a file from an enabled plugin's assets/ directory with its detected media type, or application/octet-stream when unknown. The route uses the same per-request lockfile check and revalidation as the frontend route. Directory requests and paths outside the plugin are rejected. |
:::note Installing a plugin needs a loopback-bound server
inspect, install, job cancel, update, and DELETE require the loopback gate shown above. These operations can download, install, or remove third-party code, and inspection contacts GitHub using a caller-supplied source. reload, enable, and disable operate on code already present on the server, so they require only the token. Refusals from these routes use a machine-readable code such as busy, already_installed, or consent_required; see the table under Plugin Center. Job events carry an English step label and English failure message and hint text. Clients should key on the step id (resolve, download, extract, verify, deps, stage, lock, reload): the editor translates those ids and shows the English label only for an id it does not know. Failure messages stay in English.
:::
Plugin jobs
A plugin can hand durable work to the server under its own id, following the Graph Copilot plugin's backend agent contract (#343). A job of kind study (spec_version 1) is a study whose owner is the plugin: its spec is a POST /api/studies body, and the routes below map one-to-one onto /api/studies. Every job also appears in GET /api/studies and in the Studies panel. Plugin-supplied server code (AgentJobHandler) is not loaded; study is the only kind.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/plugins/{plugin_id}/jobs/kinds | GET | open | {plugin_id, api_version, kinds[{kind, spec_version, description}]}. A server without plugin jobs answers 404, so a plugin can feature-check here and keep its in-page engine. |
/api/plugins/{plugin_id}/jobs | POST | token | Submit {kind, spec_version, spec, idempotency_key?}. An idempotency key is required (the Idempotency-Key header, the body's idempotency_key or the spec's; they must agree) and is scoped to the plugin. 202 with the job (the study detail plus job_id, plugin_id, kind, spec_version, api_version, created: true and request_digest); a retry with the key is 200 with the same job; the key reused for another request is 409. 404 for a plugin that is not installed, 409 for a disabled one, 400 for an unknown kind or spec_version, 422 for a spec that does not fit the study schema. |
/api/plugins/{plugin_id}/jobs | GET | open | The plugin's jobs, newest first, with counts; ?status=, ?limit= and ?offset= as for studies. |
/api/plugins/{plugin_id}/jobs/{job_id} | GET | open | One job, as GET /api/studies/{id} returns it. A job of another plugin, or a study submitted to /api/studies, is 404. |
/api/plugins/{plugin_id}/jobs/{job_id}/events | GET | open | The job's audit log after ?after= (or ?cursor=); each event also carries sequence (its cursor). ?wait= long-polls up to 60 s. |
/api/plugins/{plugin_id}/jobs/{job_id}/cancel | POST | token | As the study cancel, with optional {expected_revision}. Allowed for a disabled plugin. |
/api/plugins/{plugin_id}/jobs/{job_id}/resume | POST | token | As the study resume, with optional {budget, expected_revision}. The plugin must be enabled. |
/api/plugins/{plugin_id}/jobs/{job_id}/variants/{index}/graph | GET | open | The graph a variant ran, to open the chosen one as a tab. |
Graph Copilot. Once the user approves an experiment plan in the chat, the plugin can delegate its execution: GET .../jobs/kinds to check for the feature, POST /api/plugins/graph-copilot/jobs with kind: "study" and the plan as the study spec (variants as ops on the current graph, the objective as an exact node_id + metric, the repetitions, a seed and a budget), keyed by an idempotency key derived from the approved plan so a retry never starts it twice; then GET .../jobs/{job_id}/events?after= or GET .../jobs/{job_id} to follow it from any tab, and GET .../variants/{index}/graph to apply the winner. The job keeps running after the tab closes. The model conversation and the approval stay in the plugin's page; the server runs no model.
Optional packs
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/packs | GET | open | List all packs, installed items, download sizes, and whether installation is supported on this server, plus active_job, last_restart_job (how the most recent restart-mode install ended, or null), remote_install_allowed, launch_mode, restart_available, and gpu. |
/api/packs/{id}/install | POST | token+loopback | Start an install job — 202 {"job_id"}; one job runs at a time across packs and plugins. Body (optional; an unknown key is 422): items (default: the whole pack minus what is already downloaded), mode — live (default) or restart, where a helper installs the packages after the server stops itself (see Installs that restart the server) — variant (GPU pack only: which torch wheel; a name outside the allowlist is 422), and from_dir (an absolute path on the server to an offline bundle made by cdui packs export; a relative path is 422, and a bundle the catalog does not vouch for, or from_dir with a restart-mode pack, is 400; see Offline install). Refused before any job exists: 400 for an unknown item, or {detail, blocked_by} for a missing prerequisite pack; 409 {detail, job_id[, reason]} while another install runs; 409 {detail, command[, reason]} when restart mode is unavailable or refused (run command yourself); 507 {detail, needed, free} when the disk cannot hold the download; 500 when the restart helper could not start. |
/api/packs/jobs/{job_id}/cancel | POST | token+loopback | Cancel the running job. An active download stops without completing the current file. Because graph and pack downloads share one transfer session, cancellation also interrupts any Hugging Face dataset or tokenizer download in progress for a graph. |
/api/packs/jobs/{job_id}/events | GET | open | Return a job's log and progress events after ?cursor=. ?wait= long-polls for up to 60 s. |
/api/packs/{id}/items/{item_id} | DELETE | token+loopback | Delete one downloaded model and free its bytes — 404 for an unknown item, 409 {detail, job_id} while that pack's own install runs, and removed: false when a file another process holds open survived the delete (Windows). A pack's Python packages are not removable from the running server — see cdui packs remove. |
The loopback gate protects operations that run a package manager in the server's Python environment. The catalog limits which packages can be requested. See Optional Packs for the catalog, restart mode, and cdui packs.
Source control
These routes back the Source Control tab. They run the server's own git in the project directory the server was started with (--project). Reads are open, every write needs the session token, and there is no loopback gate.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/git/status | GET | open | {repo: {state, project_dir, git_version, nested_toplevel}, status}. repo.state is no_project, git_missing, git_too_old, not_repo or ready; every state other than ready is still a 200, with status: null. nested_toplevel names the repository that a not_repo project directory sits inside. |
/api/git/log | GET | open | One page of history, newest first: {commits[{sha, short, parents, author_name, author_email, authored_at, refs, subject, body}], has_more, unborn}. ?skip= is 0 to 2147483647 (default 0) and ?limit= is 1 to 100 (default 30); a value out of range is 422. |
/api/git/commits/{sha}/files | GET | open | The files one commit changed against its first parent: [{path, orig_path, kind, xy, score}]. |
/api/git/diff | GET | open | The patch for one file: ?path=, ?scope=worktree|index|commit, and ?sha=, which scope=commit requires and the other two scopes refuse (an empty sha= counts as absent). ?blobs=1 also returns both whole sides. Returns {patch, binary, truncated, old_ref, new_ref, old_text, new_text, old_missing, new_missing}; the patch is cut at 1 MiB. |
/api/git/file | GET | open | One file at ?ref= (HEAD, index, worktree, or a commit id of 7 to 40 hex characters): {text, binary, size, truncated}. text holds at most 2 MiB; past that truncated is true and text can be empty. An ignored file read at worktree is 403 ignored. |
/api/git/config | GET | open | {name, email, name_scope, email_scope}; a scope is local, global, system, or null when the value is unset. |
/api/git/branches | GET | open | {current, detached, local[{name, sha, current, upstream, ahead, behind, gone, subject, committed_at}], remote[{name, remote, sha, subject, committed_at}]}. |
/api/git/remotes | GET | open | [{name, fetch_url, push_url}]. A password or token inside a URL is shown as ***. |
/api/git/stashes | GET | open | Newest first: [{index, message, branch, created_at}]; index is git's stash@{N}. |
/api/git/init | POST | token | Make the project directory a repository and write the .gitignore (first line .env) and .gitattributes that cdui project init writes. An existing .gitignore that does not ignore .env gets that line appended; other existing files are left alone. No body. |
/api/git/stage | POST | token | {paths: [...]} (at most 500) or {all: true}, exactly one; both, neither, or an empty list is 422. |
/api/git/unstage | POST | token | Same body; takes the paths back out of the index. |
/api/git/discard | POST | token | Same body; restores tracked files from the index and deletes untracked ones. Ignored files are never touched. |
/api/git/commit | POST | token | {message, all?: false, amend?: false}, with message up to 10,000 characters; detail.sha and detail.short name the new commit. |
/api/git/config | PUT | token | {name?, email?}, always written to this repository's own config (--local); sending neither is 400 invalid_value. Returns the identity as it now reads, in the GET shape. |
/api/git/branches | POST | token | {name, checkout?: true, start_point?}: create a branch, and switch to it unless checkout is false. |
/api/git/checkout | POST | token | {target, kind: "local" | "remote"}: switch to a local branch, or create a local branch that tracks a remote one (target is then <remote>/<branch>). changed_paths lists the files that changed. |
/api/git/branches/{name} | PUT | token | {new_name}: rename a branch. {name} may contain /. |
/api/git/branches/{name} | DELETE | token | Delete a merged branch. An unmerged one is 409 branch_not_merged until the request is repeated with ?force=1; the current branch is 400. |
/api/git/remotes | POST | token | {name, url}: add a remote. url must be https://, ssh://, file:// or user@host:path, else 400 invalid_url; a name already in use is 409 remote_exists. |
/api/git/remotes/{name} | PUT | token | {url}: point the remote at another URL, under the same URL rule. |
/api/git/remotes/{name} | DELETE | token | Remove the remote. |
/api/git/stashes | POST | token | {message?, include_untracked?: true}; {} is a valid body (a request with no body is 422). detail.stash is always 0. A tree with nothing to stash is 400. |
/api/git/stashes/{index}/pop | POST | token | Apply a stash and drop it. A pop that conflicts is 409 conflict and keeps the stash. |
/api/git/stashes/{index}/apply | POST | token | Apply a stash and keep it. |
/api/git/stashes/{index} | DELETE | token | Drop one stash. On this route and the two above, an index that is not in the current list is 404 and a negative index is 422. |
/api/git/merge/abort | POST | token | Undo the merge in progress; 404 when there is none. No body. |
/api/git/resolve | POST | token | {path, side: "ours" | "theirs" | "mark"} for a file the status lists as conflicted, else 400 path_not_in_status. mark stages the file as it is on disk. |
/api/git/fetch | POST | token | {remote?}; {} is the normal body. Fetches and prunes. |
/api/git/pull | POST | token | {strategy?: "ff-only" | "merge"} (default ff-only): fetch, then merge the upstream. A diverged branch under ff-only is 409 diverged; a merge that conflicts is 409 conflict and stays in progress. |
/api/git/push | POST | token | {remote?, set_upstream?: false}. set_upstream: true publishes the branch (push -u) to remote; when remote is omitted, to the branch's upstream remote if it has one, else to the only remote (400 invalid_value with several, 409 no_remote with none). remote without set_upstream: true is 400. A plain push with no upstream is 409 no_upstream. detail.published says which of the two ran. |
/api/git/sync | POST | token | Pull (fast-forward only) and then push, or publish a branch whose upstream is missing or gone. No body. It stops at the first failure, and the steps before it stay done; detail.steps lists the steps that ran. |
- Responses. Every write except
PUT /api/git/configanswers{status, changed_paths, head, detail}: a fresh status, the files the operation changed, HEAD afterwards, and operation-specific extras. - Errors. A failure is
{"detail": {"code", "message", "hint", "stderr"}}.codecomes from a fixed list that the editor translates, andstderris the tail of git's own output;message,hintandstderrare redacted of anything that looks like a credential.busy(409) also carriesop, the operation that holds the lock. 503git_service_unavailablemeans the server's startup did not create the git service. - Repository state. Every route other than
statusandinitneedsrepo.stateto beready, and otherwise fails with that state as its code:no_project(409; the server was started without--project),git_missing(503),git_too_old(409) ornot_repo(409).initneeds only the project directory and git 2.23 or newer. - One operation at a time. A second write while one runs is 409
busy.fetch,pushand the network steps ofpullandsynchold a separate lock, so a commit still works while a push is transferring, and a second network request is the one that gets 409busy. - Dotenv files.
/diffand/filerefuse.envand.env.*(not.env.example; matched case-insensitively) in any path segment, at every ref, with 403ignored.
Files, models, images and media
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/files | GET | open | List uploaded data files (.csv, .tsv, .txt, .json) — the DATA_FILE dropdown behind CSVReader and friends. |
/api/files/upload | POST | token | Upload a data file (those four extensions only). |
/api/files/download/{filename} | GET | open | Download a data file. |
/api/files/{filename} | DELETE | token | Delete a data file. |
/api/models | GET | open | List uploaded model files. |
/api/models/upload | POST | token | Upload a model weight file. |
/api/models/download/{filename} | GET | open | Download a model weight file (supports nested paths). |
/api/models/{filename} | DELETE | token | Delete a model file. |
/api/images | GET | open | List uploaded image files. |
/api/images/upload | POST | token | Upload an image file. |
/api/images/download/{filename} | GET | open | Download an image file. |
/api/images/{filename} | DELETE | token | Delete an image file. |
/api/media | GET | open | List run-produced media (.mp4, .webm, .gif, .png, .jpg, .jpeg), recursively. |
/api/media/{filename} | GET | open | Serve one media file inline with its real Content-Type and Range support, so a <video> element can seek. Read-only: files appear here only because a node (VideoWrite) wrote them — there is no upload and no delete. |
LLM proxy
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/llm/chat | POST | token | Stream a unified SSE chat completion from the configured provider (OpenAI / OpenRouter / Anthropic / OpenAI-Codex / custom OpenAI-compatible). |
/api/llm/models | POST | token | List the models available for a provider. |
/api/llm/codex/login | POST | token | Start the OpenAI-Codex (ChatGPT account) OAuth login flow. |
/api/llm/codex/status | GET | open | Report OpenAI-Codex OAuth login status. |
/api/llm/codex/logout | POST | token | Clear stored OpenAI-Codex OAuth tokens. |
Limits and errors
-
Body size.
MAX_RUN_BODY_BYTES(64 MB,CODEFYUI_MAX_RUN_BODY_BYTES) limits every request body and counts bytes as they arrive, including chunked requests. Four routes useMAX_UPLOAD_SIZE(500 MB,CODEFYUI_MAX_UPLOAD_SIZE) instead:/api/files/upload,/api/images/upload,/api/models/upload, and/api/custom-nodes/upload. They allow an additional 64 KB for multipart metadata and apply the configured limit to the file itself. Exceeding either limit returns 413. -
File names. The four upload routes store a file under the part of its name after the last
/, and answer 400 for a name that Windows cannot store: one that contains< > : " \ | ? *or a control character, is longer than 255 bytes in UTF-8 (a Chinese character takes 3), is made only of dots, or is a device name such ascon.csv. The rule is the same on every server OS, so uploaded files can move between Windows, macOS and Linux. The download and delete routes and/api/custom-nodes/togglerefuse a name with a control character, and on a Windows server also one with those characters; on Linux and macOS such a name is looked up as usual, so a file already stored under one stays reachable. They set no length limit: a name part longer than the server's file system holds answers 404./api/custom-nodes/uploadand/api/custom-nodes/togglealso refuse names that start with__, such as__init__.py. Each refusal'sdetailsays what to change./api/presets/createapplies the same rule to a preset's name, measuring the length of the file it writes (the name with.jsonadded), with a codeddetail. -
Order of refusals. The Host guard runs first and can return 421, authentication runs next and can return 403 or 401, and body-size validation runs last. A rejected request body is not read. An unauthenticated request therefore does not receive a 413 response.
-
WebSocket frames.
WS_MAX_MESSAGE_BYTES(CODEFYUI_WS_MAX_MESSAGE_BYTES, which defaults to the request-body limit) is enforced by the transport. An oversized frame closes the connection with code 1009 rather than returning 413. See Graph as a Function, section 8 when launching uvicorn directly. -
Error shapes. The API uses four formats by route family:
{"detail": "<text>"}— the default format, including 413 responses and the 403 that the loopback gate returns on plugin and pack routes. Pack routes add fields next todetail, such asjob_id,reason,blocked_by,command,needed, andfree.{"detail": {"code", "message", "details"}}— app-management routes and/api/keys, for example 404app_not_found, 422incomplete_cursor, and 404key_not_found.{"detail": {"code", ...}}withoutmessage— Plugin Center routes. Additional fields can includejob_id,known,missing_capabilities,allowed_modules,plugin_id,inspection_id, andid.{"detail": {"code", "message", "hint", "stderr"}}— every/api/gitfailure;busyalso carriesop. See Source control.
Two shapes cut across the families: the
/api/graph/savecollision guard answers 409{"detail": {"error": "graph_exists", "file", "name"}}, and a request that does not match a route's declared body or query parameters (a missing field, an unknown key where the body forbids one, a value out of range) is FastAPI's 422{"detail": [{"type", "loc", "msg", ...}]}.POST /api/graph/run/{name}andPOST /api/apps/{slug}/invokeare the exception to all of these: every response — 413 and 401 included — is the 9-key run envelope, see the envelope.
WebSocket protocol
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/ws/execution | WebSocket | token | Attach/subscribe view over a run: execute starts one, attach replays its event log from a cursor and then follows it live, detach unsubscribes, cancel stops it. Closing the socket never cancels. |
/ws/worker | WebSocket | worker token, or token from loopback | Where worker agents (cdui agent start) connect. The credential travels in X-CodefyUI-Token (or ?token=): a worker token is accepted from any peer, the session token only from a loopback peer. A Host outside the allowlist, or any request carrying an Origin header, is closed with 4003 before the socket is accepted (an HTTP client sees 403), and a missing or invalid credential is answered with HTTP 401 (under uvicorn; a server without handshake responses closes with 4401, which also reads as 403). Only a 401 tells the agent that its token was refused. The first frame must be hello within 10 s; a protocol-version or CodefyUI-version mismatch (both ends must run the same release), an invalid or reserved worker name, a worker token's hello under another worker's name, or a connection replacing a live one of the same name that authenticated another way gets reject and a close. A worker counts as on this machine only when it connects from loopback and reports this machine's id. The frames are defined in backend/app/core/worker_protocol.py; see Execution Architecture. When it closes, the worker's runs keep running there and become disconnected; they continue when the worker reconnects within CODEFYUI_WORKER_RESUME_GRACE_S (300 s) and end interrupted after that, at once with a grace of 0 or when the worker is removed (Remote Workers, When the connection drops). |
Handshake. ws://<host>:<port>/ws/execution?token=<session token> — the ?token= query parameter, since browsers cannot set headers on a WebSocket handshake, or the X-CodefyUI-Token header for non-browser clients. Close codes before the socket is accepted: 4003 when the Host — or, for a browser, the Origin — is not whitelisted, 4401 for a missing or invalid token; 1009 later, for a frame over the size cap.
Client actions — JSON text frames with an action field:
| Action | Fields | Effect |
|---|---|---|
execute | nodes, edges, presets, subgraphs, settings, device, seed, deterministic, record_outputs, changed_nodes, name, and the canvas flags (verbose_mode, graph_id, weights_persistent, backward_mode, auto_backward, error_mode, max_retries) | Submit on the interactive lane and attach to the new run. A run this socket already follows is detached, not cancelled. settings, the graph's own settings object, is stored with the run's graph; the run itself uses device, which is cpu when omitted. The run is listed under name (the canvas sends its tab's label), clipped to 64 characters. |
attach | run_id, cursor (an integer, 0 or more, not past the run's latest cursor) | Replay the event log from cursor, then follow live; replaces the previous attachment. |
detach | — | Stop following. Never cancels. |
cancel | run_id (optional; defaults to the attached run) | Cooperative stop. The pre-v2 alias stop was removed in 3.0. |
clear_cache | — | Drop this socket's execution cache. |
Server frames. Stored run events use {type, run_id, cursor, ...payload} (execution_start, node_status, execution_stopped, and others). A node_status for a block or preset card rolls up every node inside it. Those nodes report on inner_node_status {node_id, container_id, status}, plus error and error_type on an error and outputs when the node produced something (its media entries and port summary; its text stays on the card's frames), where node_id is the id the run gives the node (<instance>/<node>, deeper for nested blocks) and container_id is the outermost card. A card's terminal node_status carries a port summary of its exposed outputs. A progress frame from inside a card stays a node_status for the card and names the node in inner_node_id. A node the run bypassed is reported once, before anything runs, with status bypassed, and a node left out because a Switch's selector param does not pick its branch is reported the same way with status unselected. Clients use cursor when attaching again. Transport-only frames are attached {run_id, cursor, status}, detached {run_id}, cancel_ack {run_id, status, cancelled}, execution_stopped {reason: "not_running"} when no run can be cancelled, cache_cleared, error {error} for a malformed or unknown action, and execution_error {error} when the run service rejects a submission. cancel_ack acknowledges the request; the run emits its own execution_stopped event after it stops. Rejections caused by the interactive limit or the one-run-per-session rule also include rejected: true and the currently attached run_id. In that case no new run starts, and the existing attachment remains active.