Tide

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 source

Tide 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 fs

A 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 configured actions; when the request leaves actions empty, any non-empty string is accepted.
  • --payload <json> and --comment <text> are optional; they become decision.payload and decision.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.

  • tide serve — inspect and resolve waits in the embedded operator console or task-specific HTTP API.
  • CLI referencerun and resume flags.
  • Workflow API — the hitl surface.

On this page