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

# Dry-run (validate) a workflow

> Validates a workflow without running it or causing any side effect. It checks that the workflow's current definition is executable, that the trigger's referenced survey and ending cards still exist in the workspace, and that every `send_email` action addressing a *literal* email address targets someone who can access this workspace. **No** workflow run is created and **no** email or webhook is sent.

The response is always `200` with `{ data: { workflowId, ok, problems } }`. `data.ok` is `true` when the workflow would execute; otherwise `data.problems` lists every issue found — each with a machine-readable `code` and the offending `field` — so they can be fixed in a single pass.

Drafts are testable — validating the setup before going live is the point of a dry run. Only an `archived` workflow is rejected, with **422** and code `invalid_workflow_state`.




## OpenAPI

````yaml /api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/test
openapi: 3.1.1
info:
  title: Formbricks API v3
  description: >
    **Surveys**: **GET /api/v3/surveys**, **POST /api/v3/surveys**, **POST
    /api/v3/surveys/generate**, **POST /api/v3/surveys/validate**, **GET
    /api/v3/surveys/{surveyId}**, **PATCH /api/v3/surveys/{surveyId}**, and
    **DELETE /api/v3/surveys/{surveyId}**.


    **Workflows extension**: **GET /api/v3/workflows**, **POST
    /api/v3/workflows**, **GET /api/v3/workflows/{workflowId}**, **PATCH
    /api/v3/workflows/{workflowId}**, **DELETE /api/v3/workflows/{workflowId}**,
    **POST /api/v3/workflows/{workflowId}/duplicate**, **POST
    /api/v3/workflows/{workflowId}/enable**, **POST
    /api/v3/workflows/{workflowId}/disable**, **POST
    /api/v3/workflows/{workflowId}/archive**, **POST
    /api/v3/workflows/{workflowId}/unarchive**, **POST
    /api/v3/workflows/{workflowId}/test**, **GET /api/v3/workflows/runs**, and
    **GET /api/v3/workflows/runs/{runId}**.


    Authenticate all management endpoints with **session cookie** or
    **`x-api-key`** (management key with access to the workspace).


    **Spec location:** source of truth is the multi-file tree at
    `docs/api-v3-reference/src/` (root `openapi.yml` plus one file per path and
    component); `docs/api-v3-reference/openapi.yml` is the generated single-file
    bundle (alongside v2 at `docs/api-v2-reference/openapi.yml`).


    **workspaceId**

    `workspaceId` is the canonical container identifier for this API. It is a
    **required query parameter on collection read endpoints** (`GET
    /api/v3/surveys`, `GET /api/v3/action-classes`, `GET
    /api/v3/contact-attribute-keys`) because those operate within a workspace.
    **`POST /api/v3/surveys` instead requires `workspaceId` in the request
    body**, not as a query parameter. Single-resource endpoints
    (`GET`/`PATCH`/`DELETE /api/v3/surveys/{surveyId}`) locate the resource by
    its globally-unique id and resolve the workspace from it, so they do **not**
    accept a `workspaceId` query parameter — sending one returns **400**
    (`Unrecognized key: "workspaceId"`).


    **Auth**

    Authenticate with either a session cookie or **`x-api-key`**. In dual-auth
    mode, V3 checks the API key first when the header is present, otherwise it
    uses the session path. Unauthenticated callers get **401** before query
    validation.


    **Pagination**

    Cursor-based pagination with **limit** + opaque **cursor** token. Responses
    return `meta.nextCursor`; pass that value back as `cursor` to fetch the next
    page. Survey list responses also include `meta.totalCount`, the total number
    of surveys matching the current filters across all pages. Workflow list
    responses do not require total counts in Scope 1. There is no `offset` in
    this contract.


    **Filtering**

    Filters use explicit operator-style query parameters under the
    **`filter[...]` family**. The survey list supports `filter[name][contains]`,
    `filter[status][in]`, and `filter[type][in]`. The workflow list supports
    `filter[name][contains]` and `filter[status][in]`; the workflow run list
    supports `filter[status][in]` and `filter[isDryRun][eq]`. Resource-scoping
    identifiers (`workspaceId`, `workflowId`, `responseId`) stay plain query
    parameters. Multi-value filters use repeated keys or comma-separated values
    (e.g. `filter[status][in]=draft&filter[status][in]=inProgress` or
    `filter[status][in]=draft,inProgress`). Sorting remains a flat `sortBy`
    query parameter.


    **Security**

    Missing/forbidden workspace returns **403** with a generic message (not
    **404**) so resource existence is not leaked. Workflow and workflow-run
    detail endpoints also return **403** for unknown IDs or inaccessible IDs to
    avoid IDOR leaks. List responses use `private, no-store`.


    **AI survey creation**

    `POST /api/v3/surveys/generate` returns a draft `POST /api/v3/surveys`
    payload plus validation metadata. It does not create a survey. It generates
    survey content for both `link` and `app` surveys; for `app` it seeds a
    default `distribution` (display once, no triggers, no targeting) that you
    finish configuring before publishing.

    Prompt text is sent to the configured AI provider for generation, but it is
    not stored by this endpoint, not logged by default, and not included in
    audit data.


    **Feedback Records** `/api/v3/feedbackRecords` is served by the **API
    gateway** (Envoy on Formbricks Cloud, Traefik in the one-click Docker
    install), which authorizes each call against the dataset and forwards it to
    the feedback store — not by the application routes that serve the rest of
    v3. Three consequences: the payload is the store's own, so members and
    filters are **`snake_case`** (`tenant_id`, `value_text`) rather than v3's
    camelCase; the dataset is named **`tenant_id`** on the wire and is required
    on every collection operation (it is the feedback dataset id, which the MCP
    `list_feedback_datasets` tool reports as `dataset_id`); and errors come in
    two shapes — gateway refusals (**400**, **401**, **403**, **413**, **503**)
    are `text/plain`, while the store's own problems are
    `application/problem+json` with a stable `code`. These eight operations are
    the whole of the gateway-served surface; any other path it receives under
    the prefix answers **400**. Authenticate with **`x-api-key`**: the key needs
    a permission on a workspace the dataset is assigned to — `read` to list, get
    and search, `write` to create and update, `manage` to delete — and may
    update or delete only in a dataset assigned to exactly one workspace.

    **OpenAPI**

    This spec is **not** produced by `pnpm generate-api-specs` (that script only
    builds v2 → `docs/api-v2-reference/openapi.yml`). When the route contract
    changes, edit the files under `docs/api-v3-reference/src/`, then run `pnpm
    api:v3:bundle` to regenerate the committed single-file bundle. `pnpm
    api:v3:lint` lints the source tree; `pnpm api:v3:check` (CI) fails when the
    bundle is stale.


    **Workflows Scope 1**

    Workflows are a follow-up extension of the existing v3 Survey API work. The
    Survey API establishes API-first survey authoring; Workflows build on that
    same v3 style to automate actions around survey responses through
    workspace-scoped JSON definitions. Scope 1 models the existing Follow-ups
    capability as workflows: one `response.completed` trigger with an optional
    `endingCardIds` filter, and `send_email` actions with Follow-up email field
    parity. The definition and run shapes mirror the shared Zod schemas in
    `packages/workflows` (ENG-1100), which are the implementation source of
    truth for this contract.


    Workflows use `draft`, `enabled`, and `disabled` lifecycle states plus an
    `archived` soft-delete state. Status only changes through the lifecycle
    endpoints (`enable`, `disable`, `archive`, `unarchive`); it is not writable
    via `POST` or `PATCH`. Enabling validates that the definition is executable
    and snapshots an immutable workflow version; runs reference that snapshot
    via `workflowVersionId`. `POST .../test` is a dry run: it validates the
    definition and resolves the trigger's references synchronously, creating no
    run and sending no email, and returns `{ workflowId, ok, problems }`
    directly. Real runs execute asynchronously and are polled via `GET
    /api/v3/workflows/runs/{runId}`.


    **Overview migration note**

    The v3-backed survey overview page intentionally removes actions that are
    not yet exposed by this contract: `Created by` filtering, `Duplicate`,
    `Copy...`, `Preview`, and `Copy link`.


    **Next steps (out of scope for this spec)**

    Additional v3 survey update endpoints, optional ETag/304, field selection,
    translation-specific writes, and survey version history.
  version: 0.1.0
  x-implementation-notes:
    route: apps/web/app/api/v3/surveys/route.ts
    generate-route: apps/web/app/api/v3/surveys/generate/route.ts
    query-parser: apps/web/app/api/v3/surveys/parse-v3-surveys-list-query.ts
    auth: apps/web/app/api/v3/lib/auth.ts
    workspace-resolution: apps/web/app/api/v3/lib/workspace-context.ts
    workflow-contract-ticket: ENG-1101
    workflow-shared-schemas: packages/workflows/src/types (feat/workflows-types)
    workflow-prisma-schema: packages/database/schema/workflows.prisma (feat/workflows-prisma-schema)
    openapi-generated: false
    spec-source: docs/api-v3-reference/src (regenerate bundle with `pnpm api:v3:bundle`)
    pagination-model: cursor
    cursor-pagination: supported
