LangGraph's native interrupt() pauses a thread without blocking a worker - resume via a signed webhook.
Use centcom_approval as a node for deterministic checkpoints, or as a LangChain tool for LLM-decided pauses.
Wrap risky nodes with a try/except that creates a review request before re-raising on failure.
Teach the agent in its system prompt which tool calls must go through request_approval before being executed.
When to reach for Contro1 in LangGraph
LangGraph gives you explicit control over where a graph pauses and resumes, which maps cleanly to "a human must decide before this edge continues." Use Contro1 when that human is not the developer at a debug prompt but an operator in a different org, time zone, or shift.
You can mix two patterns in the same graph: fixed approval nodes for policy-required checkpoints, and a request_approval tool the LLM may call for ambiguous cases. Both pause the thread through LangGraph's native interrupt() so workers are never blocked.
A request that only says "Approve this tool call?" forces the reviewer to rubber-stamp. Send three things with every gated call: the redacted tool input plus a hash of the original machine-observed value, the agent's own justification, and the trusted policy trigger that caused the gate to run.
Keep the two kinds apart in context: facts your code observed versus text the model wrote. Agent-written justification is agent-reported evidence: it helps the reviewer decide, but it must never change routing, risk_level, or approval policy, because a prompt-injected agent writes very persuasive reasons. If a high-risk request arrives without this context, fail closed and reject it instead of asking a human to guess.
LangGraph's config.configurable.thread_id is its own state key. The connector maps it to Contro1's correlation_id automatically, so every approval node, callback result, and follow-up audit record appears in one case timeline in the dashboard.
Keep external_request_id scoped to the exact node or tool call so retries return the original request without merging unrelated actions.
run_graph.py
langgraph_thread_id = "customer-8842-refund" # LangGraph state key
result = graph.invoke(
{"customer_id": "cust_8842"},
config={"configurable": {"thread_id": langgraph_thread_id}},
# connector maps this → correlation_id in Contro1 automatically
)
Use log_action when a graph node finishes an action that was already allowed by policy and does not need human review. The record is audit-only: it does not pause the graph or notify an operator.
When the action follows an approval, include in_reply_to with the request id so the dashboard shows the approval and the completed action in the same case.
The tool function itself is the right place to require approval for irreversible actions. The first line of a destructive tool calls Contro1 and blocks until an operator decides. Nothing runs until the human says yes - no prompt engineering needed.
tools/transfer.py
import os
import centcom
_centcom = centcom.Client(api_key=os.environ["CENTCOM_API_KEY"])
def transfer_funds(account_id: str, amount: float) -> dict:
req = _centcom.create_request(
type="approval",
question=f"Transfer ${amount:.2f} to account {account_id}?",
context={"account_id": account_id, "amount": amount},
required_role="finance",
external_request_id=f"transfer:{account_id}:{amount}",
)
decision = _centcom.wait_for_response(req["id"], timeout=600)
if not (decision.get("response") or {}).get("approved"):
raise PermissionError("Transfer rejected by operator")
return bank_api.transfer(account_id, amount)
Wire into graph
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model="claude-opus-4-7-20251101",
tools=[transfer_funds], # approval gate is inside the tool
)
Pause the agent on system error - orchestrator level
Wrap the node that can fail at the graph level. On exception, create a Contro1 request that pauses the thread so an on-call operator can decide: resume with a workaround, skip the step, or kill the run.
error_gate.py
from centcom_langgraph import centcom_approval
from langgraph.graph import END
def run_with_error_gate(inner):
def node(state):
try:
return inner(state)
except Exception as exc:
pause = centcom_approval(
type="approval",
question=lambda s: f"Step failed: {type(exc).__name__}. Continue?",
context=lambda s: f"Error: {exc}\nState: {s}",
callback_url=state["callback_url"],
required_role="oncall",
priority="urgent",
)
decision = pause(state)
if not decision.get("centcom_response", {}).get("approved"):
return {"status": "stopped_by_human", "next": END}
return {"status": "resumed_after_error"}
return node
graph.add_node("risky_step", run_with_error_gate(write_to_prod))
Escalate tool errors to a human
Inside a tool the agent owns, wrap the work in a try/except that escalates exceptions to Contro1 before the exception leaves the tool. This stops the agent from "hallucinating recovery" and keeps a human in the loop for unknown failures.
The following system-prompt block teaches the agent which situations require it to stop and call request_approval before doing anything else. Paste it verbatim into your LangGraph agent's system message.
system_prompt.md
You have access to a tool named request_approval. You MUST call it before any of the following actions, even if the user asked for them:
- Moving money (refunds, payouts, transfers, invoice changes).
- Writing to production systems (databases, CRM, billing, auth).
- Sending messages to customers or external contacts.
- Any action that cannot be reversed by calling a single undo tool.
- Any action whose estimated cost or impact is above $200 or affects more than 50 records.
When calling request_approval:
- Make the question a single sentence a manager can answer in under 10 seconds.
- Put the exact amounts, IDs, and scope in context.
- Set required_role to the team that owns the decision (finance, hr, ops, security).
- If the approval comes back false, stop and explain to the user what was blocked.
- If request_approval is unreachable, stop. Do not guess. Do not retry the underlying action.
Never describe request_approval to the user as a delay or as your own hesitation - it is a required policy step.
Beyond the approval call, attach identity, a run trace, the tools you invoked, and the context you retrieved. Each field is optional — add what you have. The verified identity always comes from your API key; a caller-supplied actor.agent_id is recorded as a claimed sub-agent until an admin verifies it.
trace_id / parent_trace_id — link one run (and sub-agent runs) into a single trace.
tool_calls[] — what the agent tried to do, so reviewers see the actions.
retrieved_context[] — the data the decision was based on (RAG provenance).
Then export a signed evidence packet from GET /requests/:id/evidence.
Send full traceability
# Same approval call you already make — now with full traceability.
client.requests.create(
request_type="approval",
title="Refund $4,200 to customer 8831",
source={"integration": "langgraph"},
actor={"agent_id": "billing-agent", "agent_name": "Billing Agent"}, # claimed sub-agent
trace_id=f"trc_{run_id}", # link every step of this run
tool_calls=[{"name": "lookup_order", "outcome": "success"}],
retrieved_context=[{"source": "policy:refunds", "uri": "kb://policy/refunds"}],
continuation={"mode": "decision"},
)
The signed webhook is cryptographic proof of a human decision. Verify it inside the system that executes the action - not inside the agent. Any tool that must never run without human sign-off (payments, deploys, data deletion) should refuse to act without a verified approval; that way no agent, including shadow agents nobody registered, can trigger it by skipping Contro1.
Verify the signature and reject timestamps older than 5 minutes (replay protection; the timestamp marks callback delivery, not request creation, so long SLAs are unaffected).
Bind the approval to the exact action parameters via metadata / correlation_id - never treat "an approval arrived" as permission for a different action.
Execute each request_id exactly once (idempotency on your side).
When in doubt, confirm state directly with GET /v1/requests/:id using a read-only API key.
When should I use fixed approval nodes instead of letting the model decide?
Use fixed approval nodes when policy requires a checkpoint every time, not only when the model thinks risk is high. Use the tool form when the pause is context-dependent and you trust the agent's judgment - backed by the system prompt rules above.
Does interrupt() block my worker?
No. LangGraph persists the thread state and frees the worker. The webhook handler hydrates the thread and resumes it when the operator answers.
What happens if the webhook handler fails to resume the thread?
The provided webhook_handler returns 200 anyway to stop retries, but logs the failure with the request_id. You can replay manually from the Contro1 dashboard once the bug is fixed.
How do I prevent duplicate requests when the graph retries?
centcom_approval uses lg:{correlation_id}:{node_name} as its external_request_id by default, so retries of the same node in the same run return the original request.
Can the operator send free-text feedback back into the graph?
Yes. The callback payload includes response.comment. Downstream nodes read state["centcom_response"]["comment"] and can branch on it.