Approval pipelines
The rules every approval request runs through, as an ordered list of blocks. A block can approve or deny a request, send it to a person, or pass it to the next block.
A block returns approve, deny, human, or next. A request that gets
next from every block goes to a person. The block types are outcome (a
fixed outcome), webhook (a service of yours), branch (blocks that run only
when a condition matches), and delegate (hands matching requests to another
pipeline); the hosted edition adds llm_judge (a model), which the open
edition refuses as an unknown type. An outcome block returns its configured
outcome for every request that reaches it; later blocks do not run.
Every request runs the entry pipeline, which every organization has and
which cannot be created or archived. Other pipelines are
created with a pipeline_key you choose
once and can never reuse, and a request reaches them only through a
delegate block. Every live pipeline has an active revision. To change one,
create a revision, then
activate it with If-Match set to
the revision you expect to be active.
Archiving a pipeline is permanent, and is
refused for entry and while an active revision still delegates to it;
its uses list those revisions.
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.
{
"id": "review-remaining-requests",
"name": "Review remaining requests",
"type": "outcome",
"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. 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. A branch whose blocks end in an outcome block never runs
out, so its reason is optional. Blocks inside a branch do not inherit its
escalation_path.
{
"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": "branch",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"when": {
"id": "b2d94f1e-7c3a-4a58-8e06-3f1c9d7b2e45",
"field": "/request/arguments/amount",
"operator": "less_than",
"value": 1000
},
"blocks": [
{
"id": "approve-small-refund",
"name": "Approve the refund",
"type": "outcome",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"outcome": "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 ids and
condition ids 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.
Example: Hand Stripe requests to another pipeline
A delegate block hands the requests that match config.when to the
pipeline named in config.pipeline_key, which continues with that pipeline's
active revision. Other requests skip it. A request that is handed over is
settled in that pipeline: if none of its blocks decides, it goes to a person
with that pipeline's default escalation path, else the defaults of the
pipelines that delegated into it, nearest first, and nothing after the
delegate runs. A delegate has no reason or escalation_path, and can
never name entry or its own pipeline.
{
"id": "to-payments",
"name": "Stripe requests",
"type": "delegate",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"when": {
"id": "0f3a7c52-91d4-4b6e-8a2f-5c7e1d9b3a64",
"field": "/request/server",
"operator": "equals",
"value": "stripe"
},
"pipeline_key": "payments"
}
}The named pipeline must exist when you create the revision and be active when
you activate it. Activation also refuses delegation that would loop or nest
more than eight pipelines deep. A preview
runs the document you send in place of this pipeline and expands each
delegate the sample matches into its target's active revision; every result
block carries its pipeline_key. Metadata earlier blocks returned is read as
/previous_metadata/<pipeline_key>/<block_key>.
List pipelines
Returns every pipeline in the organization: the entry pipeline, where every request starts, first, then every pipeline a delegate block can hand requests to. Each entry carries the key, the name, the active revision number and a summary of its history.
A pipeline always has an active revision. An empty revision passes every request that enters the pipeline through to a person. An archived pipeline has no active revision, carries archived_at, and is left out unless status says otherwise. The list is narrowed to the pipelines you may read.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
status | query · enum | Which pipelines to return. One of live, archived, all |
limit | query · integer | How many pipelines to return. Defaults to 50. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | object | The pipelines |
400 | ErrorResponse |
|
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot read pipelines |
Response body200
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"request_approval_pipelines": [
{
"pipeline_key": "string",
"name": "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",
"referenced_by": [
{
"pipeline_key": "string",
"pipeline_name": "string",
"revision": 1,
"block_key": "string",
"block_name": "string"
}
]
}
]
}Create a pipeline
Creates a pipeline that a delegate block in another pipeline can hand requests to, with its first revision active at once. You choose the key; it is taken for good, archived pipelines included, and can never be entry, which every organization already has.
Creating a pipeline is an organization-wide act: it needs pipeline.write at the organization. Grant a team pipeline.read, pipeline.write and pipeline.activate at pipeline:<key> and they can edit the new pipeline and nothing else.
Request
Request body
The pipeline's key. Taken for good once used, archived pipelines included; never entry.
The display name.
The first revision's blocks, in the order they should run. Omit for a pipeline that passes every request that enters it through to a person until it is edited.
The escalation path for requests that reach a person from this pipeline without a block naming a path, as in a document.
Send those requests to the person who owns the agent instance that made them instead. default_escalation_path, if set, is then the fallback for a request whose agent has no owner able to decide it (a service agent, or an owner who was deactivated); without it the fallback is the enclosing pipeline's default, else any authorized reviewer. The owner is the only reviewer: the request never moves to the path because the owner has not answered.
Response
Response codes
| Status | Body | Description |
|---|---|---|
201 | RequestApprovalPipelineRevision | The new pipeline's first revision, active |
400 | ErrorResponse | The key, the name, or the document is invalid, or a delegate names a pipeline that does not exist ( |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot create pipelines |
409 | ErrorResponse | The key was ever used ( |
Response body201
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pipeline_key": "payments",
"name": "Payments",
"blocks": [
{
"id": "review-remaining-requests",
"name": "Review remaining requests",
"type": "outcome",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"outcome": "human",
"reason": "Requests not handled by earlier blocks need human review."
}
}
],
"default_escalation_path": "string",
"default_ask_agent_owner": true
}'{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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 a pipeline's active revision
Returns the pipeline's active revision, with its blocks in the order they run. entry is the pipeline every request starts in; any other pipeline runs for the requests a delegate block hands to it.
The ETag header carries the active revision number. Pass it as If-Match when you activate another revision.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | RequestApprovalPipelineRevision | The active revision |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot read this pipeline |
404 | ErrorResponse | No pipeline with this key exists, or it is archived |
Response body200
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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"
}
]
}Archive a pipeline
Archives the pipeline: every revision is stamped, none is active, and its key stays taken. Its revisions remain readable with status=archived or all.
The entry pipeline cannot be archived, and neither can a pipeline that a delegate block in an active revision still hands requests to; remove those blocks first. GET .../uses lists them.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
204 | The pipeline is archived | |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot archive this pipeline |
404 | ErrorResponse | No pipeline with this key exists |
409 | ErrorResponse | The pipeline is the entry pipeline ( |
Example
curl -X DELETE "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"List where a pipeline is delegated to
Returns the active pipeline revisions whose delegate blocks hand requests to this pipeline. The same set refuses archiving it.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | object | The uses |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot read this pipeline |
Response body200
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/uses" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"uses": [
{
"pipeline_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"revision": 1,
"block_key": "string"
}
]
}Preview a pipeline
/api/v1/request-approval-pipelines/{pipeline_key}/previewSession or API keyRequires pipeline.writeRuns 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 document stands in for this pipeline. Delegate blocks expand to their target pipelines' current active revisions, so the preview walks what a request would; a skipped delegate calls nothing. The sample names an agent by slug and name; any valid slug will do, and conditions read it as agent.slug.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
Request body
The blocks, in the order they should run.
The default escalation path, as in a document. Reported in the result when the outcome is human.
Whether the default asks the agent's owner first, as in a document.
The request to run the pipeline against.
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | RequestApprovalPipelinePreview | What the pipeline would do with the sample |
400 | ErrorResponse | The document or the sample is invalid, a delegate names a pipeline with no active revision ( |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot edit this pipeline |
Response body200
Why the run ended. block_outcome: a block decided. block_error, block_timeout, retries_exhausted, invalid_block_output: a block failed. end_of_pipeline: the request passed every block. end_of_branch: the request entered a branch or a delegated pipeline and none of its blocks decided.
One of block_outcome, block_error, block_timeout, retries_exhausted, invalid_block_output, end_of_pipeline, end_of_branch
The block that ended the run: for end_of_branch, the branch or the delegate. Absent when the request passed every block.
What each block the request reached did, in order, each with the pipeline it belongs to. A branch or delegate appears when its condition is evaluated, and once more with end_of_branch when none of the blocks inside decided.
The path a human outcome goes to: the ending block's path, else the default of the pipeline the request was in, else the defaults of the pipelines that delegated into it. Absent when the outcome is automatic or no path applies.
Where the path came from. block_escalation: the ending block named it. pipeline_default: a pipeline's default. no_escalation: no path applies.
One of block_escalation, pipeline_default, no_escalation
The pipeline whose block or default supplied the path. Absent with no_escalation.
Present when the route asks the agent's owner, with escalation_path as the fallback. A preview has no real agent instance, so it names the route, never a person.
Example
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/preview" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"id": "review-remaining-requests",
"name": "Review remaining requests",
"type": "outcome",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"outcome": "human",
"reason": "Requests not handled by earlier blocks need human review."
}
}
],
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"sample": {
"request": {
"tool": "issue_refund",
"server": "stripe",
"arguments": {
"amount_cents": 12000
},
"agent_reasoning": "string",
"context": {}
},
"agent": {
"slug": "string",
"name": "string"
},
"previous_metadata": {}
}
}'{
"outcome": "approve",
"reason_code": "block_outcome",
"terminal_block_id": "string",
"blocks": [
{
"block_id": "string",
"pipeline_key": "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",
"escalation_path_pipeline_key": "string",
"ask_agent_owner": true
}List pipeline revisions
/api/v1/request-approval-pipelines/{pipeline_key}/revisionsSession or API keyRequires pipeline.readReturns the 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
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
cursor | query · string | The |
limit | query · integer | How many revisions to return per page. Defaults to 50. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | object | The revisions, newest first |
400 | ErrorResponse |
|
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot read this pipeline |
Response body200
The revisions on this page, newest first.
Present when another page follows.
How many revisions the pipeline has, across every page.
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"revisions": [
{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z"
}
],
"next_cursor": "string",
"total_count": 1
}Create a pipeline revision
/api/v1/request-approval-pipelines/{pipeline_key}/revisionsSession or API keyRequires pipeline.writeCreates a new revision of the pipeline from a complete document. The document is validated and compiled first, and every escalation path, webhook endpoint and pipeline it names must exist. The revision is created as a draft, which can be changed in place until it is activated. Activate it to put it into use. Omit name to keep the active revision's.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
Request body
A new display name for the pipeline. Omit to keep the active revision's.
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 that enters the pipeline through to a person.
The escalation path for requests that reach a person from this pipeline without a block naming a path. That includes requests that run out of it. A delegated pipeline without one inherits the defaults of the pipelines that delegated into it, nearest first; with none anywhere, 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.
Send those requests to the person who owns the agent instance that made them instead. default_escalation_path, if set, is then the fallback for a request whose agent has no owner able to decide it (a service agent, or an owner who was deactivated); without it the fallback is the enclosing pipeline's default, else any authorized reviewer. The owner is the only reviewer: the request never moves to the path because the owner has not answered.
Response
Response codes
| Status | Body | Description |
|---|---|---|
201 | RequestApprovalPipelineRevision | The new revision, inactive |
400 | ErrorResponse | The document is invalid, or a delegate names a pipeline that does not exist ( |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot edit this pipeline |
404 | ErrorResponse | No pipeline with this key exists |
409 | ErrorResponse | The pipeline is archived ( |
Response body201
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "string",
"blocks": [
{
"id": "review-remaining-requests",
"name": "Review remaining requests",
"type": "outcome",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"outcome": "human",
"reason": "Requests not handled by earlier blocks need human review."
}
}
],
"default_escalation_path": "string",
"default_ask_agent_owner": true
}'{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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 a pipeline revision
/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}Session or API keyRequires pipeline.readReturns one revision of the pipeline, active or not, with its blocks in the order they run.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
revisionrequired | path · int64 | The revision number. Revisions count up from 1. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | RequestApprovalPipelineRevision | The revision |
400 | ErrorResponse |
|
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot read this pipeline |
404 | ErrorResponse | No such revision |
Response body200
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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"
}
]
}Update a draft pipeline revision
/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}Session or API keyRequires pipeline.writeReplaces the content of a draft revision, one that has never been active, with a complete document. The revision keeps its number. The document is checked exactly as a new revision's. Omit name to keep the draft's. Once a revision has been active it can no longer change.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
revisionrequired | path · int64 | The revision number. Revisions count up from 1. |
Request body
A new display name for the pipeline. Omit to keep the active revision's.
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 that enters the pipeline through to a person.
The escalation path for requests that reach a person from this pipeline without a block naming a path. That includes requests that run out of it. A delegated pipeline without one inherits the defaults of the pipelines that delegated into it, nearest first; with none anywhere, 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.
Send those requests to the person who owns the agent instance that made them instead. default_escalation_path, if set, is then the fallback for a request whose agent has no owner able to decide it (a service agent, or an owner who was deactivated); without it the fallback is the enclosing pipeline's default, else any authorized reviewer. The owner is the only reviewer: the request never moves to the path because the owner has not answered.
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | RequestApprovalPipelineRevision | The updated draft |
400 | ErrorResponse | The document is invalid, or a delegate names a pipeline that does not exist ( |
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot edit this pipeline |
404 | ErrorResponse | No such revision |
409 | ErrorResponse | The revision has been active ( |
Response body200
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X PUT "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "string",
"blocks": [
{
"id": "review-remaining-requests",
"name": "Review remaining requests",
"type": "outcome",
"enabled": true,
"timeout": "50ms",
"max_attempts": 1,
"config": {
"outcome": "human",
"reason": "Requests not handled by earlier blocks need human review."
}
}
],
"default_escalation_path": "string",
"default_ask_agent_owner": true
}'{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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"
}
]
}Report what a pipeline revision depends on
/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}/referencesSession or API keyRequires pipeline.readReports the escalation paths, webhook endpoints and pipelines the revision names with their current state, and any block the deployment or plan cannot run, so a rollback can be explained before it is attempted. The activation itself re-checks inside its transaction.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
revisionrequired | path · int64 | The revision number. Revisions count up from 1. |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | PipelineReferences | Live state of every dependency |
400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
Response body200
The pipelines the revision's delegate blocks hand requests to.
The escalation paths the revision names.
The webhook endpoints the revision posts to.
Blocks the deployment or the plan cannot run right now.
Whether activating the revision would succeed right now.
Example
curl -X GET "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}/references" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY"{
"pipelines": [
{
"pipeline_key": "string",
"name": "string",
"block_keys": [
"string"
],
"exists": true,
"active": true
}
],
"escalation_paths": [
{
"path_key": "string",
"block_keys": [
"string"
],
"default": true,
"exists": true,
"active": true
}
],
"webhook_endpoints": [
{
"endpoint_key": "string",
"name": "string",
"block_keys": [
"string"
],
"exists": true,
"active": true
}
],
"blocks": [
{
"block_key": "string",
"type": "string",
"problem": "string"
}
],
"activatable": true
}Activate a pipeline revision
/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}/activateSession or API keyRequires pipeline.activateMakes 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, every webhook endpoint its blocks post to, and every pipeline its delegate blocks hand requests to must have an active revision, and the delegation graph must stay a tree: no pipeline may reach itself again, no chain may be deeper than 8 delegations, and the flattened program from the entry pipeline may hold at most 10,000 blocks. Otherwise the call fails with 409 and nothing changes.
Request
Parameters
| Parameter | Type | Description |
|---|---|---|
pipeline_keyrequired | path · string | The pipeline's key. |
revisionrequired | path · int64 | The revision number. Revisions count up from 1. |
If-Matchrequired | header · string | The revision you expect to be active, quoted, for example |
Response
Response codes
| Status | Body | Description |
|---|---|---|
200 | RequestApprovalPipelineRevision | The revision, now active |
400 | ErrorResponse |
|
401 | ErrorResponse | You are not signed in |
403 | ErrorResponse | You cannot activate this pipeline |
404 | ErrorResponse | No such revision |
409 | ErrorResponse | A reference has no active revision ( |
412 | ErrorResponse |
|
428 | ErrorResponse | The |
Response body200
The pipeline's key.
The pipeline's display name as of this revision.
The revision number. Revisions count up from 1.
Whether this is the revision in use.
Every block in the revision, including blocks inside branches.
The escalation path for requests that reach a person from this pipeline without a block naming a path.
Send requests that reach a person without a block naming a path to the person who owns the agent instance that made them, instead of default_escalation_path. The path, if any, is then the fallback for a request whose agent has no owner able to decide it; without one the fallback is the enclosing pipeline's default, else any authorized reviewer.
The membership or organization-key actor that created the revision.
When the revision was first active. Absent for a draft, which can still be changed.
Present when the pipeline was archived.
The blocks, in the order they run. A branch holds its own blocks in config.blocks; a delegate names the pipeline it hands matching requests to in config.pipeline_key.
Example
curl -X POST "$WITHHUMAN_URL/api/v1/request-approval-pipelines/{pipeline_key}/revisions/{revision}/activate" \
-H "Authorization: Bearer $WITHHUMAN_API_KEY" \
-H "If-Match: "2""{
"id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"pipeline_key": "string",
"name": "string",
"revision": 1,
"is_active": true,
"block_count": 0,
"default_escalation_path": "string",
"default_ask_agent_owner": true,
"created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"created_at": "2026-09-08T12:02:11Z",
"activated_at": "2026-09-08T12:02:11Z",
"archived_at": "2026-09-08T12:02:11Z",
"blocks": [
{
"id": "review-remaining-requests",
"snapshot_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
"name": "Review remaining requests",
"type": "outcome",
"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"
}
]
}