# Approval pipelines

The ordered list of blocks every approval request runs through. A block can decide the request itself, ask a service of yours, hand it to a person, or, as a branch, run blocks of its own for the requests that match its condition. The hosted edition adds a block that asks a model.

A block returns `approve`, `deny`, `human`, or `next`. A request that gets
`next` from every block goes to a person. The block types are `always` (a fixed
outcome), `cel` (a condition), `webhook` (a service of yours), and `branch`
(blocks that run only when a condition matches); the hosted edition adds
`llm_judge` (a model), which the open edition refuses as an unknown type. An
`always` block returns its configured `outcome` for every request that
reaches it; later blocks do not run.

The organization pipeline runs first, for every request. The request's agent
pipeline runs only if the organization pipeline returns no final outcome.
An `always` block in the organization pipeline, outside any branch, prevents
agent pipelines from running. Both pipelines have an active revision. To change one,
[create a revision](#createRequestApprovalPipelineRevision), then
[activate it](#activateRequestApprovalPipelineRevision) with `If-Match` set to
the revision you expect to be active.

### Example: Review remaining requests

Append this block after your specific rules to send every remaining request
to human review. It inherits the pipeline's default reviewers because it
does not set `escalation_path`.

```json
{
  "id": "review-remaining-requests",
  "name": "Review remaining requests",
  "type": "always",
  "enabled": true,
  "timeout": "50ms",
  "max_attempts": 1,
  "config": {
    "outcome": "human",
    "reason": "Requests not handled by earlier blocks need human review."
  }
}
```

Set `outcome` to `approve` or `deny` for an automatic decision instead.
`escalation_path` is allowed only with `human`. Later blocks remain part of
the revision and must be valid, even though they cannot run.

### Example: Check refunds in a branch

A `branch` block runs the blocks in `config.blocks` only for requests that
match `config.when`, which takes the same condition as a `cel` block. Other
requests skip it. A request that enters a branch ends there: if none of its
blocks decides, it goes to a person with the branch's `reason` and
`escalation_path`, or the pipeline default, and later blocks do not run.
Blocks inside a branch do not inherit its `escalation_path`.

```json
{
  "id": "refunds",
  "name": "Refunds",
  "type": "branch",
  "enabled": true,
  "timeout": "50ms",
  "max_attempts": 1,
  "config": {
    "when": {
      "id": "6c1f0e2a-4b7d-4e35-9a1c-8d2b5f3e7a90",
      "field": "/request/tool",
      "operator": "equals",
      "value": "stripe.refund"
    },
    "blocks": [
      {
        "id": "approve-small-refunds",
        "name": "Approve small refunds",
        "type": "cel",
        "enabled": true,
        "timeout": "50ms",
        "max_attempts": 1,
        "config": {
          "when": {
            "id": "b2d94f1e-7c3a-4a58-8e06-3f1c9d7b2e45",
            "field": "/request/arguments/amount",
            "operator": "less_than",
            "value": 1000
          },
          "on_match": "approve",
          "reason": "Small refunds are pre-authorized."
        }
      }
    ],
    "reason": "Refunds that no block decided need Payments review.",
    "escalation_path": "payments-review"
  }
}
```

A branch needs at least one block, and branches can nest. Block `id`s and
condition `id`s must be unique across the whole document, including blocks
inside branches, and the 1,000-block limit counts every one of them. Reads
return the same nesting, with `snapshot_id`, `config_version` and
`content_hash` on every block at every level. In a
preview, a branch the sample enters reports the outcome `enter` and a
skipped one `next`; a sample that runs out of a branch ends with
`reason_code` `end_of_branch`, and the branch's closing entry has
`end_of_branch: true`.

## List pipelines

`GET /api/v1/request-approval-pipelines`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns every pipeline in the organization: the organization pipeline and one pipeline per agent. Each entry carries the active revision number and a summary of its history.

A pipeline exists as soon as the organization or the agent does, and it always has an active revision. An empty revision passes every request through to a person. The one exception is an archived agent: its pipeline is archived with it, has no active revision, carries `archived_at`, and is left out unless `status` says otherwise. Restoring the agent brings it back.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Which pipelines to return. `live` (the default) leaves archived pipelines out, `archived` returns only them, and `all` returns both. |
| `limit` | query | integer | no | How many pipelines to return. Defaults to 50. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The pipelines |
| 400 | ErrorResponse | `status` is not live, archived, or all |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |

Response body (200):

- `request_approval_pipelines` · array<RequestApprovalPipelineSummary> · required
  - `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
  - `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
  - `active_revision` · int64: The revision in use. Absent only for an archived pipeline.
  - `latest_revision` · int64 · required: The newest revision, active or not.
  - `revision_count` · int64 · required
  - `latest_created_at` · date-time · required: When the newest revision was created.
  - `archived_at` · date-time: Present when the pipeline was archived with its agent: readable, never active again until the agent is restored.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "request_approval_pipelines": [
    {
      "scope": "organization",
      "agent_slug": "string",
      "active_revision": 1,
      "latest_revision": 1,
      "revision_count": 1,
      "latest_created_at": "2026-09-08T12:02:11Z",
      "archived_at": "2026-09-08T12:02:11Z"
    }
  ]
}
```

## Retrieve the active organization pipeline

`GET /api/v1/request-approval-pipelines/organization`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns the organization pipeline's active revision, with its blocks in the order they run. The organization pipeline runs first, for every request, before the pipeline of the request's agent.

The `ETag` header carries the active revision number. Pass it as `If-Match` when you activate another revision.

### Request

No parameters or body.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The active revision |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |
| 404 | ErrorResponse | No revision is active |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Preview an organization pipeline

`POST /api/v1/request-approval-pipelines/organization/preview`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.write`

Runs a pipeline document against a sample request and returns what each block would do and the final outcome. Nothing is saved: not the document, not the sample, not the result. Use it to test a revision before you create it.

The sample names an agent by slug and name. For the organization pipeline the slug can be any valid slug; for an agent's pipeline it must be that agent's.

### Request

Request body:

- `blocks` · array<AlwaysBlockDefinition | CELBlockDefinition | WebhookBlockDefinition | BranchBlockDefinition | LLMJudgeBlockDefinition> · required: The blocks, in the order they should run.
- `default_escalation_path` · string: The default escalation path, as in a document. Reported in the result when the outcome is `human`.
- `sample` · object · required: The request to run the pipeline against.
  - `request` · object · required: The tool call.
    - `tool` · string · required: The tool the sample agent is calling.
    - `server` · string: The MCP server that defines the tool, as an adapter would report it. Leave it out for a built-in tool.
    - `arguments` · object · required: The tool's arguments.
    - `agent_reasoning` · string: The agent's own explanation of the call. Only blocks that include it see it.
    - `context` · object: Runtime metadata, as an adapter would report it. Only blocks that include it see it.
  - `agent` · object · required: The agent making the call.
    - `slug` · string · required: The agent's slug.
    - `name` · string · required: A name for the sample agent.
  - `previous_metadata` · object: Metadata as if earlier blocks had returned it, keyed by scope and then by block key. Use it to carry the organization preview's metadata into an agent preview, so the two previews together behave like one request.
    - `organization` · object: Metadata from the organization pipeline, by block key.
    - `agent` · object: Metadata from the agent's pipeline, by block key.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelinePreview | What the pipeline would do with the sample |
| 400 | ErrorResponse | The document or the sample is invalid |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot edit pipelines |

Response body (200):

- `outcome` · enum · required: The final outcome. One of `approve`, `deny`, `human`.
- `reason_code` · enum · required: Why the run ended. `block_outcome`: a block decided. `block_error`: a block failed. `end_of_pipeline`: the request passed every block. `end_of_branch`: the request entered a branch and none of its blocks decided. One of `block_outcome`, `block_error`, `end_of_pipeline`, `end_of_branch`.
- `terminal_block_id` · string: The block that ended the run: for `end_of_branch`, the branch. Absent when the request passed every block.
- `blocks` · array<RequestApprovalPipelinePreviewBlock> · required: What each block the request reached did, in order. A branch appears when its condition is evaluated, and once more with `end_of_branch` when none of its blocks decided.
  - `block_id` · string · required: The block's key.
  - `position` · integer · required: The block's position in the pipeline, from 0, counting in document order with each branch before the blocks it holds.
  - `outcome` · enum · required: What the block returned. A branch returns `enter` when its condition matched and `next` when it did not. One of `next`, `approve`, `deny`, `human`, `enter`.
  - `reason` · string: The note the block recorded.
  - `metadata` · object: The metadata the block returned, if any.
  - `cost` · int64 · required: The cost the block recorded.
  - `error_code` · string: Why the block failed, if it did: `invalid_input`, `evaluation_error`, `cost_limit_exceeded`, `resource_limit_exceeded`, `timeout`, `invalid_block_output`, `internal_error`, `provider_error`, `endpoint_unreachable`, `endpoint_error`, `endpoint_rejected`, `outcome_not_allowed`, `endpoint_unavailable`, or a code a kind the edition adds reports (the hosted judge's `model_unavailable`).
  - `delivery` · object: For webhook blocks. What was called and what came back. Never the bodies, never the secret.
    - `endpoint_key` · string · required: The endpoint that was called.
    - `endpoint_revision` · int64: The revision that was delivered to. Absent when the endpoint had no active revision.
    - `endpoint_host` · string · required: The host that was called.
    - `http_status` · integer: The HTTP status the endpoint returned.
    - `duration_ms` · int64 · required: How long the call took, in milliseconds.
    - `reason` · string: The endpoint's own explanation, at most 1,024 characters.
  - `trace` · array<object> · required: How the condition was evaluated, node by node. For condition blocks and branches.
    - `node_id` · uuid · required: The condition's id.
    - `matched` · boolean · required: Whether the condition held.
    - `error` · boolean · required: Whether evaluating it failed.
  - `end_of_branch` · boolean: Present and true on the step that closes a branch the request entered when none of its blocks decided. The outcome is `human`.
- `escalation_path` · string: The path a `human` outcome goes to, within this scope alone: the ending block's path, or the document's default. Absent when the outcome is automatic or no path applies.
- `escalation_path_reason` · enum: Where the path came from. `block_escalation`: the ending block named it. `pipeline_default`: the document's default. `no_escalation`: no path applies. One of `block_escalation`, `pipeline_default`, `no_escalation`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization/preview" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "blocks": [
    {
      "id": "review-remaining-requests",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      }
    }
  ],
  "default_escalation_path": "string",
  "sample": {
    "request": {
      "tool": "issue_refund",
      "server": "stripe",
      "arguments": {
        "amount_cents": 12000
      },
      "agent_reasoning": "string",
      "context": {}
    },
    "agent": {
      "slug": "string",
      "name": "string"
    },
    "previous_metadata": {
      "organization": {},
      "agent": {}
    }
  }
}'
```

200 response

```json
{
  "outcome": "approve",
  "reason_code": "block_outcome",
  "terminal_block_id": "string",
  "blocks": [
    {
      "block_id": "string",
      "position": 0,
      "outcome": "next",
      "reason": "string",
      "metadata": {},
      "cost": 0,
      "error_code": "string",
      "delivery": {
        "endpoint_key": "string",
        "endpoint_revision": 1,
        "endpoint_host": "string",
        "http_status": 1,
        "duration_ms": 1,
        "reason": "string"
      },
      "trace": [
        {
          "node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "matched": true,
          "error": true
        }
      ],
      "end_of_branch": true
    }
  ],
  "escalation_path": "string",
  "escalation_path_reason": "block_escalation"
}
```

## List organization pipeline revisions

`GET /api/v1/request-approval-pipelines/organization/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns the organization pipeline's revisions, newest first, without their blocks.

