Human-in-the-loop
Suspend a workflow for a human decision, then resume it.
A workflow can pause for a human decision with hitl.wait(...). In an
interactive terminal, tide run displays the task, records the person's
decision, and immediately continues the same invocation. In non-interactive
use, the run returns WaitingHuman and remains parked until a person resolves
the task with tide resume. Replay returns to the suspension point with the
decision in hand — journaled work before the wait is not reissued.
Waiting for a decision
hitl.wait(request) describes the task a human must act on and returns their
decision:
const schema = S.object({ documentId: S.text() });
export default {
schema,
main: async (ctx: Infer<typeof schema>) => {
const decision = await hitl.wait({
title: `Review extracted evidence for ${ctx.documentId}`,
description: "Approve or deny the extracted evidence.",
actions: ["approved", "rejected"],
tags: ["evidence-review"],
priority: "high",
moduleName: "evidence_review",
payload: { summary: "", review_notes: "" },
classification: "evidence",
});
log("human action:", decision.action);
log("payload:", JSON.stringify(decision.payload));
await writeFile("decision.json", JSON.stringify(decision, null, 2));
},
};The request shape (title and moduleName are required; description,
actions, tags, priority, payload, classification, … are optional) is the
HitlWaitRequest type. The returned HumanWaitOutcome carries action,
payload, comment, and provenance (resolvedBy, resolvedAt). Local CLI and
serve resolutions currently record the nil UUID for resolvedBy because these
surfaces do not yet authenticate an operator identity.
When actions is non-empty, first-class control surfaces such as the tide serve console present and enforce that closed set. Omitting it preserves an
open action contract.
With defineWorkflow, the same surface is on the context as ctx.hitl.
Running and suspending
Run the workflow as usual:
tide run review.ts -i review-001 --input '{"documentId":"doc-42"}'When it hits hitl.wait in a terminal, Tide prints the complete task request
(including its id, ordinal, module, description, metadata, and payload
expectations), then prompts:
Human decision required
Task ID: …
Wait ordinal: 0
Module: evidence_review
Title: Review extracted evidence for doc-42
...
Action: approved
Payload JSON (optional, Enter for {}): {"summary":"looks correct"}
Comment (optional): verified against sourceTide appends the validated decision and resumes under the same invocation id, store instance, and backend. If the workflow reaches another wait, the prompt repeats. The default process-local memory store therefore supports sequential interactive waits without writing workflow state to disk.
When actions contains a finite list, Tide renders numbered choices and accepts
either a number or action name. With no list, the action remains an open-ended
non-empty string. Payload must be valid JSON; invalid input is rejected and
prompted again. Pressing Ctrl-C or sending EOF records no partial decision.
Suspending without a prompt
Tide never prompts with --json, --no-interactive, or when stdin or stdout is
not a terminal. It returns the WaitingHuman result immediately:
tide run review.ts -i review-001 --store fs --no-interactive
# suspended: waiting for human task … (wait #0)
# resume with: tide resume review-001 --action <action> review.ts --store fsA suspended run is not a failure — its exit code is 0. Persistent stores
keep the journal and any embedded Argon fork available for a later tide resume. A memory-backed suspended run warns that it cannot be resumed after the
process exits.
Resuming
Record the human's decision and continue:
tide resume review-001 review.ts --action approved \
--payload '{"summary":"looks correct","review_notes":"ok"}' \
--comment "verified against source"<invocation-id>and the workflow file are positional.--action <action>is required. It must match one of the request's configuredactions; when the request leavesactionsempty, any non-empty string is accepted.--payload <json>and--comment <text>are optional; they becomedecision.payloadanddecision.comment.
tide resume appends the resolution to the journal, replays the run up to the
hitl.wait, and resumes from there with the outcome. Multiple waits in one
workflow resolve in order — each tide resume resolves the lowest outstanding
wait.
Default posture and capability wiring
The hitl capability is provided by the host. The shipped tide binary wires a
local HITL service so hitl.wait suspends for tide resume as shown above. In
the argon-free runtime with capabilities disabled, hitl.wait refuses
deterministically rather than suspending.
Driving it programmatically
tide run --json makes suspension machine-readable and never prompts: a
suspended run emits "status": "waiting_human" with a pendingHumanWait
object (the task id and wait ordinal) and the invocation id, so an agent can
issue the matching tide resume. See Driving Tide from an
agent.
Related
- tide serve — inspect and resolve waits in the embedded operator console or task-specific HTTP API.
- CLI reference —
runandresumeflags. - Workflow API — the
hitlsurface.