Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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:

bash
1
2
3
4
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"}'
bash
1
2
3
4
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.

bash
 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:

bash
1
2
3
4
5
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.

python
 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"])
javascript
 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;
bash
1
2
3
4
5
6
7
8
9
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:

json
1
2
3
4
5
6
7
{
  "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:

bash
1
2
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.
Updated 2026-05-18 15:01:32 View source (.md) rev 12