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
- 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. - 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).
- Resume —
ask(...)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
| Channel | Gate | Where the human decides |
|---|---|---|
| Production agents | Marshal HITL SDK helpers + guardrails | Marshal → HITL Approvals inbox (approve / deny) |
| IDE coding agents | Relay Soft ask | Confirm in the editor before side effects |
| Prompt / skill promotion | Evals review queues | Human 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_KEY — SDK install.
Python
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.