LangGraph
Pause LangGraph agents for human approval before sensitive actions.
LangGraph pauses agents with interrupt(HumanInterrupt) and resumes with Command({ resume: HumanResponse }). HITLy provides the approval inbox and resume API.
How it works with HITLy
- A graph node calls
interrupt(HumanInterrupt)and POSTs to HITLy/api/v1/approvals. - HITLy stores an envelope and shows it in the inbox.
- A reviewer accepts, edits, or rejects in the HITLy inbox.
- HITLy pauses the agent, takes the decision, and resumes the original thread with signed proof:
- LangGraph Platform —
POST /threads/{threadId}/runs/waitwith{ command: { resume: HumanResponse } }
- LangGraph Platform —
- The graph verifies the signature and checks
response.type:accept→ proceed with the actionignore(HITLy reject) → bail without taking the actionedit→ apply edited arguments, then proceed
Agent Inbox / hitl.sh collect an answer; HITLy pauses the agent, takes a decision, resumes the original run with proof. Origins keep interrupt(); HITLy owns the envelope and the signed resume.
See the runnable project in examples/langgraph (refund graph with FastAPI server).
Setup
Create a workspace, API key, and LangGraph connection (deployment URL + token).
Set env:
HITLY_API_URL=http://localhost:3001
HITLY_API_KEY=hitly_...
HITLY_PROJECT_ID=prj_...
HITLY_RESUME_SECRET=hitly_resume_...
LANGGRAPH_BASE_URL=http://127.0.0.1:2024Integration guide (Python)
1. Install dependencies
pip install langgraph langchain-core httpx2. Create HITLy helper module
Copy examples/langgraph/src/hitly.py (or write it yourself):
from hitly import HitlyApprovalConfig, notify_hitly_approval, verify_hitly_resume
# Initialize config (validates env vars)
config = HitlyApprovalConfig()
# In your graph node:
await notify_hitly_approval(
config=config,
thread_id=thread_id,
graph_id="refund-graph",
action={"action": "send-refund", "args": {"orderId": "OR-1234", "amount": 123.45}},
description="Refund approval required",
)
# After interrupt:
try:
verify_hitly_resume(human_response, run_id=thread_id, secret=config.resume_secret, required=True)
except HitlyResumeError as e:
# Reject spoofed resume
return bail_or_reject()3. Graph node example
from langgraph.graph import StateGraph
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from langgraph.prebuilt import task
from hitly import HitlyApprovalConfig, notify_hitly_approval, verify_hitly_resume, HitlyResumeError
async def approval_node(state, config):
thread_id = config["configurable"]["thread_id"]
hitly_config = HitlyApprovalConfig()
# Idempotent notify
@task
async def notify_once():
await notify_hitly_approval(
config=hitly_config,
thread_id=thread_id,
graph_id="refund-graph",
action={"action": "send-refund", "args": {"orderId": state["order_id"], "amount": state["amount"]}},
description=f"Refund of ${state['amount']} requires approval",
# Evidence fields
system_id="refund-graph-prod",
inventory_id="ai-inv-refund-graph-v1",
policy_id="refund-over-100",
risk_tier="high" if state["amount"] > 1000 else "medium",
)
await notify_once()
# Interrupt and wait for HITL<sub><i>y</i></sub> resume
human_response = interrupt(
HumanInterrupt(
action_request={"action": "send-refund", "args": {...}},
config={"allow_accept": True, "allow_ignore": True},
description="Refund approval required",
)
)
# Verify signed proof (fail-closed: guessed threadId rejected)
try:
verify_hitly_resume(human_response, run_id=thread_id, secret=hitly_config.resume_secret, required=True)
except HitlyResumeError as e:
return {**state, "approved": False, "rejection_reason": f"Resume verification failed: {e}"}
# Check resume type
if human_response.get("type") == "ignore":
return {**state, "approved": False} # HITL<sub><i>y</i></sub> reject: do not issue refund
elif human_response.get("type") == "accept":
return {**state, "approved": True}
return {**state, "approved": True}
# Build graph with checkpointer
builder = StateGraph(RefundState)
builder.add_node("approval", approval_node)
# ... add edges ...
checkpointer = MemorySaver()
refund_graph = builder.compile(checkpointer=checkpointer)4. FastAPI resume route
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from langgraph.types import Command
app = FastAPI()
class ResumeRequest(BaseModel):
assistant_id: str | None = None
command: dict
@app.post("/threads/{thread_id}/runs/wait")
async def resume_thread(thread_id: str, req: ResumeRequest):
if thread_id not in threads:
raise HTTPException(status_code=404, detail="Thread not found")
config = threads[thread_id]["config"]
resume_value = req.command.get("resume")
# Resume the graph with HumanResponse
result = await refund_graph.ainvoke(Command(resume=resume_value), config)
threads[thread_id]["state"] = result
return {"thread_id": thread_id, "status": "completed", "state": result}Running locally
yarn workspace @hitly/app dev— HITLy on port 3001.cd examples/langgraph && python -m src.server— FastAPI server on port 2024.- Open http://127.0.0.1:2024 in your browser to see the demo page.
- Click "Request Refund" to start the graph. When it pauses, decide in the HITLy inbox at http://localhost:3001/inbox.
Alternatively, start threads via API:
curl -X POST http://127.0.0.1:2024/refund -H 'Content-Type: application/json' -d '{"order_id": "OR-1234", "amount": 123.45}'Production
HITLy must reach deploymentUrl to call the resume endpoint. Do not point a hosted HITLy workspace at localhost. Use LangGraph Cloud or a publicly accessible deployment.
Configuration options
| Option | Use it to |
|---|---|
api_url | HITLy app origin, such as http://localhost:3001 |
api_key | Workspace ingest key |
project_id | Project that owns the API key |
resume_secret | Project resume secret (for signature verification) |
deployment_url | Origin LangGraph server HITLy will call on resume |
thread_id | Thread to resume |
graph_id / assistant_id | Graph or assistant identifier |
Evidence Fields (Optional)
For audit and compliance, pass evidence context in the notify call:
| Field | Description |
|---|---|
system_id | AI system being governed (e.g. "refund-graph-prod") |
inventory_id | This AI system's record in your inventory/registry (Layer 1) |
policy_id | Policy that triggered approval |
policy_rationale | Why approval is required |
risk_tier | Risk level (e.g. "high", "medium", "low") |
tool_name | Tool or action name |
sensitivity | Sensitivity flags (not raw PII) |
data_categories | Data category flags (GDPR/compliance) |
Evidence events are written to the configured HTTP sink (project settings → Evidence Storage). See Envelope for full details.
Decision mapping
| HITLy | HumanResponse |
|---|---|
accept | { type: 'accept' } |
edit | { type: 'edit', args: ActionRequest } |
respond | { type: 'response', args: string } |
ignore / reject | { type: 'ignore' } |