---
title: Human-in-the-loop and checkpoints
path: concepts/hitl-and-checkpoints
status: published
---

# Human-in-the-loop and checkpoints

A `@checkpoint` in ScaiCore pauses the invocation, persists its state, and waits for an external signal to resume. ScaiFlow exposes checkpoints in three flavors, all of which compile to the same underlying IR block kind.

## The three checkpoint flavors on the canvas

### `checkpoint` (plain)

Bare deterministic pause — no HITL routing, no automatic publish. Use this when you want to:

- Set a runtime breakpoint while developing (toggle `breakpoint: true` on the property panel; codegen prepends a synthetic `debug_breakpoint` checkpoint before the actual block).
- Hold execution until an external system explicitly resumes it via `POST /v1/scaicore/checkpoints/{id}/resolve`.

Config: `type` (defaults `approval`), `prompt`, `options`, `timeout`, `on_timeout`.

### `hitl_decision`

Lightweight `@checkpoint { type = "approval" }`. No preceding queue publish. Use when the agent already routed to a HITL surface (a Slack message, an in-app modal) and the human responds back via a side channel.

Config: `type`, `timeout`, `on_timeout`, `options`.

### `hitl_review`

The full HITL pattern. Compiles to a **single** `@checkpoint` block carrying `hitl_target: {scope, queue, hitl_spec}`. ScaiCore's runtime:

1. Detects `hitl_target` on the checkpoint.
2. Auto-publishes a review request to the named ScaiQueue queue.
3. Suspends the invocation, persisting state to S3.
4. Subscribes to `scaiqueue.message.completed` events for the published message.
5. On completion, deserializes the resolution and resumes the flow with the resolution available as a variable.

There is no correlation-id threading on your side, no bridge daemon, no webhook receiver. ScaiCore owns the publish + resolve cycle entirely.

Config:

- **`scope`** — ScaiQueue scope. Can be omitted to inherit from `flow.config.scaiqueue.scope`.
- **`queue`** — ScaiQueue queue slug. Required.
- **`sections`** — the form to render in the reviewer's UI. Three section types: `context` (read-only display), `input` (editable fields), `decision` (button choices). Edited via the canvas Form Builder.
- **`assignee`** — optional user/group hint.
- **`timeout`**, **`on_timeout`** — what to do if no human responds.
- **`decision_bind`** — variable name for the result; defaults `decision`.

## The HITL form (sections)

```jsonc
"sections": [
  { "type": "context", "title": "Customer", "content": "{{customer_id}}" },
  { "type": "input",
    "field": "amount",
    "kind": "currency",
    "default": "{{amount}}",
    "extras": { "confidence": "{{confidence}}" } },
  { "type": "decision",
    "field": "decision",
    "options": [
      { "id": "approve", "label": "Approve", "variant": "primary" },
      "reject"
    ] }
]
```

- **`context`** sections render read-only content. Use them to show the data a human needs to make the decision.
- **`input`** sections render editable form fields. The `kind` controls the widget (`text`, `currency`, `textarea`, …). `default` can be a template expression filled from flow variables.
- **`decision`** sections render buttons. The chosen button's `id` becomes the value of the section's `field`.

The optional `extras.confidence` is a documented convention — ScaiQueue's renderer surfaces it as a confidence indicator next to the input. (ScaiQueue's typed `HitlSpec` doesn't formally type it; it passes through under the SDK's `extras` dict.)

## Scope inheritance

`flow.config.scaiqueue.scope` is the flow-level default. Any `hitl_review`, `queue_publish`, `queue_consume`, or `queue_escalation` node that omits `scope` in its own config inherits the flow's scope at compile time. This includes the `hitl_target.scope` written into the checkpoint block.

If a node doesn't specify a queue at compile time and no flow-level scope is set, deploy will fail with a meaningful error.

## Runtime breakpoints

Set `node.config.breakpoint = true` on any node to make the runtime pause every invocation just before that node. Codegen prepends a synthetic `checkpoint_type: "debug_breakpoint"` block with `node_id: "{original_id}__breakpoint"` so the canvas can correlate paused checkpoints back to canvas nodes.

The canvas's Live Runs panel has a `<PendingBreakpointsBar>` that shows currently-paused executions. Click "Continue" to resume; the underlying call is `POST /v1/scaicore/checkpoints/{id}/resolve` with `{"resolution": "continue"}`.

`debug_breakpoint` checkpoints and `hitl_review` checkpoints share the underlying IR primitive but render different UIs — the bar filters by `checkpoint_type`.

## Resolving a checkpoint via the API

Any pending checkpoint can be resolved via the ScaiFlow proxy at `POST /v1/scaicore/checkpoints/{checkpoint_id}/resolve`:

```bash
curl -X POST "https://scaiflow.example/api/v1/scaicore/checkpoints/${CHECKPOINT_ID}/resolve" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"resolution": "approve", "comment": "Within team budget."}'
```

The proxy requires `scaicore:manage` permission (resuming has side effects). It forwards to ScaiGrid's `/v1/modules/scaicore/checkpoints/{id}/resolve`.

For HITL Review checkpoints, set `resolution` to whatever the decision section's `id` was (e.g. `"approve"`, `"reject"`). For debug breakpoints, use `"continue"`.
