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

  1. A graph node calls interrupt(HumanInterrupt) and POSTs to HITLy /api/v1/approvals.
  2. HITLy stores an envelope and shows it in the inbox.
  3. A reviewer accepts, edits, or rejects in the HITLy inbox.
  4. HITLy pauses the agent, takes the decision, and resumes the original thread with signed proof:
    • LangGraph PlatformPOST /threads/{threadId}/runs/wait with { command: { resume: HumanResponse } }
  5. The graph verifies the signature and checks response.type:
    • accept → proceed with the action
    • ignore (HITLy reject) → bail without taking the action
    • edit → 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:2024

Integration guide (Python)

1. Install dependencies

pip install langgraph langchain-core httpx

2. 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

  1. yarn workspace @hitly/app dev — HITLy on port 3001.
  2. cd examples/langgraph && python -m src.server — FastAPI server on port 2024.
  3. Open http://127.0.0.1:2024 in your browser to see the demo page.
  4. 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

OptionUse it to
api_urlHITLy app origin, such as http://localhost:3001
api_keyWorkspace ingest key
project_idProject that owns the API key
resume_secretProject resume secret (for signature verification)
deployment_urlOrigin LangGraph server HITLy will call on resume
thread_idThread to resume
graph_id / assistant_idGraph or assistant identifier

Evidence Fields (Optional)

For audit and compliance, pass evidence context in the notify call:

FieldDescription
system_idAI system being governed (e.g. "refund-graph-prod")
inventory_idThis AI system's record in your inventory/registry (Layer 1)
policy_idPolicy that triggered approval
policy_rationaleWhy approval is required
risk_tierRisk level (e.g. "high", "medium", "low")
tool_nameTool or action name
sensitivitySensitivity flags (not raw PII)
data_categoriesData category flags (GDPR/compliance)

Evidence events are written to the configured HTTP sink (project settings → Evidence Storage). See Envelope for full details.

Decision mapping

HITLyHumanResponse
accept{ type: 'accept' }
edit{ type: 'edit', args: ActionRequest }
respond{ type: 'response', args: string }
ignore / reject{ type: 'ignore' }

On this page