---
title: "Create a workflow"
method: POST
path: "/v1/projects/{project_id}/workflows"
tags: ["Workflows"]
---

# Create a workflow

`POST /v1/projects/{project_id}/workflows`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/workflows`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Creates a new workflow definition. The definition is statically validated.

## Request body

- CreateWorkflowRequest
  - `name` string, required
  - `description` string, nullable
  - `states` WorkflowState[], required
    - `name` string, required
    - `initial` boolean
    - `terminal` boolean
    - `kind` string: `human` marks a human-in-the-loop parking state: the state never dispatches (declaring `on_enter` on it is rejected at validation) and the task parks until a principal fires a transition.
    - `stalled_after` integer, nullable: Seconds a task may sit in this state before the stall sweeper emits a `tasks.stalled` event (once per stall episode, re-armed on the next transition). Must be a positive integer when set. Omit or null to never stall. The event does not move the task — route on it with a webhook/trigger.
    - `on_enter` object, nullable: What entering the state dispatches, and how the result routes. Omitted or null on a state that only parks.
      - `dispatch` object: The one thing entering the state runs. `kind` picks it and names the id it needs: `agent` → `agent_id`, `orchestration` → `orchestration_id`, `tool` → `tool_id` (with an optional `operation_id`).
        - `kind` 'agent' | 'orchestration' | 'tool': Which target is dispatched.
        - `agent_id` string, nullable: The agent a `kind: agent` dispatch generates with.
        - `orchestration_id` string, nullable: The orchestration a `kind: orchestration` dispatch starts a run of.
        - `tool_id` string, nullable: The tool a `kind: tool` dispatch calls.
        - `operation_id` string, nullable: Which operation of a multi-operation tool to invoke — the same selector an orchestration `tool` node takes.
        - `input_mapping` object, nullable: JSON Logic building the dispatch input from the task. Carried verbatim.
        - `payload_writes` object, nullable: JSON Logic expressions over `{task, result}` whose values are written into `task.payload` when the dispatch completes, one named channel per key. Carried verbatim.
      - `retry` object, nullable: Re-runs the dispatch's execution failures before `on_failure` applies. Omitted means a single attempt.
        - `max_attempts` integer: Attempts to make, 1–10.
        - `backoff_seconds` number, nullable: Delay before the second attempt.
        - `backoff_multiplier` number, nullable: Factor the delay grows by per further attempt: `backoff_seconds * backoff_multiplier^(n - 2)`.
      - `on_complete` object[], nullable: Rules evaluated in order when the dispatch completes; the first whose `when` is truthy fires its transition.
        - `when` unknown
        - `transition` string: The transition to fire, by name.
      - `on_failure` string, nullable: Transition fired when the dispatch fails after its last attempt. Omitted parks the task with `automation_status: failed`.
  - `transitions` WorkflowTransition[], required
    - `name` string, required
    - `from` string[], required
    - `to` string, required
    - `guard` object, nullable
    - `requires_approval` boolean: Gate the transition behind a human approval. When `true`, firing the transition (by anyone other than the approval resolution itself) parks a pending `ApprovalItem` instead of moving the task; the task exposes `pending_transition` until the item resolves. Approval fires the transition as the `approval` principal (its guard re-evaluated at resolution time); rejection or expiry clears the gate and appends a history note.
  - `payload_schema` object, nullable
  - `version_label` string: Optional tag for the version this create archives, e.g. `initial`.

## Response `201`

Workflow created

- Workflow
  - `id` string
  - `project_id` string
  - `name` string
  - `description` string, nullable
  - `version` integer: Incremented on every write that changes the state machine; prior versions are archived. A task pins the version it entered on, so these fields are a draft for tasks created from now on rather than a live rewrite of the ones already in flight.
  - `states` WorkflowState[]
    - `name` string, required
    - `initial` boolean
    - `terminal` boolean
    - `kind` string: `human` marks a human-in-the-loop parking state: the state never dispatches (declaring `on_enter` on it is rejected at validation) and the task parks until a principal fires a transition.
    - `stalled_after` integer, nullable: Seconds a task may sit in this state before the stall sweeper emits a `tasks.stalled` event (once per stall episode, re-armed on the next transition). Must be a positive integer when set. Omit or null to never stall. The event does not move the task — route on it with a webhook/trigger.
    - `on_enter` object, nullable: What entering the state dispatches, and how the result routes. Omitted or null on a state that only parks.
      - `dispatch` object: The one thing entering the state runs. `kind` picks it and names the id it needs: `agent` → `agent_id`, `orchestration` → `orchestration_id`, `tool` → `tool_id` (with an optional `operation_id`).
        - `kind` 'agent' | 'orchestration' | 'tool': Which target is dispatched.
        - `agent_id` string, nullable: The agent a `kind: agent` dispatch generates with.
        - `orchestration_id` string, nullable: The orchestration a `kind: orchestration` dispatch starts a run of.
        - `tool_id` string, nullable: The tool a `kind: tool` dispatch calls.
        - `operation_id` string, nullable: Which operation of a multi-operation tool to invoke — the same selector an orchestration `tool` node takes.
        - `input_mapping` object, nullable: JSON Logic building the dispatch input from the task. Carried verbatim.
        - `payload_writes` object, nullable: JSON Logic expressions over `{task, result}` whose values are written into `task.payload` when the dispatch completes, one named channel per key. Carried verbatim.
      - `retry` object, nullable: Re-runs the dispatch's execution failures before `on_failure` applies. Omitted means a single attempt.
        - `max_attempts` integer: Attempts to make, 1–10.
        - `backoff_seconds` number, nullable: Delay before the second attempt.
        - `backoff_multiplier` number, nullable: Factor the delay grows by per further attempt: `backoff_seconds * backoff_multiplier^(n - 2)`.
      - `on_complete` object[], nullable: Rules evaluated in order when the dispatch completes; the first whose `when` is truthy fires its transition.
        - `when` unknown
        - `transition` string: The transition to fire, by name.
      - `on_failure` string, nullable: Transition fired when the dispatch fails after its last attempt. Omitted parks the task with `automation_status: failed`.
  - `transitions` WorkflowTransition[]
    - `name` string, required
    - `from` string[], required
    - `to` string, required
    - `guard` object, nullable
    - `requires_approval` boolean: Gate the transition behind a human approval. When `true`, firing the transition (by anyone other than the approval resolution itself) parks a pending `ApprovalItem` instead of moving the task; the task exposes `pending_transition` until the item resolves. Approval fires the transition as the `approval` principal (its guard re-evaluated at resolution time); rejection or expiry clears the gate and appends a history note.
  - `payload_schema` object, nullable
  - `created_at` string, date-time
  - `updated_at` string, date-time

## Other responses

- `400`: Bad request (invalid definition)
- `401`: Unauthorized
- `403`: Forbidden
- `409`: A workflow with this name already exists

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
