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

Add a human approval step

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. 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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
1
scaicore run greet.scaicore --flow greet --input '{"name": "Ada"}'

Output:

text
1
2
3
4
  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
1
2
3
4
5
6
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 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.

Updated 2026-05-18 00:59:53 View source (.md) rev 4