---
title: "Create a task"
method: POST
path: "/v1/projects/{project_id}/tasks"
tags: ["Tasks"]
---

# Create a task

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

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/tasks`
- 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 task bound to a workflow. By default the task is placed in the workflow's initial state; passing `state` places it directly in that named state instead — an alternate entry point for starting a task mid-flow (e.g. "a new recorte for an existing theme by id"), rather than re-submitting from the initial state and hoping a guard or similarity gate recognizes it. Entering the resulting state, initial or named, behaves identically: that state's `on_enter` automation fires and its `stalled_after` clock arms.

## Headers

- `Idempotency-Key` string

## Request body

- CreateTaskRequest
  - `workflow_id` string, required
  - `title` string, required
  - `payload` object
  - `assignee` string, nullable
  - `idempotency_key` string: The create's deduplication key, unique within the project and claimed for as long as the task it created exists. The same key with the same body returns that task as it is now (`200`) instead of creating a second one; with a different body it is `409 IDEMPOTENCY_KEY_REUSED`. A create that fails claims no key. Equivalent to the `Idempotency-Key` header.
  - `state` string: Name of a declared workflow state to create the task in directly, instead of the workflow's `initial` state. Must name a state declared on the workflow, or the request is rejected with `TASK_STATE_NOT_FOUND` (400). Defaults to the `initial` state.
  - `tool_context` object: Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and `mcp` tool call made by this task's automation dispatches — the agent generations a state's `on_enter` starts, and the agent nodes of any orchestration run it starts. The header name is `X-Naturali-Context-` plus the key verbatim; no character is re-cased. Creation is the task's first move, so this is the bag the entry state's `on_enter` runs with. Each transition may replace it (see `TransitionTaskRequest.tool_context`). The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped in any casing and re-derived server-side, so a task-dispatched generation cannot forge them. A key outside the HTTP header-name grammar is rejected with `INVALID_TOOL_CONTEXT_KEY` (400). Write-only: the stored bag is never returned by any task read, and it is cleared when the task reaches a terminal state.
  - `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.

## Response `200`

The idempotency key already names a task created by the same request; that task is returned as it is now and no second task is created

- 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

- `201`: Task created
- `400`: Bad request — invalid payload (`TASK_PAYLOAD_INVALID`), `state` does not name a declared state of the workflow (`TASK_STATE_NOT_FOUND`), or an invalid idempotency key (`VALIDATION_FAILED`)
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Workflow not found
- `409`: The idempotency key names a task created by a different request (`IDEMPOTENCY_KEY_REUSED`)

---

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