ComfyPilotComfyPilot
文档 / 接口 / What the run printed

What the run printed

Returns the job's captured execution log. Fetched on demand: a log

接口 what-the-run-printed 更新于 2026-11-13

查看官方原文

What the run printed

Returns the job's captured execution log. Fetched on demand: a log

is a debugging artifact a caller wants occasionally, while GET /api/v2/jobs/{id} is polled to terminal on every run, so the log is a resource of its own rather than a field that would ride every one of those polls to be read at most once.

Captured whenever the worker reports its own outcome, success and failure alike, since a job that succeeds while producing the wrong thing is exactly what a failure-only log cannot explain. A run the platform or the provider killed — out of memory, a crashed worker, a timeout, a job past its maximum runtime — never gets that far, so it reaches a terminal status carrying no log at all. That is a real gap and worth stating: the failures a caller most wants a log for are the ones least likely to have produced one.

204 is the normal answer for a job with no log. The cases behind it: this surface does not capture logs at all, the job has not finished, the job predates log capture, the run was killed before the worker could report one, the worker reported none, or capture was attempted and failed.

A surface that captures logs may say which side of the job's end the answer falls on, in the optional No-Log-Reason response header:

  • not_finished: the job has not reached a terminal status. It may have a log once it does, so a caller that wants one reads again after a terminal status.
  • finished: the job reached a terminal status with no log captured. This does not say why. In particular it does not mean the run printed nothing: a killed run, an empty report and a failed capture all answer the same.

The header is absent on a surface that captures no logs, and on a build that does not send it yet, so its absence says nothing about the job; read the job's status instead. A 204 on a job already in a terminal state means stop asking, with or without the header, and so does a missing urls.logs.

Only jobs run on the serverless platform (a {deployment}.run.comfy.app host) have one today. An implementation that captures no logs must still serve this operation, answering 204 for every job it can read, so that the two answers stay distinct — Comfy Cloud does. A self-hosted deployment on a build predating this operation has not implemented it yet and will answer a routing 404 instead, which is the case job.urls.logs exists to keep a client out of: its absence says the surface has no logs at all, without a request.

Tied to the job's own retention: this 404s under the same conditions GET /api/v2/jobs/{id} does (unknown, not-yours, or past its retention deadline). Nothing ages a log out ahead of the job's own expires_at. Account deletion is an exception: it clears the workflow, caller labels and log in a personal workspace the account occupies alone. A scrubbed job answers 200 with empty text, truncated: false, complete: true and captured_at set to the scrub time, even if it never captured a worker log.

Live tailing is not offered here yet. When it is, it arrives on this same path under Accept: text/event-stream, leaving this JSON snapshot the default; its resume semantics will be defined then, against a capture that is incremental. Until then the SSE log event on GET /api/v2/jobs/{id}/events is the reserved live rail, and this is the authoritative snapshot it reconciles against.

OpenAPI

````yaml /openapi-v2.yaml get /api/v2/jobs/{id}/logs openapi: 3.0.3 info: title: Comfy API v2 version: 2.0.0

description: |

The official, versioned HTTP API for running ComfyUI workflows from external applications: upload inputs, submit a workflow, observe execution, retrieve results.

Design principles:

  • Poll-first. Every capability is reachable via plain GET polling; the SSE stream is a live enhancement, never the source of truth.
  • Everything is resumable. Submission is idempotent; job state and outputs are retrievable by ID until expires_at.
  • UUID identity, content-addressed dedup. Assets are UUID-identified records over blobs keyed by a server-computed blake3 hash. The hash is nullable and may be computed lazily.
  • Follow links, don't build URLs. Responses embed follow-up URLs.

