Skip to main content
Version: 1.0.0

Tutorial - Human interaction and approval

This complete, offline recipe distinguishes display, input, required approval, and source-level review. It also checks a real checkpoint resume. It needs an installed Touba CLI and Node.js 24, but no SDK, secret or model provider.

1. Create the two programs​

Commands below use a macOS/Linux terminal. Create the files with the displayed names inside this new directory:

Terminal
mkdir -p human-validation-demo
cd human-validation-demo
human-validation.tb
module examples.human_validation

type ChangeRequest
summary is Text

strategy RunHumanValidation
takes request as ChangeRequest
gives Text
do
show
with message as "Review required"
ask
with prompt as "Ticket?"
into ticket
human.approve
with context as request
into approved
give ticket
review.tb
module examples.human_review

type Analysis
confidence is Float
summary is Text

skill ReviewWhenNeeded
takes analysis as Analysis
gives Analysis
do
if analysis.confidence < 0.8
human.review
with context as analysis
into decision
give analysis
else
give analysis

human.approve and human.review both block until approved. Rejection stops both paths. The distinction is intent and trace mode, not enforcement strength: required_approval versus agent_review. The latter name also identifies a source-level review; it does not mean a model initiated it.

2. Derive references from the current execution​

The following script runs each stage with the public CLI, verifies its exit code and writes the actual response files. It does not invent interaction or approval references. Its approved/rejected answers are explicit test decisions, not decisions to reuse for a real business approval.

exercise.mjs
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";

function run(args, expectedExit) {
const child = spawnSync("touba", ["run", ...args, "--output", "json"], { encoding: "utf8" });
assert.equal(child.status, expectedExit, child.stderr);
return JSON.parse(child.stdout);
}
function save(path, value) {
writeFileSync(path, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 });
}
const base = ["human-validation.air.json", "--entry", "strategy:examples.human_validation/RunHumanValidation",
"--inputs-json", JSON.stringify({ request: { summary: "Publish the reviewed report" } })];
const waiting = run(base, 3);
assert.equal(waiting.reason, "user_input_pending");
assert.equal(waiting.messages[0].text, "Review required");
const interactionRef = waiting.pending_interaction.interaction_ref;
save("user-inputs.json", { [interactionRef]: "TICKET-42" });
save("ask.trace.json", waiting.trace);

const paused = run([...base, "--user-inputs", "user-inputs.json",
"--human-checkpoint-out", "human.checkpoint.json"], 3);
assert.equal(paused.reason, "human_approval_pending");
const checkpoint = JSON.parse(readFileSync("human.checkpoint.json", "utf8"));
const approvalRef = checkpoint.approval_ref;
save("human-decisions.json", { [approvalRef]: "approved" });
save("approval.trace.json", paused.trace);

const resumed = run(["human-validation.air.json", "--entry", "strategy:examples.human_validation/RunHumanValidation",
"--human-resume-checkpoint", "human.checkpoint.json", "--human-decisions", "human-decisions.json"], 0);
assert.equal(resumed.reason, "completed");
assert.equal(resumed.result, "TICKET-42");
save("resumed.trace.json", resumed.trace);

save("rejected-decisions.json", { [approvalRef]: "rejected" });
const refused = run([...base, "--user-inputs", "user-inputs.json", "--human-decisions", "rejected-decisions.json"], 4);
assert.equal(refused.reason, "human_approval_rejected");
assert.equal(refused.result, null);
save("rejected.trace.json", refused.trace);
assert.ok(!JSON.stringify(resumed.trace).includes("TICKET-42"));

const reviewArgs = ["review.air.json", "--entry", "skill:examples.human_review/ReviewWhenNeeded",
"--inputs-json", JSON.stringify({ analysis: { confidence: 0.5, summary: "Check the source" } })];
const reviewPending = run(reviewArgs, 3);
const reviewRef = reviewPending.trace.events.find(event => event.kind === "human_approval_requested").payload.approval_ref;
const reviewed = run([...reviewArgs, "--human-decisions-json", JSON.stringify({ [reviewRef]: "approved" })], 0);
assert.equal(reviewed.result.summary, "Check the source");
assert.equal(run([...reviewArgs, "--human-decisions-json", JSON.stringify({ [reviewRef]: "rejected" })], 4).reason, "human_approval_rejected");
console.log(JSON.stringify({ interactionRef, approvalRef, reviewRef, result: resumed.result }));
console.log("ASK_APPROVAL_RESUME_REJECTION_AND_REVIEW_OK");
Terminal
touba compile human-validation.tb --out human-validation.air.json
touba compile review.tb --out review.air.json
node exercise.mjs

Expected: ASK_APPROVAL_RESUME_REJECTION_AND_REVIEW_OK and result TICKET-42. Inspect the generated JSON files to see the exact references. They depend on the current AIR; adding a preceding instruction may change them. Never reuse decisions or a checkpoint after modifying the program. The runtime checks the full AIR identity before resume.

3. Understand the public results​

OutcomeCLI exitreason
waiting for an answer3user_input_pending
waiting for approval3human_approval_pending
approved and completed0completed
human rejection4human_approval_rejected

--output json separates result, messages, pending_interaction and trace. Messages and answers are business outputs; the trace records their events and references, not the raw text. A pause is not a successful run.

The human checkpoint contains business inputs and intermediate values in plain JSON. It is locally protected with restrictive permissions, not encrypted. Store it securely; do not place it in Git or public logs. Resolved secrets must not enter it. A checkpoint resumes the saved continuation rather than replaying earlier effects. Merely restarting touba run starts a new execution.

Next steps​

For a real side effect after approval, see approval before external effects. A provider can separately request request_human_review; this dynamic choice does not replace a source-level approval barrier. See governed agent decisions.