---
title: "Start an orchestration run"
method: POST
path: "/v1/projects/{project_id}/orchestration-runs"
tags: ["Orchestrations"]
---

# Start an orchestration run

`POST /v1/projects/{project_id}/orchestration-runs`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/orchestration-runs`
- 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 run for the orchestration named by orchestration_id. By default the run executes durably in the background: the response returns immediately with status "queued" (a worker then claims it and moves it to "running") and progress is observed via get-orchestration-run or run lifecycle webhook events (orchestration_runs.started/awaiting_input/completed/failed). Delay and poll waits park the run as "sleeping" and are woken by a background scheduler, surviving restarts. Pass wait=true to block until the run reaches a terminal or awaiting_input state.

## Headers

- `Idempotency-Key` string

## Request body

- StartOrchestrationRunRequest
  - `orchestration_id` string, required: Orchestration to run (orch_...).
  - `input` object: Initial state for the run (merged with orchestration defaults).
  - `tool_context` object: Key-value pairs forwarded as `X-Naturali-Context-<key>` headers on every `http` and `mcp` tool call made by an agent node of this run — including the agents of any child run a `loop` or `sub_orchestration` node starts. The header name is `X-Naturali-Context-` plus the key verbatim; no character is re-cased. The bag is stored on the run and re-read on every step, so it survives an `awaiting_input` pause, a `sleeping` wait, a background worker drive and a crash redrive. It is **write-only**: a run is a record every principal who may read runs can read, and a credential in it is not theirs to see, so it is never returned on one. It also does not outlive the run: reaching a terminal status (`completed`, `failed`, `cancelled` or `expired`) clears it. A key that is not a valid HTTP header name, or two keys that map to the same header, are rejected with `400 INVALID_TOOL_CONTEXT_KEY` and no run is created. The reserved identity keys (`session_id`, `actor_id`, `actor_external_id`) are stripped at generation time — a caller cannot address them from here.
  - `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.
  - `idempotency_key` string: Deduplication key, unique within the project, that makes a retry of an ambiguous failure safe. The first request under a key starts the run and answers `201`; any later request carrying the same key answers `200` with that same run, whatever state it has reached — including a retry that arrives while the original is still running. The key is claimed by the run record and stays claimed for as long as the record exists, so it never silently expires and lets a second run through. `orchestration_id`, `input`, `tool_context` and `metadata` are the request the key names: reusing a key with any of them changed is `409 IDEMPOTENCY_KEY_REUSED` rather than a replay of a run that does something else. `wait` is not part of that comparison — it says how the caller waits, not what the run is — so a retry may flip it.
  - `wait` boolean: When true, block until the run reaches a terminal (completed/failed) or awaiting_input state and return the settled run. When false (default), return immediately with status "queued" and execute the run in the background.

## Response `200`

Duplicate request — the run the `idempotency_key` already names is returned and no second run is started.

