---
title: "Update a workflow"
method: PATCH
path: "/v1/projects/{project_id}/workflows/{workflow_id}"
tags: ["Workflows"]
---

# Update a workflow

`PATCH /v1/projects/{project_id}/workflows/{workflow_id}`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/workflows/{workflow_id}`
- 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`)

Updates a workflow definition. Structural changes (states/transitions) are re-validated. Existing tasks in a removed state stay put but can only leave via transitions valid in the new definition.

## Path parameters

- `workflow_id` string, required

## Headers

- `If-Match` string

## Request body

- UpdateWorkflowRequest
  - `name` string
  - `description` string, nullable
  - `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
  - `version_label` string: Optional tag for the version this write archives, e.g. `pre-rewire`. Ignored when the write changes no definition field, since no version is archived.
  - `expected_version` integer, nullable: The version the caller believes the resource holds. When the resource is at any other version the write is refused with `409 VERSION_CONFLICT` and nothing is written; `meta.current_version` on that response names the version in force. Omit it to write unconditionally. Omitting it does not make the write unordered: two writes that reach the server together are still serialized, and the one whose version was taken first is refused the same way. What the field adds is refusing a write whose author read the resource some time ago and has not seen what happened since. The `If-Match` header carries the same precondition for a client that prefers the HTTP spelling. Sending both with different versions is `400 VALIDATION_FAILED`.

## Response `200`

Workflow updated

- 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
- `404`: Workflow not found
- `409`: The resource has moved past the version this write read (`VERSION_CONFLICT`): a stated `expected_version` or `If-Match` no longer matches, or a concurrent write took the version first. `meta.current_version` names the version in force. Nothing is written.

---

[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)