servers:
  - url: https://app.formbricks.com
    description: >-
      Formbricks Cloud. Paths in this spec already include the `/api/v3` prefix;
      self-hosted instances substitute their own origin.
security: []
paths:
  /api/v3/workflows/{workflowId}/test:
    post:
      tags:
        - V3 Workflows
      summary: Dry-run (validate) a workflow
      description: >
        Validates a workflow without running it or causing any side effect. It
        checks that the workflow's current definition is executable, that the
        trigger's referenced survey and ending cards still exist in the
        workspace, and that every `send_email` action addressing a *literal*
        email address targets someone who can access this workspace. **No**
        workflow run is created and **no** email or webhook is sent.


        The response is always `200` with `{ data: { workflowId, ok, problems }
        }`. `data.ok` is `true` when the workflow would execute; otherwise
        `data.problems` lists every issue found — each with a machine-readable
        `code` and the offending `field` — so they can be fixed in a single
        pass.


        Drafts are testable — validating the setup before going live is the
        point of a dry run. Only an `archived` workflow is rejected, with
        **422** and code `invalid_workflow_state`.
      operationId: testWorkflowV3
      parameters:
        - $ref: '#/components/parameters/WorkflowIdPath'
      responses:
        '200':
          description: >
            Validation result. The request succeeded; inspect `data.ok` to see
            whether the workflow is ready, and `data.problems` for any issues.
          headers:
            X-Request-Id:
              schema:
                type: string
              description: Request correlation ID
            Cache-Control:
              schema:
                type: string
              example: private, no-store
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/WorkflowTestResult'
              examples:
                ok:
                  summary: Workflow is valid and ready to enable
                  value:
                    data:
                      workflowId: clworkflow0000000000000000
                      ok: true
                      problems: []
                problems:
                  summary: Workflow has unresolved trigger references
                  value:
                    data:
                      workflowId: clworkflow0000000000000000
                      ok: false
                      problems:
                        - code: survey_not_found
                          field: definition.trigger.config.surveyId
                          message: >-
                            The referenced survey does not exist in this
                            workspace.
                        - code: ending_card_not_found
                          field: definition.trigger.config.endingCardIds
                          message: >-
                            Ending card clending000000000000000000 does not
                            exist on the survey.
                recipientNotAllowed:
                  summary: >-
                    A send_email action addresses a literal recipient without
                    workspace access
                  value:
                    data:
                      workflowId: clworkflow0000000000000000
                      ok: false
                      problems:
                        - code: recipient_not_allowed
                          field: definition.nodes.config.to
                          message: >-
                            Recipient outsider@external.example cannot access
                            this workspace. A send_email action may only address
                            someone with access to this workspace or a
                            respondent field.
        '401':
          $ref: '#/components/responses/V3Unauthorized'
        '403':
          $ref: '#/components/responses/V3Forbidden'
        '422':
          $ref: '#/components/responses/V3UnprocessableContent'
        '429':
          $ref: '#/components/responses/V3TooManyRequests'
        '500':
          $ref: '#/components/responses/V3InternalServerError'
      security:
        - sessionAuth: []
        - apiKeyAuth: []