Paging is by cursor. When more revisions follow, the response carries `next_cursor`. Pass it back as `cursor` to get the next page. The last page has no `next_cursor`. `total_count` is how many revisions there are across every page.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no | The `next_cursor` from the previous page. |
| `limit` | query | integer | no | How many revisions to return per page. Defaults to 50. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The revisions, newest first |
| 400 | ErrorResponse | `cursor` is not a cursor this endpoint issued |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |

Response body (200):

- `revisions` · array<RequestApprovalPipelineRevisionSummary> · required: The revisions on this page, newest first.
  - `id` · uuid · required
  - `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
  - `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
  - `revision` · int64 · required: The revision number. Revisions count up from 1.
  - `is_active` · boolean · required: Whether this is the revision in use.
  - `block_count` · integer · required: Every block in the revision, including blocks inside branches.
  - `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
  - `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
  - `created_at` · date-time · required
  - `archived_at` · date-time: Present when the revision was archived with its agent. An archived revision can be read but not activated until the agent is restored.
- `next_cursor` · string: Present when another page follows.
- `total_count` · int64 · required: How many revisions the pipeline has, across every page.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "revisions": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "scope": "organization",
      "agent_slug": "string",
      "revision": 1,
      "is_active": true,
      "block_count": 0,
      "default_escalation_path": "string",
      "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "created_at": "2026-09-08T12:02:11Z",
      "archived_at": "2026-09-08T12:02:11Z"
    }
  ],
  "next_cursor": "string",
  "total_count": 1
}
```

## Create an organization pipeline revision

`POST /api/v1/request-approval-pipelines/organization/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.write`

Creates a new revision of the organization pipeline from a complete document. The document is validated and compiled first. The revision is created inactive, and it never changes. Activate it to put it into use.

### Request

Request body:

- `blocks` · array<AlwaysBlockDefinition | CELBlockDefinition | WebhookBlockDefinition | BranchBlockDefinition | LLMJudgeBlockDefinition> · required: The blocks, in the order they should run. A branch holds its own blocks in `config.blocks`, and the pipeline may hold at most 1,000 blocks counting those. An empty list passes every request through to a person.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path. That includes requests that reach the end of the pipeline. The agent's default wins over the organization's. With neither, any reviewer who can decide may take the request from the queue. The path must exist when the revision is created, and be active when it is activated.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | RequestApprovalPipelineRevision | The new revision, inactive |
| 400 | ErrorResponse | The document is invalid |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot edit pipelines |

Response body (201):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "blocks": [
    {
      "id": "review-remaining-requests",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      }
    }
  ],
  "default_escalation_path": "string"
}'
```

