---
title: "Migrate a task to a newer workflow version"
method: POST
path: "/v1/projects/{project_id}/tasks/{task_id}/migrate"
tags: ["Tasks"]
---

# Migrate a task to a newer workflow version

`POST /v1/projects/{project_id}/tasks/{task_id}/migrate`

## Request

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

Moves an open task onto a newer version of its workflow. A state the target version also declares maps to itself; `state_map` maps a state it lacks. The payload is re-validated against the target `payload_schema`. The mapped state's `on_enter` is not fired: transition the task when you want the target's automation. A task with a dispatch in flight or a transition pending approval is refused, since both belong to the version they started under. Recorded in history as a row with `transition: null` and both versions.

## Path parameters

- `task_id` string, required

## Request body

- MigrateTaskRequest
  - `to_version` integer, required: The workflow version to move the task onto; newer than its current one.
  - `state_map` object: Current state name to a state the target version declares. Required only for a state the target lacks; an entry for a state both declare overrides the identity mapping.

## Response `200`

The task on the target version

- Task
  - `id` string
  - `project_id` string
  - `workflow_id` string
  - `workflow_version` integer, nullable: The workflow version this task runs on, fixed when the task was created. Transitions, approval gates and payload validation all resolve through it, so editing the workflow never re-shapes a task already in flight. `null` for tasks created before pinning existed, which run on the live definition.
  - `title` string
  - `state` string
  - `status` 'open' | 'closed'
  - `payload` object: Caller-owned task data; input to guards (as `task.payload`) and dispatch mappings. The engine never writes into it except the workflow's declared `payload_writes`.
  - `metadata` MetadataBag: Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase. No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.
  - `last_result` unknown
  - `assignee` string, nullable
  - `active_dispatch` object, nullable: { kind, id, status } of the current state's dispatch, if any. `kind` is `generation`, `orchestration_run` or `tool_call`; a `tool_call` always carries a null `id`, since a direct tool call leaves no addressable record. Carries an additional `attempt` (1-based) while the state's `on_enter.retry` policy is in effect.
  - `automation_status` 'running' | 'completed' | 'failed' | 'unrouted' | 'paused' | 'null', nullable: Status of the current state's dispatch. `null` until a state with an automation is entered. `paused` means an operator pause suppressed this state's `on_enter` before it ran, so resume-task will start it.
  - `pause_requested_at` string, date-time, nullable: When an operator paused this task's automation, or null when no pause is in force.
  - `pause_reason` string, nullable: The reason supplied with the pause, when one was.
  - `automation_chain_depth` integer: Server-owned. How many machine-driven transitions have run back-to-back with no outside intervention — a dispatch outcome routed through `on_complete`/`on_failure`, or a `transition-task` call made by a dispatched run or agent with its run-as token. Any move by a person, a plain API key, or an approval resolution resets it to `0`. Once it would exceed the server's limit (`TASK_AUTOMATION_CHAIN_LIMIT`, default 50) the next such transition is refused with `TASK_AUTOMATION_CHAIN_LIMIT`, bounding a cycle composed across workflows and orchestrations.
  - `pending_transition` string, nullable: The name of a `requires_approval` transition parked awaiting a human decision. Non-null while an ApprovalItem gates the move; the task stays in its current state and no other transition may fire until the approval resolves.
  - `entered_state_at` string, date-time
  - `created_at` string, date-time
  - `updated_at` string, date-time

## Other responses

- `400`: The version does not exist or is not newer than the task's, the current state is unmapped, a mapped state is undeclared or terminal, or the payload fails the target `payload_schema`
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Task not found
- `409`: The task is closed, has a transition pending approval, or has a dispatch in flight

---

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