---
title: Handle human review
description: Build a review queue on the API — find paused executions, claim them, approve or deny with a note, act in bulk, and reconcile with the audit trail.
canonical_url: "https://cascades.crscreditapi.com/docs/guides/handle-human-review"
doc_version: "1.0.0"
last_updated: "2026-10-05T19:24:37Z"
source: docs/guides/handle-human-review.md.erb
---

# Handle human review

When a cascade routes an applicant to human review, the execution stops at `awaiting_review` and waits. This guide builds the loop your reviewers need: a queue, a claim, a decision with a note, and the trail to prove it. The concepts are in [Human review](/docs/concepts/human-review).


### Find what is waiting

Executions that need a person are `needs_human_review: true` with `review_status: in_review`. Filter for them, newest first:


**cURL**

```shell
curl "https://cascades.crscreditapi.com/api/v1/executions" \
  -H "Authorization: Bearer {{API_KEY}}"
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions", {
  method: "GET",
  headers: {
    "Authorization": "Bearer {{API_KEY}}"
  }
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.get(
    "https://cascades.crscreditapi.com/api/v1/executions",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "https://cascades.crscreditapi.com/api/v1/executions", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "List executions using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call GET /executions with curl.
```


Add `filter[workflow_id]` to build one queue per cascade, and use `assigned_to_user_id` to show who holds each item. Poll this list, or maintain it from `execution.*` webhooks plus a status check.

### Claim the review

Claim before you show the case to a reviewer so two people never decide the same execution:


**cURL**

```shell
curl -X POST "https://cascades.crscreditapi.com/api/v1/executions/1842/review_assignment" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "assignee": "me"
  }'
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions/1842/review_assignment", {
  method: "POST",
  headers: {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "assignee": "me"
  })
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.post(
    "https://cascades.crscreditapi.com/api/v1/executions/1842/review_assignment",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
    json={
      "assignee": "me"
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions/1842/review_assignment")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"
request["Content-Type"] = "application/json"
request.body = <<~JSON
  {
    "assignee": "me"
  }
JSON

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := []byte(`{
	  "assignee": "me"
	}`)
	req, err := http.NewRequest("POST", "https://cascades.crscreditapi.com/api/v1/executions/1842/review_assignment", bytes.NewBuffer(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "Claim or unassign a review 1842 using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call POST /executions/{id}/review_assignment with curl.
```


`{ "assignee": "me" }` claims for the user behind the key; `{ "assignee": null }` releases. A review held by someone else answers `403 review_assigned_elsewhere` on decision. Each API client maps to one user, so give each reviewer their own client if you need per-person attribution.

### Show the reviewer the evidence

`GET /executions/{id}` with `executions:read:full` returns `step_outputs` — the report fields and the conditions that sent the applicant to review — and `GET /executions/{id}/report_artifacts` lists the report PDFs. That is what a reviewer reads before deciding.

### Record notes as you go

Notes become `note_added` events in the audit trail; they are not editable afterwards:


**cURL**

```shell
curl -X POST "https://cascades.crscreditapi.com/api/v1/executions/1842/review_notes" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "note": "Pulled credit report manually; matches OFAC cleanly."
  }'
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions/1842/review_notes", {
  method: "POST",
  headers: {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "note": "Pulled credit report manually; matches OFAC cleanly."
  })
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.post(
    "https://cascades.crscreditapi.com/api/v1/executions/1842/review_notes",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
    json={
      "note": "Pulled credit report manually; matches OFAC cleanly."
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions/1842/review_notes")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"
request["Content-Type"] = "application/json"
request.body = <<~JSON
  {
    "note": "Pulled credit report manually; matches OFAC cleanly."
  }
JSON

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := []byte(`{
	  "note": "Pulled credit report manually; matches OFAC cleanly."
	}`)
	req, err := http.NewRequest("POST", "https://cascades.crscreditapi.com/api/v1/executions/1842/review_notes", bytes.NewBuffer(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "Append a review note 1842 using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call POST /executions/{id}/review_notes with curl.
```


### Approve or deny


**cURL**

```shell
curl -X POST "https://cascades.crscreditapi.com/api/v1/executions/1842/review_decisions" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "approved",
    "note": "Manual review confirmed; OFAC clean."
  }'
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions/1842/review_decisions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "decision": "approved",
    "note": "Manual review confirmed; OFAC clean."
  })
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.post(
    "https://cascades.crscreditapi.com/api/v1/executions/1842/review_decisions",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
    json={
      "decision": "approved",
      "note": "Manual review confirmed; OFAC clean."
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions/1842/review_decisions")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"
request["Content-Type"] = "application/json"
request.body = <<~JSON
  {
    "decision": "approved",
    "note": "Manual review confirmed; OFAC clean."
  }
JSON

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := []byte(`{
	  "decision": "approved",
	  "note": "Manual review confirmed; OFAC clean."
	}`)
	req, err := http.NewRequest("POST", "https://cascades.crscreditapi.com/api/v1/executions/1842/review_decisions", bytes.NewBuffer(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "Approve or deny a paused review 1842 using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call POST /executions/{id}/review_decisions with curl.
```