201 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Retrieve an organization pipeline revision

`GET /api/v1/request-approval-pipelines/organization/revisions/{revision}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns one revision of the organization pipeline, active or not, with its blocks in the order they run.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The revision |
| 400 | ErrorResponse | `revision` is not a positive integer |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |
| 404 | ErrorResponse | No such revision |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization/revisions/{revision}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Activate an organization pipeline revision

`POST /api/v1/request-approval-pipelines/organization/revisions/{revision}/activate`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.activate`

Makes a revision the active one. New requests use it from then on. Activating an older revision is how you roll back.

Pass the revision you expect to be active in `If-Match`, quoted, as returned in `ETag`. If someone activated another revision in the meantime, the call fails with 412 and nothing changes.

Every escalation path the revision names, and every webhook endpoint its blocks post to, must have an active revision. Otherwise the call fails with 409 and nothing changes.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |
| `If-Match` | header | string | yes | The revision you expect to be active, quoted, for example `"2"`. Take it from the `ETag` of your last read. If the active revision changed in the meantime, the call fails with 412 and nothing changes. Escalation paths and webhook endpoints accept `"0"` when no revision is active. Pipelines always have an active revision. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The revision, now active |
| 400 | ErrorResponse | `revision` or `If-Match` is malformed |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot activate pipelines |
| 404 | ErrorResponse | No such revision |
| 409 | ErrorResponse | An escalation path or webhook endpoint the revision uses has no active revision. `error.code` is `escalation_path_inactive` or `webhook_endpoint_inactive`, and `error.details` lists the keys |
| 412 | ErrorResponse | `If-Match` does not match the active revision |
| 428 | ErrorResponse | The `If-Match` header is missing |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/organization/revisions/{revision}/activate" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "If-Match: "2""
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Retrieve an agent's active pipeline

