---
audience: engineers
summary: Drop a @checkpoint into a flow, observe the runtime suspend, and learn how
  a host resumes execution with a human decision.
title: Add a human approval step
path: tutorials/human-approval
status: published
---

Some flows shouldn't auto-complete. When the LLM's output needs human sign-off — a refund decision, an outbound message, a compliance call — `@checkpoint` is the primitive. The runtime pauses the flow, persists its scope, and surfaces a request to whatever the host wired up. Resume happens later, with a human decision flowing back into the flow.

This tutorial builds on [add an LLM call](./add-an-llm-call). It adds a `@checkpoint` between the `@flexible` and the return, asks the human to approve the greeting, and routes based on the answer.

## 1. Insert the @checkpoint

```scaicore
@flow greet(name: string): Greeting {
    candidate = @flexible {
        goal = "Generate a warm, contextual greeting for the named person"
        input = { name: name }
        output: Greeting
    }

    decision = @checkpoint approval {
        present = {
            prompt = "Approve this greeting?"
            payload = candidate
        }
        options = ["approve", "reject"]
        on_timeout = "escalate"
        timeout = "1h"
    }

    @rigid {
        return @match decision {
            "approve" => candidate
            "reject"  => { message: "Hello.", mood: :neutral }
        }
    }
}
```

Three things to notice: the `@checkpoint` is bound to `decision` so the resolution becomes a value, the `options` list constrains what the human can pick, and a `@match` block routes on the decision. `on_timeout = "escalate"` tells the runtime to surface a longer-lived signal rather than failing outright if the timeout elapses with no answer.

## 2. Run it and see SUSPENDED

```bash
scaicore run greet.scaicore --flow greet --input '{"name": "Ada"}'
```

Output:

```
  Compiling greet.scaicore...
  Running HelloWorld:greet...
  Suspended at checkpoint
  Checkpoint: 8b3f1c4d2a7e6b9c
```

The runtime ran the `@flexible` block, hit the `@checkpoint`, serialized the flow's scope (including `candidate`), and stopped. The checkpoint id is what the host needs to resume with.

## 3. Resume from the host

The CLI is invocation-only; resume happens through the host you're embedding the runtime in. The shape:

```python
from scaicore.runtime.host_types import ResumeRequest

result = engine.resume(ResumeRequest(
    checkpoint_id="8b3f1c4d2a7e6b9c",
    resolution={"decision": "approve"},
))
```

The `resolution` dict is matched against the checkpoint block's `on_response` arms (or, with no arms, bound directly to the checkpoint's `decision` variable). The flow continues from the block immediately after the `@checkpoint`. The `@rigid` block's `@match` runs, the right branch returns, and `result.status` is `COMPLETED`.

## 4. Route to a queue (v1.0.0+)

If your host has a queue system like ScaiQueue, `@checkpoint hitl_review` plus a `hitl_target` ships the decision directly to that queue, instead of through the generic checkpoint handler:

```scaicore
decision = @checkpoint hitl_review {
    options = ["approve", "reject"]
    hitl_target = {
        scope = "greeting-reviewers"
        queue = "approvals"
        hitl_spec = {
            sections = [
                { title: "Greeting", body: candidate.message },
                { title: "Mood",     body: candidate.mood }
            ]
            timeout_s = 3600
        }
    }
}
```

The runtime evaluates `hitl_target` and passes it to the host's checkpoint handler. The host is expected to publish a `hitl_request` to the addressed `(scope, queue)` carrying `hitl_spec`, subscribe to its completion event, and invoke `engine.resume` when the message resolves. See [the changelog](../changelog) for the full v1.0.0 wire-up.

## What you gained

A flow that doesn't auto-complete on the model's output. The runtime persists everything before the pause; whatever in-flight LLM context, memory, or partial work the flow had is restored intact on resume. Combined with `:entity` instance mode, you get a per-entity workflow that can wait days for a human and pick up exactly where it left off.