Additive changes only within v2; breaking changes require v3. servers:

  • url: http://127.0.0.1:8189 description: Self-hosted (comfy-api-proxy)
  • url: https://cloud.comfy.org description: Comfy Cloud
  • url: https://{deployment}.run.comfy.app description: Serverless deployment variables: deployment: description: >- DNS-safe deployment id (subdomain label). Staging uses {deployment}.stg.run.comfy.app. default: dep-1234abcd-56ef-7890-abcd-ef1234567890 security:
  • bearerAuth: []
  • {} tags:
  • name: assets description: UUID-identified records over content-addressed blobs.
  • name: jobs description: One execution of a workflow — durable, pollable, cancelable. paths: /api/v2/jobs/{id}/logs: get: tags:
  • jobs summary: What the run printed description: | Returns the job's captured execution log. Fetched on demand: a log is a debugging artifact a caller wants occasionally, while GET /api/v2/jobs/{id} is polled to terminal on every run, so the log is a resource of its own rather than a field that would ride every one of those polls to be read at most once.

Captured whenever the worker reports its own outcome, success and failure alike, since a job that succeeds while producing the wrong thing is exactly what a failure-only log cannot explain. A run the platform or the provider killed — out of memory, a crashed worker, a timeout, a job past its maximum runtime — never gets that far, so it reaches a terminal status carrying no log at all. That is a real gap and worth stating: the failures a caller most wants a log for are the ones least likely to have produced one.

204 is the normal answer for a job with no log. The cases behind it: this surface does not capture logs at all, the job has not finished, the job predates log capture, the run was killed before the worker could report one, the worker reported none, or capture was attempted and failed.

A surface that captures logs may say which side of the job's end the answer falls on, in the optional No-Log-Reason response header:

  • not_finished: the job has not reached a terminal status. It may have a log once it does, so a caller that wants one reads again after a terminal status.
  • finished: the job reached a terminal status with no log captured. This does not say why. In particular it does not mean the run printed nothing: a killed run, an empty report and a failed capture all answer the same.

The header is absent on a surface that captures no logs, and on a build that does not send it yet, so its absence says nothing about the job; read the job's status instead. A 204 on a job already in a terminal state means stop asking, with or without the header, and so does a missing urls.logs.

Only jobs run on the serverless platform (a {deployment}.run.comfy.app host) have one today. An implementation that captures no logs must still serve this operation, answering 204 for every job it can read, so that the two answers stay distinct — Comfy Cloud does. A self-hosted deployment on a build predating this operation has not implemented it yet and will answer a routing 404 instead, which is the case job.urls.logs exists to keep a client out of: its absence says the surface has no logs at all, without a request.

Tied to the job's own retention: this 404s under the same conditions GET /api/v2/jobs/{id} does (unknown, not-yours, or past its retention deadline). Nothing ages a log out ahead of the job's own expires_at. Account deletion is an exception: it clears the workflow, caller labels and log in a personal workspace the account occupies alone. A scrubbed job answers 200 with empty text, truncated: false, complete: true and captured_at set to the scrub time, even if it never captured a worker log.

