---
title: "Create a workflow"
method: POST
path: "/api/v3/workflows"
tags: ["V3 Workflows"]
---

# Create a workflow

`POST /api/v3/workflows`

Creates a `draft` workflow. The workflow definition is the public source of truth used by
the dashboard and external clients, following the document-first pattern established by the
v3 Survey API. Runtime persistence, revision creation, and version snapshotting are
implementation details behind this contract.

Workflows are always created as drafts; use `POST /api/v3/workflows/{workflowId}/enable` to
make them respond to trigger events. Graph reference failures (duplicate node ids, edges
pointing at missing nodes, `entryNodeId` not referencing the trigger, unknown `surveyId` or
`endingCardIds` in the workspace) are rejected with **400** and structured `invalid_params`.

## Request body

- CreateWorkflowRequest
  - `workspaceId` string, cuid2, required
  - `name` string, required
  - `description` string, nullable
  - `status` 'draft' — New workflows are created as drafts in Scope 1.
  - `definition` WorkflowDefinition, required — Scope 1 workflow JSON document, mirroring `ZWorkflowDefinition` in `packages/workflows`. The definition is the public source of truth for the builder and API clients. Scope 1 intentionally excludes user-authored if/else branches, webhook actions, schedules, delays, loops, and AI/MCP-assisted creation; future node types extend `nodes` additively. Graph rules enforced on every write (**400** with `invalid_params` on violation): node ids are unique across `trigger` and `nodes`; every edge references existing node ids; the trigger has at most one outgoing edge; `entryNodeId` equals `trigger.id`. Trigger-only drafts (no `nodes`, no `edges`) are valid persisted documents. Executability rules (exactly one outgoing trigger edge, acyclic graph, every node reachable from the trigger) are additionally enforced by `enable` and `test` with **422** `workflow_not_executable`.
    - `schemaVersion` 1 — Definition schema version. Optional on writes (defaults to the current version); always present in responses.
    - `trigger` WorkflowResponseCompletedTriggerNode, required
      - `id` string, required
      - `type` 'trigger', required
      - `triggerType` 'response.completed', required
      - `label` string — Optional display label shown in the builder.
      - `config` object, required
        - `surveyId` string, cuid2, required — Survey whose completed responses trigger the workflow. Must exist in the workspace.
        - `endingCardIds` string[] — Ending card ids that should trigger the workflow. Empty or omitted means every ending matches.
      - `ui` WorkflowNodeUi — Builder-only UI metadata that does not affect workflow execution. Additional keys are preserved.
        - `position` object
          - `x` number, required
          - `y` number, required
        - `collapsed` boolean
    - `nodes` WorkflowSendEmailActionNode[] — Child nodes of the graph. The trigger lives in `trigger`, not in this array. Scope 1 accepts `send_email` action nodes only. Optional on writes (defaults to empty for trigger-only drafts); always present in responses.
      - `id` string, required
      - `type` 'action', required
      - `actionType` 'send_email', required
      - `label` string — Optional display label shown in the builder.
      - `config` WorkflowSendEmailActionConfig, required — Send-email action with 1:1 field parity to survey Follow-ups (`ZSurveyFollowUpAction`). `to`, `subject` and `body` accept an empty string on create and update, so an author can save an unfinished draft. Completeness is enforced when the workflow has to run: enabling it returns **422** `workflow_not_executable` naming each blank field, and the dry run reports them as problems.
        - `to` string, required — Recipient: either a literal email address (e.g. a teammate) or the element id of a survey question / hidden field whose answer contains the respondent's email address (contact-info elements resolve index [2]) — the same semantics as the Follow-ups `to` field. A **literal** address must belong to someone who can access this workspace — an organization owner or manager, or a member of a team the workspace is shared with. Enabling a workflow whose literal recipient cannot access the workspace returns **422** `workflow_not_executable`, the dry run reports a `recipient_not_allowed` problem, and the runner refuses to send to it — so a workflow cannot forward response data to an arbitrary external inbox. An element-id `to` resolves to the respondent's own address and is never allowlist-checked.
        - `from` string, email, required
        - `replyTo` string[], required
        - `subject` string, required — Email subject. Used verbatim (recall tokens are not expanded in the subject), the same as Follow-ups. Capped at RFC 5322's maximum line length.
        - `body` string, required — Email body as HTML with recall tokens (`#recall:[elementId]/fallback:x#`). Recall tokens are expanded against the response, the result is sanitized to a narrow HTML allowlist, and it is wrapped in the branded Follow-ups email template — identical rendering to survey Follow-ups.
        - `attachResponseData` boolean, required
        - `includeVariables` boolean
        - `includeHiddenFields` boolean
      - `ui` WorkflowNodeUi — Builder-only UI metadata that does not affect workflow execution. Additional keys are preserved.
        - `position` object
          - `x` number, required
          - `y` number, required
        - `collapsed` boolean
    - `edges` WorkflowEdge[] — Optional on writes (defaults to empty for trigger-only drafts); always present in responses.
      - `id` string, required
      - `source` string, required — Node id the edge starts from.
      - `target` string, required — Node id the edge points to.
      - `sourceHandle` string — Builder handle identifier on the source node. Reserved for branch nodes (`then`/`else`) in future scopes.
      - `targetHandle` string — Builder handle identifier on the target node.
    - `entryNodeId` string, required — Workflow entry point. Must equal `trigger.id`.

