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:
1 2 3 4 | |
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 (
assigneeset togroup:approversinside the program). Best when many people share responsibility. - Route to a role (
assigneeset torole: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;unroutedones are auto-cancelled.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
3. Start the Core#
1 2 | |
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:
1 2 3 4 5 6 7 | |
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:
1 2 3 4 5 6 | |
A tenant admin can see every pending checkpoint in the tenant via /checkpoints/all. Both endpoints return cursor-paginated results.
Fetch the full payload:
1 2 | |
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#
1 2 3 4 5 6 7 8 | |
The row's status flips to resolved. The CoreEngine picks up the resolution from its state blob and resumes execution.
To reject instead:
1 | |
To send it to someone else without resolving:
1 2 3 4 | |
Reassignment re-runs the assignee parser and re-fires the notifier.
7. Inspect the history#
1 2 | |
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_requestmessages 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.