`GET /api/v1/request-approval-pipelines/agents/{agent_slug}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns the active revision of one agent's pipeline, with its blocks in the order they run. It runs after the organization pipeline, for every request this agent makes.

The `ETag` header carries the active revision number. Pass it as `If-Match` when you activate another revision.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The active revision |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |
| 404 | ErrorResponse | No agent with this slug exists, or the agent and its pipeline are archived |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Preview an agent's pipeline

`POST /api/v1/request-approval-pipelines/agents/{agent_slug}/preview`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.write`

Runs a pipeline document against a sample request and returns what each block would do and the final outcome. Nothing is saved: not the document, not the sample, not the result. Use it to test a revision before you create it.

The preview runs this document alone. To see what a request would meet end to end, preview the organization pipeline first and pass its metadata in `previous_metadata`.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |

Request body:

- `blocks` · array<AlwaysBlockDefinition | CELBlockDefinition | WebhookBlockDefinition | BranchBlockDefinition | LLMJudgeBlockDefinition> · required: The blocks, in the order they should run.
- `default_escalation_path` · string: The default escalation path, as in a document. Reported in the result when the outcome is `human`.
- `sample` · object · required: The request to run the pipeline against.
  - `request` · object · required: The tool call.
    - `tool` · string · required: The tool the sample agent is calling.
    - `server` · string: The MCP server that defines the tool, as an adapter would report it. Leave it out for a built-in tool.
    - `arguments` · object · required: The tool's arguments.
    - `agent_reasoning` · string: The agent's own explanation of the call. Only blocks that include it see it.
    - `context` · object: Runtime metadata, as an adapter would report it. Only blocks that include it see it.
  - `agent` · object · required: The agent making the call.
    - `slug` · string · required: The agent's slug.
    - `name` · string · required: A name for the sample agent.
  - `previous_metadata` · object: Metadata as if earlier blocks had returned it, keyed by scope and then by block key. Use it to carry the organization preview's metadata into an agent preview, so the two previews together behave like one request.
    - `organization` · object: Metadata from the organization pipeline, by block key.
    - `agent` · object: Metadata from the agent's pipeline, by block key.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelinePreview | What the pipeline would do with the sample |
