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

Approval flow

This tutorial wires a Core that pauses execution at an approval block, routes the checkpoint to a reviewer, lets them approve or reject it via the API, and resumes the program. We won't write any DSL — the language side of authoring an approval block is documented at /docs/scaicore. What we do here is the wrapper-side wiring.

Roughly 20 minutes if you have a compiled .scaicore-ir bundle that contains at least one approval checkpoint.

1. Confirm your bundle has a checkpoint block#

After parsing the bundle:

bash
1
2
3
4
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/cores/parse-bundle" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -F "file=@approval-agent.scaicore-ir" \
  | jq '.data.summary'

The summary doesn't list checkpoints explicitly, but if the agent emits a checkpoint block at runtime, the wrapper will catch it once the Core is running. If you're not sure, run the bundle through the standalone ScaiCore runtime first.

2. Create the Core and pick a routing strategy#

Two reasonable defaults for an approval flow:

  • Route to a group (assignee set to group:approvers inside the program). Best when many people share responsibility.
  • Route to a role (assignee set to role:tenant_admin). Best when responsibility follows seniority.

The Core itself doesn't care — the routing string is baked into the IR. What the wrapper exposes is the checkpoint_mode field, which decides how aggressive ScaiGrid is about creating rows for every HITL pause:

  • auto — every checkpoint produces a row (default; what you want here).
  • visual_queue — only checkpoints that pass a visibility filter.
  • routed — only checkpoints with a resolved assignee; unrouted ones are auto-cancelled.
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import httpx, os

core = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaicore/cores",
    headers={"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"},
    json={
        "name": "Approval Agent",
        "runtime_mode": "event_driven",
        "concurrency_mode": "stateless",
        "source": parsed["source"],
        "checkpoint_mode": "auto",
    },
).json()["data"]
javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const res = await fetch(`${process.env.SCAIGRID_HOST}/v1/modules/scaicore/cores`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Approval Agent",
    runtime_mode: "event_driven",
    concurrency_mode: "stateless",
    source: parsed.source,
    checkpoint_mode: "auto",
  }),
});
const { data: core } = await res.json();

3. Start the Core#

bash
1
2
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/cores/$CORE_ID/start" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

Status moves through starting to running. The wrapper resolves environment variables (decrypting any secrets), resolves identity (service-account by default), and composes the skill preamble before the engine is registered.

4. Trigger the program#

How the program runs depends on its runtime mode. For an event-driven approval agent, send the trigger event:

bash
1
2
3
4
5
6
7
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/cores/$CORE_ID/events" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "purchase.request",
    "data": { "amount": 1250, "requester": "alice@acme" }
  }'

The event is accepted and routed to the engine; when the program hits its approval block, execution suspends and a checkpoint row is written.

5. Find the pending checkpoint#

The assignee discovers it by listing their own pending work:

python
1
2
3
4
5
6
pending = httpx.get(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaicore/checkpoints",
    headers={"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"},
).json()["data"]
for cp in pending:
    print(cp["id"], cp["prompt"], cp["priority"])

A tenant admin can see every pending checkpoint in the tenant via /checkpoints/all. Both endpoints return cursor-paginated results.

Fetch the full payload:

bash
1
2
curl "$SCAIGRID_HOST/v1/modules/scaicore/checkpoints/$CP_ID" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

You'll see the prompt, the canonical options (if any), the parsed assignee, the priority, the expiry settings, and the program-attached context.

6. Resolve it#

bash
1
2
3
4
5
6
7
8
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/checkpoints/$CP_ID/resolve" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "approve",
    "response_data": { "amount_approved": 1250 },
    "comment": "Within department limits."
  }'

The row's status flips to resolved. The CoreEngine picks up the resolution from its state blob and resumes execution.

To reject instead:

json
1
{ "decision": "reject", "comment": "Above department limit." }

To send it to someone else without resolving:

bash
1
2
3
4
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/checkpoints/$CP_ID/reassign" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assignee": "user:bob@acme", "comment": "Above my pay grade." }'

Reassignment re-runs the assignee parser and re-fires the notifier.

7. Inspect the history#

bash
1
2
curl "$SCAIGRID_HOST/v1/modules/scaicore/checkpoints/$CP_ID/history" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"

You get a compact event list — created, notification sent, resolved. For a full audit trail of who saw the checkpoint, query ScaiGrid's audit-events pipeline with module=scaicore.

Patterns#

  • Expiry with default. Set expiry_action: "default_option" so unattended checkpoints auto-resolve with a known-safe decision instead of blocking the program forever.
  • Escalate when stale. Set expiry_action: "escalate" + escalation_target: "role:tenant_admin" to bounce stale rows to a wider audience without losing them.
  • ScaiQueue for human-review workflows. Programs that publish hitl_request messages into ScaiQueue get the wrapper's queue↔checkpoint loop closer for free — completed queue messages auto-resolve the matching checkpoint, idempotently.
  • Frozen skill versions. If the Core has bound ScaiSkills, the resolved set is frozen into the checkpoint context. The resume reads those pinned versions back, so a yanked skill mid-pause still lets the existing checkpoint complete.
Updated 2026-05-18 15:01:29 View source (.md) rev 11