- OrchestrationRun
  - `id` string, required: Public ID (run_...).
  - `orchestration_id` string, required: Public ID of the parent orchestration.
  - `orchestration_version` integer, nullable: The orchestration version this run executes, fixed when the run started. Every later step of the run — the first drive, a wake from `sleeping`, a human or approval resume, a redrive after a crash — resolves the graph from this version, so editing the orchestration never re-shapes a run already in flight. Fetch the graph it names at `GET /v1/projects/{project_id}/orchestrations/{orchestration_id}/versions/{version}`. Null for runs created before pinning existed, which execute the live graph.
  - `project_id` string, required: Public ID of the owning project.
  - `status` 'queued' | 'running' | 'sleeping' | 'awaiting_input' | 'completed' | 'failed' | 'cancelled' | 'expired', required: Run lifecycle state. `queued` awaits a worker; `running` is actively executing; `sleeping` is parked on a delay/poll wait (no worker); `awaiting_input` is parked on a human node; `completed`/`failed`/ `cancelled` are terminal; `expired` is a wait that passed its deadline.
  - `state` object, required: Current accumulated state.
  - `active_nodes` string[], required: Node IDs currently active.
  - `artifacts` object, required: Map of node ID to output artifact.
  - `error` StoredError: Why a resource's work failed, in the shape of an error response's `error` without `hint` and `docs_url`. Every `error` field a resource records carries it, so `code` can be read without first checking the type of `error`.
    - `code` string, required: A key from the server's ERROR_CODES registry, documented on the Error Codes page. `INTERNAL_ERROR`, with the message `Internal Server Error`, records an unexpected failure whose detail reaches the server log only.
    - `message` string, required
    - `meta` object: The failure's own structured context, when it names any.
  - `trace_id` string, nullable
  - `input` object, nullable: Initial input provided at run creation.
  - `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.
  - `idempotency_key` string, nullable: The deduplication key the run was started under, unique within the project and claimed for as long as the run record exists. Null for a run started without one, which every platform-started run is.
  - `parent_orchestration_run_id` string, nullable: The run whose node started this one — set only on a child a `loop` or `sub_orchestration` node spawned, null for a run a caller started. A child is its own run with its own usage events, so this is what makes a delegated run's spend attributable to the run that ordered it.
  - `parent_node_id` string, nullable: The node within `parent_orchestration_run_id` that started this run. Null when `parent_orchestration_run_id` is null.
  - `orchestration_run_depth` integer: `loop` / `sub_orchestration` edges between this run and the run a caller started: `0` for a caller-started run, one more than its parent's for a child. Starting a child past the effective bound — the smaller of the deployment's `MAX_ORCHESTRATION_RUN_DEPTH` (default 10) and the project's `max_orchestration_run_depth` — is refused with `ORCHESTRATION_RUN_DEPTH_LIMIT`, which fails the run that tried to descend. That bounds a graph whose `sub_orchestration` node names itself, directly or through a cycle of two graphs, which the intra-graph cycle check cannot see.
  - `output` object, nullable: What a completed run returns: the orchestration's `output_mapping` evaluated over the final state, or, when it declares none, the terminal node artifacts keyed by node id. Null unless the run completed.
  - `usage` UsageTotals: Token counts and cost for one slice of the meter. Tokens and cost only: a `compute_second` or `gb_day` meter is reported by the aggregate's `components` array. The three input dimensions partition the prompt — `input_tokens` = `uncached_input_tokens` + `cached_tokens` + `cache_write_tokens` — and are separate because they are separately priced. Note that the `input_tokens` **component** on a usage event is the uncached figure alone; here `input_tokens` is the whole prompt.
    - `cost_usd` number, nullable: Sum of priced component costs; null when nothing in the slice was priced. Always null at step altitude — the ledger prices one event per generation at write time, and a price read back per step would disagree with it after any price change.
    - `input_tokens` integer: Full prompt tokens, reconstructed from the components.
    - `uncached_input_tokens` integer: Prompt tokens priced at the plain input rate — the prompt minus cache reads and cache writes.
    - `output_tokens` integer
    - `cached_tokens` integer: Prompt tokens served from the provider's prompt cache.
    - `cache_write_tokens` integer: Prompt tokens written into the provider's prompt cache.
    - `reasoning_tokens` integer: A non-billable subset of `output_tokens`, reported where the provider breaks it out.
  - `usage_own` UsageTotals: Token counts and cost for one slice of the meter. Tokens and cost only: a `compute_second` or `gb_day` meter is reported by the aggregate's `components` array. The three input dimensions partition the prompt — `input_tokens` = `uncached_input_tokens` + `cached_tokens` + `cache_write_tokens` — and are separate because they are separately priced. Note that the `input_tokens` **component** on a usage event is the uncached figure alone; here `input_tokens` is the whole prompt.
    - `cost_usd` number, nullable: Sum of priced component costs; null when nothing in the slice was priced. Always null at step altitude — the ledger prices one event per generation at write time, and a price read back per step would disagree with it after any price change.
    - `input_tokens` integer: Full prompt tokens, reconstructed from the components.
    - `uncached_input_tokens` integer: Prompt tokens priced at the plain input rate — the prompt minus cache reads and cache writes.
    - `output_tokens` integer
    - `cached_tokens` integer: Prompt tokens served from the provider's prompt cache.
    - `cache_write_tokens` integer: Prompt tokens written into the provider's prompt cache.
    - `reasoning_tokens` integer: A non-billable subset of `output_tokens`, reported where the provider breaks it out.
  - `required_action` RequiredAction: Why an awaiting_input run is parked. `node_id`, `prompt` and `context` describe a node's own pause and are present for every kind but `paused`, which is an operator pause with no node of its own and carries `reason` instead.
    - `type` 'human_input' | 'webhook_receive' | 'approval' | 'paused', required: Discriminator identifying the kind of pause. Open enum — new pause kinds may be added in minor releases; clients must tolerate unknown values.
    - `node_id` string: The node the run is parked at. Absent for a `paused` action, which no node produced.
    - `prompt` string
    - `context` object
    - `reason` string, nullable: Present for `paused` — the reason the operator gave, or null when they gave none.
    - `options` string[], nullable
    - `approval_spec` object: Present only for `approval` pauses — the frozen tool proposal the engine emits as an ApprovalItem when the run parks. Copied as a value; inner keys stay exactly as authored.
    - `approval_id` string: Present once the approval item is emitted.
    - `expires_at` string, date-time: Present for `approval` pauses — when the item expires.
  - `pause_requested_at` string, date-time, nullable: When an operator pause was requested, or null when none is in force. Independent of `status`: a `running` run keeps running until its next checkpoint, and a run parked on a node keeps that node's `required_action`.
  - `pause_reason` string, nullable: The reason supplied with the pause, when one was.
  - `started_at` string, date-time, nullable
  - `completed_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `201`: Run created and executed
- `400`: Validation error (e.g. a `tool_context` key that cannot become a header, `metadata` that is not a JSON object, or an `idempotency_key` that is not a non-empty string of at most 255 characters). No run is created.
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Orchestration not found
- `409`: `IDEMPOTENCY_KEY_REUSED` — the key is already claimed by a run started from a different request. No run is created.

---

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