| 400 | ErrorResponse | The document or the sample is invalid |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot edit pipelines |

Response body (200):

- `outcome` · enum · required: The final outcome. One of `approve`, `deny`, `human`.
- `reason_code` · enum · required: Why the run ended. `block_outcome`: a block decided. `block_error`: a block failed. `end_of_pipeline`: the request passed every block. `end_of_branch`: the request entered a branch and none of its blocks decided. One of `block_outcome`, `block_error`, `end_of_pipeline`, `end_of_branch`.
- `terminal_block_id` · string: The block that ended the run: for `end_of_branch`, the branch. Absent when the request passed every block.
- `blocks` · array<RequestApprovalPipelinePreviewBlock> · required: What each block the request reached did, in order. A branch appears when its condition is evaluated, and once more with `end_of_branch` when none of its blocks decided.
  - `block_id` · string · required: The block's key.
  - `position` · integer · required: The block's position in the pipeline, from 0, counting in document order with each branch before the blocks it holds.
  - `outcome` · enum · required: What the block returned. A branch returns `enter` when its condition matched and `next` when it did not. One of `next`, `approve`, `deny`, `human`, `enter`.
  - `reason` · string: The note the block recorded.
  - `metadata` · object: The metadata the block returned, if any.
  - `cost` · int64 · required: The cost the block recorded.
  - `error_code` · string: Why the block failed, if it did: `invalid_input`, `evaluation_error`, `cost_limit_exceeded`, `resource_limit_exceeded`, `timeout`, `invalid_block_output`, `internal_error`, `provider_error`, `endpoint_unreachable`, `endpoint_error`, `endpoint_rejected`, `outcome_not_allowed`, `endpoint_unavailable`, or a code a kind the edition adds reports (the hosted judge's `model_unavailable`).
  - `delivery` · object: For webhook blocks. What was called and what came back. Never the bodies, never the secret.
    - `endpoint_key` · string · required: The endpoint that was called.
    - `endpoint_revision` · int64: The revision that was delivered to. Absent when the endpoint had no active revision.
    - `endpoint_host` · string · required: The host that was called.
    - `http_status` · integer: The HTTP status the endpoint returned.
    - `duration_ms` · int64 · required: How long the call took, in milliseconds.
    - `reason` · string: The endpoint's own explanation, at most 1,024 characters.
  - `trace` · array<object> · required: How the condition was evaluated, node by node. For condition blocks and branches.
    - `node_id` · uuid · required: The condition's id.
    - `matched` · boolean · required: Whether the condition held.
    - `error` · boolean · required: Whether evaluating it failed.
  - `end_of_branch` · boolean: Present and true on the step that closes a branch the request entered when none of its blocks decided. The outcome is `human`.
- `escalation_path` · string: The path a `human` outcome goes to, within this scope alone: the ending block's path, or the document's default. Absent when the outcome is automatic or no path applies.
- `escalation_path_reason` · enum: Where the path came from. `block_escalation`: the ending block named it. `pipeline_default`: the document's default. `no_escalation`: no path applies. One of `block_escalation`, `pipeline_default`, `no_escalation`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}/preview" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "blocks": [
    {
      "id": "review-remaining-requests",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      }
    }
  ],
  "default_escalation_path": "string",
  "sample": {
    "request": {
      "tool": "issue_refund",
      "server": "stripe",
      "arguments": {
        "amount_cents": 12000
      },
      "agent_reasoning": "string",
      "context": {}
    },
    "agent": {
      "slug": "string",
      "name": "string"
    },
    "previous_metadata": {
      "organization": {},
      "agent": {}
    }
  }
}'
```

200 response

```json
{
  "outcome": "approve",
  "reason_code": "block_outcome",
  "terminal_block_id": "string",
  "blocks": [
    {
      "block_id": "string",
      "position": 0,
      "outcome": "next",
      "reason": "string",
      "metadata": {},
      "cost": 0,
      "error_code": "string",
      "delivery": {
        "endpoint_key": "string",
        "endpoint_revision": 1,
        "endpoint_host": "string",
        "http_status": 1,
        "duration_ms": 1,
        "reason": "string"
      },
      "trace": [
        {
          "node_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "matched": true,
          "error": true
        }
      ],
      "end_of_branch": true
    }
  ],
  "escalation_path": "string",
  "escalation_path_reason": "block_escalation"
}
```

## List an agent's pipeline revisions

`GET /api/v1/request-approval-pipelines/agents/{agent_slug}/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns the revisions of one agent's pipeline, newest first, without their blocks.