- `approved` resumes down the decision's `pass_steps`.
- `denied` resumes down the decision's `fail_steps`. If an adverse-action node lives in `human_review_steps`, its draft was already prepared when the execution was flagged; `auto_send` does not apply to that placement — a person sends the notice from the app.

In test mode the branch runs inline and you get `200` with the final state. In live mode you get `202` with `resume_enqueued: true`; poll until `status` leaves `awaiting_review`, or wait for `execution.completed`. A second decision on the same review is `409 review_already_decided`.

### Reconcile with the audit trail


**cURL**

```shell
curl "https://cascades.crscreditapi.com/api/v1/executions/1842/review_events" \
  -H "Authorization: Bearer {{API_KEY}}"
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions/1842/review_events", {
  method: "GET",
  headers: {
    "Authorization": "Bearer {{API_KEY}}"
  }
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.get(
    "https://cascades.crscreditapi.com/api/v1/executions/1842/review_events",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions/1842/review_events")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	req, err := http.NewRequest("GET", "https://cascades.crscreditapi.com/api/v1/executions/1842/review_events", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "List review audit events 1842 using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call GET /executions/{id}/review_events with curl.
```


Events arrive oldest first: `flagged` (when the cascade paused), `assigned` / `unassigned`, `note_added`, `approved` or `denied`, and `re_evaluated` if the run was replayed. When an adverse-action node parks a notice draft for review, the trail adds `adverse_action_drafted`, then `adverse_action_sent` or `adverse_action_skipped`; `adverse_action_replayed` marks a sent notice that was resent from the app. Each carries `actor_user_id` and `created_at`. Store the trail with your own case record; it is the record of who decided what and when.


## Deciding in bulk

For end-of-day sweeps, `POST /executions/review_bulk_actions` applies `approve`, `deny` or `assign_to_me` to up to 25 ids and reports what it could not do:


**cURL**

```shell
curl -X POST "https://cascades.crscreditapi.com/api/v1/executions/review_bulk_actions" \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Content-Type: application/json" \
  -d '{
    "execution_ids": [
      1842,
      1843,
      1844,
      1845,
      1846
    ],
    "bulk_action": "approve",
    "note": "Manual review batch 2026-09-09 — OFAC and credit clean."
  }'
```

**Node.js**

```javascript
const response = await fetch("https://cascades.crscreditapi.com/api/v1/executions/review_bulk_actions", {
  method: "POST",
  headers: {
    "Authorization": "Bearer {{API_KEY}}",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "execution_ids": [
      1842,
      1843,
      1844,
      1845,
      1846
    ],
    "bulk_action": "approve",
    "note": "Manual review batch 2026-09-09 — OFAC and credit clean."
  })
});

const { data, meta } = await response.json();
console.log(response.status, data, meta.request_id);
```

**Python**

```python
import requests

response = requests.post(
    "https://cascades.crscreditapi.com/api/v1/executions/review_bulk_actions",
    headers={
        "Authorization": "Bearer {{API_KEY}}",
    },
    json={
      "execution_ids": [
        1842,
        1843,
        1844,
        1845,
        1846
      ],
      "bulk_action": "approve",
      "note": "Manual review batch 2026-09-09 — OFAC and credit clean."
    },
)
response.raise_for_status()
payload = response.json()
print(payload["data"], payload["meta"]["request_id"])
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://cascades.crscreditapi.com/api/v1/executions/review_bulk_actions")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer {{API_KEY}}"
request["Content-Type"] = "application/json"
request.body = <<~JSON
  {
    "execution_ids": [
      1842,
      1843,
      1844,
      1845,
      1846
    ],
    "bulk_action": "approve",
    "note": "Manual review batch 2026-09-09 — OFAC and credit clean."
  }
JSON

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
payload = JSON.parse(response.body)
puts payload["data"], payload.dig("meta", "request_id")
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := []byte(`{
	  "execution_ids": [
	    1842,
	    1843,
	    1844,
	    1845,
	    1846
	  ],
	  "bulk_action": "approve",
	  "note": "Manual review batch 2026-09-09 — OFAC and credit clean."
	}`)
	req, err := http.NewRequest("POST", "https://cascades.crscreditapi.com/api/v1/executions/review_bulk_actions", bytes.NewBuffer(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer {{API_KEY}}")
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

**Agent**

```shell
claude mcp add cascades \
  -e CASCADES_API_KEY={{API_KEY}} \
  -e CASCADES_PROD_BASE_URL=https://cascades.crscreditapi.com \
  -- npx -y @crscreditapi/cascades-mcp-server

# Then ask your assistant:
#   "Bulk approve / deny / assign_to_me using the Cascades API."
# No MCP tool covers this endpoint yet; the assistant can call POST /executions/review_bulk_actions with curl.
```


```json
{
  "data": {
    "object": "bulk_review_result",
    "applied": 4,
    "enqueued": 4,
    "skipped": [
      { "id": 1846, "reason": "assigned_elsewhere" }
    ]
  },
  "meta": { "request_id": "…" }
}
```

Bulk decisions are always asynchronous, even in test mode. Ids from another tenant come back as `not_found`.

## Related

- [Human review](/docs/concepts/human-review)
- [Adverse action](/docs/concepts/adverse-action) — what a denial triggers

## Sitemap

See the full [sitemap](https://cascades.crscreditapi.com/sitemap.md) for all pages.
