> ## Documentation Index
> Fetch the complete documentation index at: https://dripart-chore-sync-comfy-api-v2-spec.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 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**, and the cases
behind it are deliberately not distinguished: 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,
capture was attempted and failed, or the job ran on the public demo
deployment, which captures and stores the log like every other
serverless deployment but withholds it on read, because that surface
takes callers with no credential and a job id would otherwise be the
only thing between one anonymous caller and another's run.

Because a `204` never says which of those it is, do not branch on the
reason — but do note that one of them resolves itself. A job that has
not finished may have a log once it does, so a caller that wants one
reads again after a terminal status. A `204` on a job already in a
terminal state is final, and so is a missing `urls.logs`; both mean
stop asking.

**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 `404`s 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`, so a job never outlives its 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**, and the cases
        behind it are deliberately not distinguished: 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,
        capture was attempted and failed, or the job ran on the public demo
        deployment, which captures and stores the log like every other
        serverless deployment but withholds it on read, because that surface
        takes callers with no credential and a job id would otherwise be the
        only thing between one anonymous caller and another's run.

        Because a `204` never says which of those it is, do not branch on the
        reason — but do note that one of them resolves itself. A job that has
        not finished may have a log once it does, so a caller that wants one
        reads again after a terminal status. A `204` on a job already in a
        terminal state is final, and so is a missing `urls.logs`; both mean
        stop asking.

        **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 `404`s 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`, so a job never outlives its 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.
        '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'
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),
        `queue_full` (429 + Retry-After), `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) and
        `deployment_stopped` (422 — terminal deployment state; a retry
        cannot succeed without operator action). 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'
            details:
              type: object
              nullable: true
              additionalProperties: true
              example:
                node_errors:
                  '12':
                    - field: model
                      reason: missing_input
  responses:
    Unauthorized:
      description: '`unauthorized` — missing or invalid credentials.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: '`forbidden` — authenticated but not allowed.'
      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 the request rate limit for this
        account. Account/rate-scoped, not job-specific — this can be returned
        even for a job id the caller doesn't own or that doesn't exist, without
        revealing which.
      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'
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before retrying.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        `Authorization: Bearer <api-key>` — account-scoped API keys on Cloud and
        serverless. Self-hosted accepts unauthenticated requests by default and
        can be configured with a static bearer token.

````