Add a human review step
You're going to add a human-in-the-loop step to an otherwise automated flow: a publisher attaches a hitl_spec to messages, the admin UI renders them as review forms, a reviewer submits a decision, and your consumer reads the decision off the completed message. About 15 minutes.
1. Pick a scope and a review queue
You need a scope and a dedicated queue where review-needing messages land. Either reuse an existing scope or:
| curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes" \
-H "Authorization: Bearer $SCAIGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug": "moderation", "display_name": "Moderation"}'
|
| curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE/queues" \
-H "Authorization: Bearer $SCAIGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug": "review-pending", "display_name": "Review Pending", "ordering": "fifo"}'
|
Reviewers will see this queue under "Queues → Messages" in the admin UI.
2. Register a reusable HITL pattern
Patterns live in the scope's pattern registry; publishers reference them by name and pass parameters. This keeps the rendering spec in one curated place rather than scattered through publish calls.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE/hitl-patterns" \
-H "Authorization: Bearer $SCAIGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "moderation-decision",
"version": "1.0",
"spec": {
"sections": [
{"type": "context", "title": "Submission", "content": "{{summary}}"},
{"type": "context", "title": "Reason flagged", "content": "{{flag_reason}}"},
{"type": "decision", "field": "decision",
"options": ["approve", "reject", "needs_more_info"]},
{"type": "input", "field": "note", "kind": "text", "required": false}
],
"timeout_s": 86400
}
}'
|
Preview the expanded spec with sample parameters before any real publishes:
| curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE/hitl-patterns/moderation-decision/expand" \
-H "Authorization: Bearer $SCAIGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version": "1.0",
"parameters": {"summary": "User posted: ...", "flag_reason": "automated-classifier-low-confidence"}}'
|
3. Publish a review-needing message
Attach the expanded spec to the message as hitl_spec. In practice your publisher would expand the pattern server-side, then publish.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | import httpx, os
H = {"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}", "Content-Type": "application/json"}
HOST = os.environ["SCAIGRID_HOST"]
SCOPE, QUEUE = os.environ["SCOPE_ID"], os.environ["QUEUE_ID"]
spec = httpx.post(
f"{HOST}/v1/modules/scaiqueue/scopes/{SCOPE}/hitl-patterns/moderation-decision/expand",
headers=H,
json={"version": "1.0",
"parameters": {"summary": "User posted: ...", "flag_reason": "low-confidence"}},
).json()["data"]["expanded"]
msg = httpx.post(
f"{HOST}/v1/modules/scaiqueue/scopes/{SCOPE}/queues/{QUEUE}/messages",
headers=H,
json={
"type": "moderation.review",
"body": {"submission_id": "sub_42"},
"labels": {"queue": "moderation"},
"correlation_id": "sub_42",
"hitl_spec": spec,
},
).json()["data"]
print("waiting for review:", msg["id"])
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | const expand = await (await fetch(
`${HOST}/v1/modules/scaiqueue/scopes/${SCOPE}/hitl-patterns/moderation-decision/expand`,
{ method:"POST", headers:H,
body:JSON.stringify({version:"1.0", parameters:{summary:"User posted: ...", flag_reason:"low-confidence"}})}
)).json();
const msg = (await (await fetch(
`${HOST}/v1/modules/scaiqueue/scopes/${SCOPE}/queues/${QUEUE}/messages`,
{ method:"POST", headers:H, body:JSON.stringify({
type:"moderation.review",
body:{submission_id:"sub_42"},
correlation_id:"sub_42",
hitl_spec: expand.data.expanded,
})}
)).json()).data;
|
| curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE/queues/$QUEUE/messages" \
-H "Authorization: Bearer $SCAIGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "moderation.review",
"body": {"submission_id": "sub_42"},
"correlation_id": "sub_42",
"hitl_spec": { "sections": [ ... ], "timeout_s": 86400 }
}'
|
The message is now pending in the review queue. The admin UI's /scaiqueue/messages view renders it as a structured form using the spec.
4. Reviewer completes the message
When a reviewer submits the form, the admin UI calls POST /scopes/{scope_id}/messages/{msg_id}/complete with a response body carrying the decision and any inputs:
| {
"response": {
"decision": "approve",
"note": "Marginal but on-policy.",
"reviewer_id": "usr_jane"
}
}
|
The message moves to completed. ScaiGrid publishes a scaiqueue.message.completed event on the internal event bus with the original correlation_id and the response payload — so any worker waiting on this submission can react without polling.
5. Wire the consumer
The consumer that started this flow either listens for scaiqueue.message.completed events on the event bus, or polls the message back out:
| curl "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE/queues/$QUEUE/messages/$MSG_ID" \
-H "Authorization: Bearer $SCAIGRID_API_KEY"
|
The full message representation includes the response field with the reviewer's decision.
Patterns that work
- One pattern per decision type. Don't templatize a generic "form"; make one pattern per kind of decision (
refund-approval, content-moderation, kyc-review). Reviewers learn the layout faster.
- Always set a timeout_s in the spec. A pending review that no one notices for two weeks is worse than an auto-rejected one. Use
_dead_letter plus a follow-up rule to handle silent reviews.
- Carry the correlation_id all the way through. Trace a single submission across automated → review → automated stages by querying
GET /scopes/{scope_id}/messages?correlation_id=....
- Permission split. Reviewers need
scaiqueue:consume; publishers need scaiqueue:publish. Maintainers of the pattern registry need scaiqueue:manage.