---
summary: "End-to-end recipe \u2014 write a Core that pauses for human approval, route\
  \ the checkpoint to a reviewer, resolve it, and watch the program resume."
title: Approval flow
path: modules/scaicore/tutorials/approval-flow
status: published
---

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](https://www.scailabs.ai/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
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
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
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
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
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
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
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
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
{ "decision": "reject", "comment": "Above department limit." }
```

To send it to someone else without resolving:

```bash
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
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.
