---
title: "Approve a MAP plan step (HITL parity with WCP)"
method: POST
path: "/api/v1/plans/{planId}/steps/{stepId}/approve"
tags: ["Multi-Agent Planning"]
---

# Approve a MAP plan step (HITL parity with WCP)

`POST /api/v1/plans/{planId}/steps/{stepId}/approve`

## Request

- Base URL: `https://orchestrator.getaxonflow.com`
- URL: `https://orchestrator.getaxonflow.com/api/v1/plans/{planId}/steps/{stepId}/approve`
- Auth: none declared
- TypeScript: <https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/docs/api/v1/plans/:planId/steps/:stepId/approve/post.ts> (a function with typed inputs; its response is checked against the schema)

Plan-scoped approval endpoint. Returns the same `ApprovalResponse`
shape as the WCP endpoint (`/api/v1/workflows/{id}/steps/{step_id}/approve`)
plus a `plan_id` field — see ADR-046 (HITL response parity).

A plan step is held only in MAP **confirm** / **step** mode, backed by
a WCP workflow: the handler delegates to the WCP service and projects
the full `retry_context`, approver metadata, and `policies_matched`. A
plan no workflow backs has no paused step, and answers 404. (The
in-memory pause the multi-agent routes used before v11.1.0 is retired,
#4249.)

## Path parameters

- `planId` string, required
- `stepId` string, required

## Request body

- object
  - `approved_by` string: Identity approving the step (overrides X-User-ID header)
  - `comment` string: Audit justification. Required on WCP-backed plans (min 10 chars); if shorter, the handler auto-fills a generated audit message.

## Response `200`

Step approved successfully

- ApprovalResponse: Rich response returned by the WCP `/approve` and `/reject` endpoints and by the MAP plan-scoped equivalents (`/api/v1/plans/{id}/steps/{step_id}/approve|reject`). Both planes project through the same helper — see ADR-046 (HITL response parity) and ADR-045 (retry_context wire contract). A refused approval answers the same status on both planes: 409 when the approval has expired (`approval_expired`), 503 when its state cannot be read (`approval_state_unreadable`). `decision` resolves to `allow` on a successful approval (the step can now proceed) or `block` on rejection (workflow aborted). `plan_id` is populated only on MAP-plane responses; on WCP-plane responses it is omitted. `retry_context` is always present and mirrors the StepGate `retry_context` shape.
  - `workflow_id` string: Underlying WCP workflow identifier
  - `plan_id` string: MAP plan id — present on MAP-plane responses. Omitted on WCP-plane responses (WCP has no plan concept).
  - `step_id` string: Step that was approved or rejected
  - `status` 'pending' | 'approved' | 'rejected' | 'expired': Flat string alias of `approval_status`. Both fields always carry the same value — `status` is convenient for loggers, dashboards, and clients that prefer a simple string; `approval_status` is the typed source of truth. First-class on both the WCP and MAP response shapes so existing clients reading either field keep working without branching.
  - `decision` 'allow' | 'block' | 'require_approval': Post-approval decision. Approved `require_approval` steps resolve to `allow`; rejected to `block`. `require_approval` on an approve/reject response means the step is still pending.
  - `reason` string: Decision reason text. Approved / rejected responses prefix the original policy reason with `Approved:` or `Rejected:`.
  - `approval_status` 'pending' | 'approved' | 'rejected' | 'expired': Terminal approval status after the mutation landed. `expired` is an auto-timeout (Evaluation-tier) — a terminal not-approved state that blocks the step, kept distinct from a human `rejected`.
  - `approval_id` string, uuid: HITL queue entry UUID of the hold this decision acted on: the step's pending hold, else its newest (hold 1 is UUID v5 over `workflow_id + ":" + step_id`; hold n >= 2, a step held again after an earlier hold was decided, is UUID v5 over `workflow_id + ":" + step_id + "#" + n`). Matches the queue row written by the WCP HITL adapter. With no hold for the step (a step-gate row outside its hold ids is not a hold) it is the hold-1 id; empty on the legacy in-memory MAP flow, and omitted when the queue could not be read.
  - `approved_by` string: Identity (X-User-ID, typically email) that approved the step
  - `approved_at` string, date-time: Timestamp when the approval was persisted
  - `rejected_by` string: Identity that rejected the step (rejection path only)
  - `rejected_at` string, date-time: Timestamp when the rejection was persisted
  - `policies_matched` PolicyMatch[]: Policies that triggered the original `require_approval` decision
    - `policy_id` string: Unique identifier for the policy
    - `policy_name` string: Human-readable name of the policy
    - `action` 'allow' | 'block' | 'require_approval' | 'redact': Action taken by this policy
    - `reason` string: Reason for the policy match
  - `retry_context` RetryContext: First-class retry and execution state (Issue #1673 Phase 1). Always present on every `StepGateResponse`, including the first gate call. Replaces the ambiguous `cached: bool` signal with unambiguous state the agent and policy engine can reason about.
    - `gate_count` integer, required: Number of /gate calls for this (workflow_id, step_id), including the current call. First call returns 1.
    - `completion_count` integer, required: Number of /complete calls for this (workflow_id, step_id). Normally 0 on first gate, 1 after the step completes.
    - `prior_completion_status` 'none' | 'completed' | 'gated_not_completed', required: "none" on first gate call. "completed" when a prior /gate + /complete both landed. "gated_not_completed" when a prior /gate landed but no /complete followed — uncertain territory the agent needs to reconcile against the downstream system before re-executing.
    - `prior_output_available` boolean, required: True iff prior_completion_status == "completed". Mirrors whether prior_output *could* be returned if include_prior_output=true.
    - `prior_output` object, nullable, required: Always present in the schema. Populated only when the caller set ?include_prior_output=true AND prior_output_available is true. Otherwise null.
    - `prior_completion_at` string, date-time, nullable, required: Timestamp of the prior /complete call, if any.
    - `first_attempt_at` string, date-time, required: Timestamp of the first /gate call for this step. On the first call, equals last_attempt_at.
    - `last_attempt_at` string, date-time, required: Timestamp of this /gate call.
    - `last_decision` 'allow' | 'block' | 'require_approval', required: Decision of the immediately prior /gate call. On the first call (gate_count == 1), equals the current decision (first-call invariant).
    - `idempotency_key` string, required: The caller-supplied business-level key recorded on this step (Issue #1673 Phase 2). Always present in the schema — empty string `""` if the caller never supplied one.
  - `message` string: Human-readable status summary

## Other responses

- `403`: MAP step approval requires Enterprise license (community mode), or (#4249) the caller's tenant cannot see the plan's workflow (`this plan belongs to another tenant`).
- `404`: No paused execution / plan step found
- `409`: The step cannot be approved, and the plan stays paused. On a plan backed by a workflow, the workflow step is not pending, its approval timed out (`approval_expired`), or its expiry could not be read (`approval_state_unreadable`). A timed-out approval is a deny. A confirm or step plan's workflow is the one bound to the plan when it entered that mode (#4249). The approval is also refused (409) when that workflow has ended, when the plan's workflow is still being set up (a plan left in that state is ended with `POST /api/v1/plan/{id}/cancel`), or, for a plan that began executing before the binding existed, when a workflow named for the plan has ended or more than one is running.

---

[API](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api.md) - [All operations](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/llms.txt) - [OpenAPI document](https://skmtc.dev/getaxonflow/apis/axonflow-orchestrator-api/revisions/f408ef3ec9dd?raw)
