Skip to Content
MarshalHuman-in-the-loop

Human-in-the-loop (HITL)

HITL answers “when must a person decide?” Agents pause for approval where policy requires it; every verdict is audited.

How it works

  1. Trigger from your agent — call Marshal SDK HITL helpers (harness.hitl().ask(...), request_approval(...), or a framework tool that wraps them). The run blocks and polls; there are no webhooks.
  2. Decide in the console — the request lands in the dashboard Marshal → HITL Approvals inbox. An operator approves, denies, answers free text, or picks an option (with an optional comment).
  3. Resumeask(...) returns the resolution so your code continues on the approved / rejected / timed-out path.

When to use it

Gate irreversible or high-blast-radius actions (deploys, secrets, payments). Keep low-risk work autonomous.

Where humans decide

ChannelGateWhere the human decides
Production agentsMarshal HITL SDK helpers + guardrailsMarshal → HITL Approvals inbox (approve / deny)
IDE coding agentsRelay Soft askConfirm in the editor before side effects
Prompt / skill promotionEvals review queuesHuman review before traffic shifts

Designing good gates

  • Gate by risk, not by default
  • Give the approver the exact action and context
  • Set timeout behavior explicitly (safe default: reject)
  • Record overrides in audit (and memory when loops would re-propose)

Trigger from the Marshal SDK

Use harness.hitl() helpers to open a request. Operators resolve it in Marshal → HITL (Approvals). Requires EXEMPLAR_API_KEYSDK install.

from exemplar_harness import Harness, HITLTimeoutError harness = Harness.from_env(agent_id="deploy-agent") hitl = harness.hitl() try: result = hitl.ask( "Deploy build 1234 to production?", description="All checks green. Blast radius: payments service.", payload={"build": "1234", "service": "payments"}, ttl_seconds=1800, # server-side expiry timeout=1800, # local wait budget (seconds) poll_interval=5, ) except HITLTimeoutError: result = None if result and result.approved: deploy() elif result and result.state == "rejected": print(f"Denied: {result.response.comment}")

Non-blocking create (still resolved in the Approvals inbox), free-text, and select:

request = hitl.request_approval("Rotate production credentials?") # Human approves/denies in Marshal → HITL; poll or cancel as needed status = hitl.get(request.request_id) if status.is_pending: hitl.cancel(request.request_id) answer = hitl.ask("Which rollback strategy?", request_type="input") choice = hitl.ask( "Pick a deployment region", request_type="select", options=["us-east-1", "eu-west-1"], )

As a tool in any agent framework:

from langchain_core.tools import tool @tool def ask_human_approval(question: str) -> str: """Ask a human operator to approve or reject an action.""" result = harness.hitl().ask(question, timeout=900) if result.approved: return "approved" return f"denied ({result.state}): {result.response.comment or 'no comment'}"

Full patterns: SDK client usage — HITL.

Live demo (Python): platform/hitl.py · Live SDK examples.

Approve or deny in the console

Open Marshal → HITL in the dashboard (Govern). Pending SDK requests appear in the Approvals inbox:

  • Approve or deny (reject) with an optional comment for the agent
  • Answer input / select requests when the agent asked for more than a yes/no
  • Refresh to pick up new requests (the panel auto-refreshes)

Until an operator decides—or the request TTL / client timeout expires—ask(...) keeps polling and the agent run stays paused.

Related: Guardrails · Relay · Evals · Client usage.

Last updated on