Paging is by cursor. When more revisions follow, the response carries `next_cursor`. Pass it back as `cursor` to get the next page. The last page has no `next_cursor`. `total_count` is how many revisions there are across every page.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |
| `cursor` | query | string | no | The `next_cursor` from the previous page. |
| `limit` | query | integer | no | How many revisions to return per page. Defaults to 50. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The revisions, newest first |
| 400 | ErrorResponse | `cursor` is not a cursor this endpoint issued |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |

Response body (200):

- `revisions` · array<RequestApprovalPipelineRevisionSummary> · required: The revisions on this page, newest first.
  - `id` · uuid · required
  - `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
  - `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
  - `revision` · int64 · required: The revision number. Revisions count up from 1.
  - `is_active` · boolean · required: Whether this is the revision in use.
  - `block_count` · integer · required: Every block in the revision, including blocks inside branches.
  - `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
  - `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
  - `created_at` · date-time · required
  - `archived_at` · date-time: Present when the revision was archived with its agent. An archived revision can be read but not activated until the agent is restored.
- `next_cursor` · string: Present when another page follows.
- `total_count` · int64 · required: How many revisions the pipeline has, across every page.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "revisions": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "scope": "organization",
      "agent_slug": "string",
      "revision": 1,
      "is_active": true,
      "block_count": 0,
      "default_escalation_path": "string",
      "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "created_at": "2026-09-08T12:02:11Z",
      "archived_at": "2026-09-08T12:02:11Z"
    }
  ],
  "next_cursor": "string",
  "total_count": 1
}
```

## Create an agent's pipeline revision

`POST /api/v1/request-approval-pipelines/agents/{agent_slug}/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.write`

Creates a new revision of one agent's pipeline from a complete document. The document is validated and compiled first. The revision is created inactive, and it never changes. Activate it to put it into use.

The pipeline exists from the moment the agent is created.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |

Request body:

- `blocks` · array<AlwaysBlockDefinition | CELBlockDefinition | WebhookBlockDefinition | BranchBlockDefinition | LLMJudgeBlockDefinition> · required: The blocks, in the order they should run. A branch holds its own blocks in `config.blocks`, and the pipeline may hold at most 1,000 blocks counting those. An empty list passes every request through to a person.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path. That includes requests that reach the end of the pipeline. The agent's default wins over the organization's. With neither, any reviewer who can decide may take the request from the queue. The path must exist when the revision is created, and be active when it is activated.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | RequestApprovalPipelineRevision | The new revision, inactive |
| 400 | ErrorResponse | The document is invalid |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot edit pipelines |
| 404 | ErrorResponse | No agent with this slug exists |
| 409 | ErrorResponse | The agent and its pipeline are archived (`request_approval_pipeline_archived`) |

Response body (201):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "blocks": [
    {
      "id": "review-remaining-requests",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      }
    }
  ],
  "default_escalation_path": "string"
}'
```

