> ## Documentation Index
> Fetch the complete documentation index at: https://docs.comfy.org/llms.txt
> Use this file to discover all available pages before exploring further.

# The workflow behind a job — authoring version if pinned, executed graph otherwise

> Returns the workflow behind a job. The response's `format` field says
which of two different shapes `workflow` is in:

- `format: save` — the original authoring workflow exactly as saved
  in the Comfy Cloud editor at the version the job ran, including
  canvas layout and frontend-only nodes (e.g. Note nodes; Get/Set
  nodes not yet expanded). Returned only when the job is pinned to a
  specific workflow version — see the "when you get which" note
  below.
- `format: api` — the executed API-format prompt graph the job
  actually ran: frontend-only constructs are gone and Get/Set nodes
  are expanded. This is the same shape `POST /api/v2/jobs`'s
  `workflow` request field takes, and never includes the
  submission's `extra_data`, which can carry a live credential.

Always branch on `format`, never assume one or the other — which
shape comes back depends on how the job was submitted, not on
anything the caller controls per-request.

A deliberate sub-resource, not a field on `GET /api/v2/jobs/{id}` —
so the polling workhorse stays cheap and a caller pays for this only
when it actually wants the workflow (for example, to recover what
produced a given output).

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) — there is no separate lifetime for the
workflow.

**When you get which:** a job only carries a pinned workflow version
when it was submitted with that association. Today that means jobs
submitted from the Comfy Cloud frontend/editor. Jobs submitted
directly through this v2 API (`POST /api/v2/jobs`) do not carry that
association — v2 job submission has no version-linking fields yet —
so they always get `format: api`. This is expected, not a bug: it
will change once v2 submission grows the same version pinning.

A job pinned to a version also falls back to `format: api` if that
version, or the workflow it belongs to, is no longer readable by the
caller — for example the caller deleted the workflow since the job
ran. This is the same fallback as an unpinned job, and for the same
reason: it is preferable to the alternative of erroring the whole
request over data that is genuinely gone.




## OpenAPI

````yaml /openapi-v2.yaml get /api/v2/jobs/{id}/workflow
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}/workflow:
    get:
      tags:
        - jobs
      summary: >-
        The workflow behind a job — authoring version if pinned, executed graph
        otherwise
      description: |
        Returns the workflow behind a job. The response's `format` field says
        which of two different shapes `workflow` is in:

        - `format: save` — the original authoring workflow exactly as saved
          in the Comfy Cloud editor at the version the job ran, including
          canvas layout and frontend-only nodes (e.g. Note nodes; Get/Set
          nodes not yet expanded). Returned only when the job is pinned to a
          specific workflow version — see the "when you get which" note
          below.
        - `format: api` — the executed API-format prompt graph the job
          actually ran: frontend-only constructs are gone and Get/Set nodes
          are expanded. This is the same shape `POST /api/v2/jobs`'s
          `workflow` request field takes, and never includes the
          submission's `extra_data`, which can carry a live credential.

        Always branch on `format`, never assume one or the other — which
        shape comes back depends on how the job was submitted, not on
        anything the caller controls per-request.

        A deliberate sub-resource, not a field on `GET /api/v2/jobs/{id}` —
        so the polling workhorse stays cheap and a caller pays for this only
        when it actually wants the workflow (for example, to recover what
        produced a given output).

        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) — there is no separate lifetime for the
        workflow.

        **When you get which:** a job only carries a pinned workflow version
        when it was submitted with that association. Today that means jobs
        submitted from the Comfy Cloud frontend/editor. Jobs submitted
        directly through this v2 API (`POST /api/v2/jobs`) do not carry that
        association — v2 job submission has no version-linking fields yet —
        so they always get `format: api`. This is expected, not a bug: it
        will change once v2 submission grows the same version pinning.

        A job pinned to a version also falls back to `format: api` if that
        version, or the workflow it belongs to, is no longer readable by the
        caller — for example the caller deleted the workflow since the job
        ran. This is the same fallback as an unpinned job, and for the same
        reason: it is preferable to the alternative of erroring the whole
        request over data that is genuinely gone.
      operationId: getJobWorkflow
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: The workflow graph.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobWorkflowResponse'
        '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:
    JobWorkflowResponse:
      type: object
      description: >-
        The workflow behind a job. See GET /api/v2/jobs/{id}/workflow's
        description for exactly when `format` is `save` vs `api`.
      required:
        - workflow
        - format
      properties:
        workflow:
          type: object
          description: The workflow, verbatim, in the shape `format` says.
          additionalProperties: true
        format:
          type: string
          enum:
            - save
            - api
          description: >-
            Discriminates the `workflow` field's shape. `save`: the original
            authoring workflow JSON, at the version pinned to the job. `api`:
            the executed API-format prompt graph.
    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
  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'
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before retrying.
  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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.