Live tailing is not offered here yet. When it is, it arrives on this same path under Accept: text/event-stream, leaving this JSON snapshot the default; its resume semantics will be defined then, against a capture that is incremental. Until then the SSE log event on GET /api/v2/jobs/{id}/events is the reserved live rail, and this is the authoritative snapshot it reconciles against. operationId: getJobLogs parameters:

  • $ref: '#/components/parameters/JobId' responses: '200': description: The captured log. content: application/json: schema: $ref: '#/components/schemas/JobLogs' '204': description: >- This job has no log. A normal answer, not an error — see the description for the cases it covers. headers: No-Log-Reason: $ref: '#/components/headers/NoLogReason' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/UpstreamError' '503': $ref: '#/components/responses/DeploymentLookupUnavailable' components: parameters: JobId: name: id in: path required: true schema: type: string example: 7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b schemas: JobLogs: type: object description: >- A job's captured execution log — the body of `GET /api/v2/jobs/{id}/logs`. Diagnostics, not a contract on content: this is whatever the workflow's own code and nodes wrote to standard output, in the order they wrote it, so nothing about its shape is stable between runs or between releases of a build. It is untrusted text — a workflow chooses what goes in it — and must be rendered as plain text rather than interpreted. required:
  • text
  • truncated
  • captured_at
  • complete properties: text: type: string description: The captured output. truncated: type: boolean description: >- The BEGINNING of the captured output was discarded — text is the TAIL of a longer run. Implementations bound what they capture and store, so a workflow that prints megabytes keeps its last lines, where a failure normally is, instead of being dropped whole. True with an empty text means the log was captured and then shed entirely to fit. This describes the stored log, never the response: it does not mean a caller asked for part of one. captured_at: type: string format: date-time description: When the run's output was read back off the worker. complete: type: boolean description: >- No further output will be appended to this log. Always true today, because a log is read back off the worker once, when the run ends, so a log that exists is already whole. Sent so that a surface which later captures output while a run is still going can say so, and a client written now against false keeps working when it does. false does not promise that more output will arrive, only that this snapshot may not be the last one. ErrorEnvelope: type: object description: | Shared error envelope with machine-readable codes. Core codes (v1): invalid_workflow (422), workflow_format_ui (422), missing_asset (422), hash_mismatch (409), blob_not_found (404), idempotency_key_reuse (422), upload_timed_out (422: an asset upload stopped sending or was too slow; retry from a faster connection), queue_full (429 + Retry-After), rate_limited (429 + Retry-After: the caller is past a request rate limit; retry), insufficient_credits (402), not_found (404), unauthorized (401), forbidden (403). Deployment-scoped surfaces add: deployment_not_ready (429 + Retry-After — the deployment can still reach ready; retry), deployment_unavailable (429 + Retry-After: the deployment is ready but its GPU provider is not taking work on it yet; retry), deployment_lookup_unavailable (503 + Retry-After, on any operation: the deployment the address names could not be looked up just now; nothing was done, so retry), database_unavailable (503 + Retry-After, only on a job submit: the job could not be saved just now and nothing was started, so retry), auth_unavailable (503 + Retry-After, on any operation: the credential could not be checked just now, which says nothing about the credential, so retry), output_unavailable (503 + Retry-After, only on a job status read: an output of the job could not be prepared just now; the job is unchanged, so retry after Retry-After, and report the job if it keeps answering), deployment_stopped (422 — terminal deployment state; a retry cannot succeed without operator action), invalid_request (422: a malformed asset upload or asset-from-hash field, or a jobs-list limit that is not a positive integer), validation_error (422, Cloud: an asset upload or asset-from-hash field was refused, e.g. a tag over the tag limits), content_blocked (451: content moderation flagged the asset's bytes) and sso_required (403: the key is valid, but the account must sign in through its organization's single sign-on, which does not accept this key). Comfy Cloud's asset upload adds invalid_content_type (422: the request is not multipart/form-data with a boundary), invalid_body (422: a malformed multipart body or upload field), validation_error (422: the platform refused the upload, for example an image over the size cap or a disallowed tag) and upload_busy (503 + Retry-After: the server is busy with other uploads and nothing was stored, so retry). Job labels add: metadata_invalid (422: a submitted metadata breaks a limit; details.key names the key, except for too many pairs, which has no details), metadata_not_supported (422: this surface keeps no job labels yet), invalid_metadata_filter (400) and invalid_cursor (400). A surface that does not list jobs yet answers not_implemented (501). A 429 is disambiguated by error.code alone; clients should treat any 429 + Retry-After as "back off and retry". required:
  • error properties: error: type: object required:
  • code
  • message properties: code: type: string example: invalid_workflow message: type: string example: 'Node 12 (KSampler): required input ''model'' is not connected' organization_id: type: string description: >- On sso_required: the organization whose single sign-on governs this key, the one that holds the account, else the one that holds the key's workspace. It is the organization query parameter of Comfy Cloud's single sign-on start; treat it as opaque. Absent when the organization is unknown, and on every other code. example: org_01HXYZEXAMPLE details: type: object nullable: true additionalProperties: true description: >- Machine-readable detail for the code. When it carries node_errors, that is keyed by node id and each value is a JobNodeError, the same shape as a job's error.node_errors, whether the refusal came at submit (for example unknown_node_class, a node class the deployment's build does not contain) or from ComfyUI after dispatch. With unknown_node_class errors it carries unknown_node_classes, the missing classes; with waits_for_browser errors, browser_wait_node_classes, the classes that wait for a browser. A submit refusal names at most fifty nodes. For invalid_node, invalid_inputs and invalid_class_type, when it found more than fifty, node_errors_truncated is true and malformed_node_count carries the full count; for waits_for_browser, browser_wait_node_count does. For unknown_node_class it names only nodes whose class is one of the at most ten listed in unknown_node_classes; when those nodes number more than fifty, node_errors_truncated is true and unknown_node_count is how many nodes have a listed class. Nodes whose class is past those ten are neither named nor counted. example: node_errors: '12': class_type: SomeCustomNode errors:
  • type: unknown_node_class message: >- this deployment's build does not contain the node class SomeCustomNode. unknown_node_classes:
  • SomeCustomNode headers: NoLogReason: schema: type: string enum:
  • not_finished
  • finished description: >- On a 204 from GET /api/v2/jobs/{id}/logs, whether the job has ended: not_finished, or finished with no log captured, which does not say why. Optional; see that operation. Clients tolerate unknown values: a later release may add a reason. RetryAfter: schema: type: integer description: Seconds to wait before retrying. responses: Unauthorized: description: >- unauthorized: missing or invalid credentials. A credential that could not be checked answers auth_unavailable (503) instead. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' Forbidden: description: >- forbidden — authenticated but not allowed. On a serverless deployment, also sso_required — the key is valid, but the account must sign in through its organization's single sign-on, which does not accept this key; it can come back on any operation. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' NotFound: description: 'not_found.' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' RateLimited: description: >- rate_limited — the caller has exceeded a request rate limit for this account. Account/rate-scoped, not resource-specific — this can be returned even for a job, asset or deployment the caller doesn't own or that doesn't exist. headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' UpstreamError: description: >- upstream_error — an unexpected failure reaching or processing the request in this implementation's backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation's own error-mapping notes. Every operation in this contract can fail this way. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' DeploymentLookupUnavailable: description: >- On deployment-scoped surfaces, deployment_lookup_unavailable: the deployment this address names could not be looked up just now. The request was refused before it did anything (nothing was created, read, changed or canceled), so the same request can be sent again after Retry-After. Every operation at a deployment's address can answer this way. A job submit can also answer this 503 with database_unavailable: the job could not be saved just now and nothing was started, so the same request can be sent again after Retry-After. No other operation answers database_unavailable. On Comfy Cloud an asset upload can also answer this 503 with upload_busy: the server is busy with other uploads and nothing was stored, so the same upload can be sent again after Retry-After. Any operation can also answer this 503 with auth_unavailable: the credential could not be checked just now, so the same request can be sent again after Retry-After. A job status read can also answer it with output_unavailable: an output of the job could not be prepared just now, and the job is unchanged. A transient storage failure clears on a later read after Retry-After; an output whose stored address is broken answers it on every read until the platform repairs the output. headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' securitySchemes: bearerAuth: type: http scheme: bearer description: >- Authorization: Bearer <credential>. The credential is one of: an account-scoped API key (comfyui-…), accepted on Cloud and serverless; a Comfy Cloud session JWT; or an OAuth access token issued for the Comfy Cloud resource. Which kinds a given deployment accepts is deployment configuration — an API key always works on Cloud and serverless, and a deployment that does not accept JWT bearers answers 401 with a message saying so. Self-hosted accepts unauthenticated requests by default and can be configured with a static bearer token.

````