201 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Retrieve an agent's pipeline revision

`GET /api/v1/request-approval-pipelines/agents/{agent_slug}/revisions/{revision}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.read`

Returns one revision of an agent's pipeline, active or not, with its blocks in the order they run.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The revision |
| 400 | ErrorResponse | `revision` is not a positive integer |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read pipelines |
| 404 | ErrorResponse | No such revision |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}/revisions/{revision}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```

## Activate an agent's pipeline revision

`POST /api/v1/request-approval-pipelines/agents/{agent_slug}/revisions/{revision}/activate`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `pipeline.activate`

Makes a revision the active one. New requests use it from then on. Activating an older revision is how you roll back.

Pass the revision you expect to be active in `If-Match`, quoted, as returned in `ETag`. If someone activated another revision in the meantime, the call fails with 412 and nothing changes.

Every escalation path the revision names, and every webhook endpoint its blocks post to, must have an active revision. Otherwise the call fails with 409 and nothing changes.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agent_slug` | path | string | yes | The slug of the agent whose pipeline this is. |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |
| `If-Match` | header | string | yes | The revision you expect to be active, quoted, for example `"2"`. Take it from the `ETag` of your last read. If the active revision changed in the meantime, the call fails with 412 and nothing changes. Escalation paths and webhook endpoints accept `"0"` when no revision is active. Pipelines always have an active revision. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | RequestApprovalPipelineRevision | The revision, now active |
| 400 | ErrorResponse | `revision` or `If-Match` is malformed |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot activate pipelines |
| 404 | ErrorResponse | No such revision |
| 409 | ErrorResponse | An escalation path or webhook endpoint the revision uses has no active revision (`escalation_path_inactive` or `webhook_endpoint_inactive`, with the keys in `error.details`), or the revision or the pipeline was archived with the type's last agent (`request_approval_pipeline_archived`) |
| 412 | ErrorResponse | `If-Match` does not match the active revision |
| 428 | ErrorResponse | The `If-Match` header is missing |

Response body (200):

- `id` · uuid · required
- `scope` · enum · required: Which requests a pipeline applies to. The organization pipeline runs first, for every request. An agent's pipeline runs after it, for that agent's requests. One of `organization`, `agent`.
- `agent_slug` · string: The agent's slug. Present for the `agent` scope only.
- `revision` · int64 · required: The revision number. Revisions count up from 1.
- `is_active` · boolean · required: Whether this is the revision in use.
- `block_count` · integer · required: Every block in the revision, including blocks inside branches.
- `default_escalation_path` · string: The escalation path for requests that reach a person without a block naming a path.
- `created_by_actor_id` · uuid · required: The membership or organization-key actor that created the revision.
- `created_at` · date-time · required
- `blocks` · array<StoredAlwaysBlock | StoredCELBlock | StoredWebhookBlock | StoredBranchBlock | StoredLLMJudgeBlock> · required: The blocks, in the order they run. A branch holds its own blocks in `config.blocks`.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/agents/{agent_slug}/revisions/{revision}/activate" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "If-Match: "2""
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "scope": "organization",
  "agent_slug": "string",
  "revision": 1,
  "is_active": true,
  "block_count": 0,
  "default_escalation_path": "string",
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "blocks": [
    {
      "id": "review-remaining-requests",
      "snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Review remaining requests",
      "type": "always",
      "enabled": true,
      "timeout": "50ms",
      "max_attempts": 1,
      "config": {
        "outcome": "human",
        "reason": "Requests not handled by earlier blocks need human review."
      },
      "config_version": 1,
      "content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ]
}
```
