Guides
Handle human review
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.
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.
-
Find what is waiting
Executions that need a person are
needs_human_review: truewithreview_status: in_review. Filter for them, newest first:cURLcurl "https://cascades.crscreditapi.com/api/v1/executions" \ -H "Authorization: Bearer {{API_KEY}}"Node.jsconst 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);Pythonimport 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"])Rubyrequire "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")Gopackage 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)) }Agentclaude 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 useassigned_to_user_idto show who holds each item. Poll this list, or maintain it fromexecution.*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:
cURLcurl -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.jsconst 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);Pythonimport 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"])Rubyrequire "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")Gopackage 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)) }Agentclaude 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 answers403 review_assigned_elsewhereon decision. Each API client maps to one user, so give each reviewer their own client if you need per-person attribution. -
Record notes as you go
Notes become
note_addedevents in the audit trail; they are not editable afterwards:cURLcurl -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.jsconst 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);Pythonimport 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"])Rubyrequire "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")Gopackage 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)) }Agentclaude 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
cURLcurl -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.jsconst 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);Pythonimport 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"])Rubyrequire "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")Gopackage 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)) }Agentclaude 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.approvedresumes down the decision'spass_steps.deniedresumes down the decision'sfail_steps. If an adverse-action node lives inhuman_review_steps, its draft was already prepared when the execution was flagged;auto_senddoes not apply to that placement — a person sends the notice from the app.
In test mode the branch runs inline and you get
200with the final state. In live mode you get202withresume_enqueued: true; poll untilstatusleavesawaiting_review, or wait forexecution.completed. A second decision on the same review is409 review_already_decided. -
Reconcile with the audit trail
cURLcurl "https://cascades.crscreditapi.com/api/v1/executions/1842/review_events" \ -H "Authorization: Bearer {{API_KEY}}"Node.jsconst 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);Pythonimport 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"])Rubyrequire "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")Gopackage 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)) }Agentclaude 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,approvedordenied, andre_evaluatedif the run was replayed. When an adverse-action node parks a notice draft for review, the trail addsadverse_action_drafted, thenadverse_action_sentoradverse_action_skipped;adverse_action_replayedmarks a sent notice that was resent from the app. Each carriesactor_user_idandcreated_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 -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."
}'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);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"])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")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))
}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.{
"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
- Adverse action — what a denial triggers