components:
  parameters:
    WorkflowIdPath:
      in: path
      name: workflowId
      required: true
      schema:
        type: string
        format: cuid2
      description: Workflow identifier.
  schemas:
    WorkflowTestResult:
      type: object
      description: >
        Result of dry-running (testing) a workflow: whether it would execute
        and, if not, why not. No run is created and no side effects occur.
      required:
        - workflowId
        - ok
        - problems
      properties:
        workflowId:
          type: string
          format: cuid2
          example: clworkflow0000000000000000
        ok:
          type: boolean
          description: >
            True when the definition is executable, every trigger reference
            resolves, and every literal `send_email` recipient can access this
            workspace.
        problems:
          type: array
          description: >-
            Every problem found, so all issues can be fixed at once. Empty when
            `ok` is true.
          items:
            type: object
            required:
              - code
              - field
              - message
            properties:
              code:
                type: string
                enum:
                  - definition_not_executable
                  - survey_not_found
                  - ending_card_not_found
                  - recipient_not_allowed
                description: Machine-readable problem category.
              field:
                type: string
                description: >
                  Dotted path to the offending field, e.g.
                  `definition.trigger.config.surveyId`.
              message:
                type: string
                description: Human-readable explanation of the problem.
    Problem:
      type: object
      description: >-
        RFC 9457 Problem Details for HTTP APIs (`application/problem+json`).
        Responses typically include a machine-readable `code` field alongside
        `title`, `status`, `detail`, and `requestId`.
      required:
        - title
        - status
        - detail
        - requestId
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
          enum:
            - ai_features_not_enabled
            - ai_generated_payload_invalid
            - ai_instance_not_configured
            - ai_output_too_long
            - ai_smart_tools_disabled
            - bad_gateway
            - bad_request
            - conflict
            - forbidden
            - internal_server_error
            - invalid_workflow_state
            - not_authenticated
            - not_found
            - payload_too_large
            - service_unavailable
            - too_many_requests
            - unprocessable_content
            - workflow_not_executable
        requestId:
          type: string
        details:
          type: object
        invalid_params:
          type: array
          items:
            $ref: '#/components/schemas/InvalidParam'
    InvalidParam:
      type: object
      description: >
        Field-level validation error. `name` and `reason` are always intended
        for humans.

        Optional machine-readable fields are included when the API can identify
        the semantic error,

        especially for survey locale, identifier, and reference validation used
        by MCP and agent clients.
      required:
        - name
        - reason
      properties:
        name:
          type: string
          description: Dot-path to the invalid request field.
        reason:
          type: string
          description: Human-readable repair guidance.
        code:
          type: string
          enum:
            - dangling_reference
            - duplicate_identifier
            - duplicate_locale
            - forbidden_identifier
            - immutable_identifier
            - invalid_graph
            - invalid_locale
            - invalid_reference
            - missing_required_field
            - missing_translation
            - unsupported_field
            - unsupported_locale
          description: >
            Machine-readable validation category, when available.
            `invalid_graph` covers workflow

            graph-structure violations such as a duplicated trigger edge, an
            unreachable node, or a

            cycle.
        identifier:
          type: string
          description: >-
            Identifier involved in the validation failure, when available. For
            language errors, this can be the normalized language code.
        referenceType:
          type: string
          enum:
            - block
            - element
            - ending
            - hiddenField
            - language
            - variable
            - variableName
            - recall
            - node
            - edge
            - survey
            - endingCard
          description: >-
            Identifier namespace involved in the validation failure. `node`,
            `edge`, `survey`, and `endingCard` are used by workflow validation.
        missingId:
          type: string
          description: Referenced id that does not exist in the submitted survey document.
        firstUsedAt:
          type: string
          description: Dot-path where a duplicate identifier was first used.
        conflictsWith:
          type: string
          description: Dot-path to the existing identifier that conflicts with this field.
      additionalProperties: false
  responses:
    V3Unauthorized:
      description: Not authenticated (no valid session or API key).
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3Forbidden:
      description: >-
        Forbidden — no workspace access, or resource does not exist (404 not
        used; avoids existence leak).
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3UnprocessableContent:
      description: >-
        Unprocessable Content — semantic validation failed or the requested
        lifecycle transition is invalid.
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3TooManyRequests:
      description: Rate limit exceeded.
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
        Retry-After:
          schema:
            type: integer
          description: Seconds until the current rate-limit window resets.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    V3InternalServerError:
      description: Internal Server Error.
      headers:
        X-Request-Id:
          schema:
            type: string
          description: Request correlation ID
        Cache-Control:
          schema:
            type: string
          example: private, no-store
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  securitySchemes:
    sessionAuth:
      type: apiKey
      in: cookie
      name: next-auth.session-token
      description: >
        NextAuth session JWT cookie. **Development:** often
        `next-auth.session-token`.

        **Production (HTTPS):** often `__Secure-next-auth.session-token`. Send
        the cookie your browser receives after sign-in.
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >
        Management API key; must include **workspaceId** as an allowed workspace
        with read, write, or manage permission.

````