## Response `201`

Workflow created successfully

- object
  - `data` WorkflowResource, required — Slim workflow shape returned by `GET /api/v3/workflows`. `triggerType` and `surveyId` are derived read-only projections of `definition.trigger`, so list consumers can render and filter without loading full definitions. Serialized dates are ISO 8601 strings.
    - `id` string, cuid2, required
    - `workspaceId` string, cuid2, required
    - `name` string, required
    - `description` string, nullable, required
    - `status` 'draft' | 'enabled' | 'disabled' | 'archived', required — Workflow lifecycle state. `draft` workflows are editable and inert. `enabled` workflows respond to trigger events. `disabled` workflows keep their configuration but are inert. `archived` workflows are soft-deleted: read-only, excluded from default list reads, and restorable via unarchive. Status only changes through lifecycle endpoints. Valid transitions: `draft` → `enabled`, `enabled` ↔ `disabled`, `enabled`/`disabled` → `draft`, any non-archived state → `archived` (archive), and `archived` → `draft` (unarchive).
    - `triggerType` 'response.completed', required — Derived from `definition.trigger.triggerType`.
    - `surveyId` string, cuid2, required — Derived from `definition.trigger.config.surveyId`.
    - `createdBy` string, cuid2, nullable, required — User id that created the workflow. `null` when the creating user was deleted.
    - `creator` object, nullable, required — Creating user's name. `null` when the creating user was deleted.
      - `name` string, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
    - `lastRun` WorkflowRunSummary, required — Slim run shape embedded as `lastRun` in workflow resources. The runs list (`GET /api/v3/workflows/runs`) returns `WorkflowRunListItem`, which extends this with `workflowName`. Excludes `triggerPayload`, `data`, and `logs`.
      - `id` string, cuid2, required
      - `workflowId` string, cuid2, required
      - `workspaceId` string, cuid2, required
      - `workflowVersionId` string, cuid2, nullable, required — Immutable workflow version snapshot the run executes against. `null` for dry runs of workflows that have never been enabled. Version contents are not readable through the v3 API in Scope 1.
      - `status` 'queued' | 'running' | 'completed' | 'failed' | 'canceled', required — Workflow run lifecycle state. `completed`, `failed`, and `canceled` are terminal. Valid transitions: `queued` → `running`, `running` → `queued` (retry/backoff), `queued` → `canceled`, and `running` → `completed`/`failed`/`canceled`. Retries do not introduce a separate status; retry state is exposed via `attempt` and `nextAttemptAt`.
      - `isDryRun` boolean, required — `true` for runs created by `POST /api/v3/workflows/{workflowId}/test`.
      - `triggerType` 'response.completed', required
      - `surveyId` string, cuid2, nullable, required
      - `responseId` string, cuid2, nullable, required — Survey response that triggered the run. `null` for synthesized dry-run data or when the response was deleted.
      - `error` string, nullable, required — Terminal or most recent failure reason. `null` while the run is healthy.
      - `attempt` integer, required — Retry attempt counter. `0` on the first execution; retries increment it without changing `status`.
      - `createdAt` string, date-time, required
      - `updatedAt` string, date-time, required
      - `startedAt` string, date-time, nullable, required
      - `finishedAt` string, date-time, nullable, required
    - `runCount` integer, required — Total number of runs (dry runs included) for this workflow.
    - `definition` WorkflowDefinition, required — Scope 1 workflow JSON document, mirroring `ZWorkflowDefinition` in `packages/workflows`. The definition is the public source of truth for the builder and API clients. Scope 1 intentionally excludes user-authored if/else branches, webhook actions, schedules, delays, loops, and AI/MCP-assisted creation; future node types extend `nodes` additively. Graph rules enforced on every write (**400** with `invalid_params` on violation): node ids are unique across `trigger` and `nodes`; every edge references existing node ids; the trigger has at most one outgoing edge; `entryNodeId` equals `trigger.id`. Trigger-only drafts (no `nodes`, no `edges`) are valid persisted documents. Executability rules (exactly one outgoing trigger edge, acyclic graph, every node reachable from the trigger) are additionally enforced by `enable` and `test` with **422** `workflow_not_executable`.
      - `schemaVersion` 1 — Definition schema version. Optional on writes (defaults to the current version); always present in responses.
      - `trigger` WorkflowResponseCompletedTriggerNode, required
        - `id` string, required
        - `type` 'trigger', required
        - `triggerType` 'response.completed', required
        - `label` string — Optional display label shown in the builder.
        - `config` object, required
          - `surveyId` string, cuid2, required — Survey whose completed responses trigger the workflow. Must exist in the workspace.
          - `endingCardIds` string[] — Ending card ids that should trigger the workflow. Empty or omitted means every ending matches.
        - `ui` WorkflowNodeUi — Builder-only UI metadata that does not affect workflow execution. Additional keys are preserved.
          - `position` object
            - `x` number, required
            - `y` number, required
          - `collapsed` boolean
      - `nodes` WorkflowSendEmailActionNode[] — Child nodes of the graph. The trigger lives in `trigger`, not in this array. Scope 1 accepts `send_email` action nodes only. Optional on writes (defaults to empty for trigger-only drafts); always present in responses.
        - `id` string, required
        - `type` 'action', required
        - `actionType` 'send_email', required
        - `label` string — Optional display label shown in the builder.
        - `config` WorkflowSendEmailActionConfig, required — Send-email action with 1:1 field parity to survey Follow-ups (`ZSurveyFollowUpAction`). `to`, `subject` and `body` accept an empty string on create and update, so an author can save an unfinished draft. Completeness is enforced when the workflow has to run: enabling it returns **422** `workflow_not_executable` naming each blank field, and the dry run reports them as problems.
          - `to` string, required — Recipient: either a literal email address (e.g. a teammate) or the element id of a survey question / hidden field whose answer contains the respondent's email address (contact-info elements resolve index [2]) — the same semantics as the Follow-ups `to` field. A **literal** address must belong to someone who can access this workspace — an organization owner or manager, or a member of a team the workspace is shared with. Enabling a workflow whose literal recipient cannot access the workspace returns **422** `workflow_not_executable`, the dry run reports a `recipient_not_allowed` problem, and the runner refuses to send to it — so a workflow cannot forward response data to an arbitrary external inbox. An element-id `to` resolves to the respondent's own address and is never allowlist-checked.
          - `from` string, email, required
          - `replyTo` string[], required
          - `subject` string, required — Email subject. Used verbatim (recall tokens are not expanded in the subject), the same as Follow-ups. Capped at RFC 5322's maximum line length.
          - `body` string, required — Email body as HTML with recall tokens (`#recall:[elementId]/fallback:x#`). Recall tokens are expanded against the response, the result is sanitized to a narrow HTML allowlist, and it is wrapped in the branded Follow-ups email template — identical rendering to survey Follow-ups.
          - `attachResponseData` boolean, required
          - `includeVariables` boolean
          - `includeHiddenFields` boolean
        - `ui` WorkflowNodeUi — Builder-only UI metadata that does not affect workflow execution. Additional keys are preserved.
          - `position` object
            - `x` number, required
            - `y` number, required
          - `collapsed` boolean
      - `edges` WorkflowEdge[] — Optional on writes (defaults to empty for trigger-only drafts); always present in responses.
        - `id` string, required
        - `source` string, required — Node id the edge starts from.
        - `target` string, required — Node id the edge points to.
        - `sourceHandle` string — Builder handle identifier on the source node. Reserved for branch nodes (`then`/`else`) in future scopes.
        - `targetHandle` string — Builder handle identifier on the target node.
      - `entryNodeId` string, required — Workflow entry point. Must equal `trigger.id`.

## Other responses

- `400` — Bad Request — malformed JSON, invalid query/body/params, duplicate name, or unsupported field.
- `401` — Not authenticated (no valid session or API key).
- `403` — Forbidden — no workspace access, or resource does not exist (404 not used; avoids existence leak).
- `422` — Unprocessable Content — semantic validation failed or the requested lifecycle transition is invalid.
- `429` — Rate limit exceeded.
- `500` — Internal Server Error.

---

[API](https://skmtc.dev/formbricks/apis/formbricks-api-v3.md) · [All operations](https://skmtc.dev/formbricks/apis/formbricks-api-v3/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/formbricks/formbricks-api-v3/revisions/3c2c022e49d2/schema)
