LangChain & LangGraph Integration
Add cryptographic human approval to LangChain agents and LangGraph workflows with a drop-in tool and decorator.
The signedapproval-langchain package provides a LangChain-compatible tool and a @require_approval decorator that block agent execution until a real human authenticates and approves. Unlike Slack-based approval flows, every decision produces an unforgeable Ed25519 signature verifiable entirely offline.
It is a direct replacement for HumanLayer's @require_approval — swap the import and add your sa_live_ API key.
Install the package
pip install signedapproval-langchain
# With async support (LangGraph async nodes):
pip install "signedapproval-langchain[async]"
# With langchain-core:
pip install "signedapproval-langchain[all]"Get your API key
Create an API key in the Dashboard → API Keys. Your key starts with sa_live_. Store it in an environment variable — never commit it to source control.
export SIGNEDAPPROVAL_API_KEY="sa_live_..."Use as a LangChain tool
Pass SignedApprovalTool to any LangChain agent. The tool blocks until the human decides.
import os
from signedapproval_langchain import SignedApprovalTool
tool = SignedApprovalTool(api_key=os.environ["SIGNEDAPPROVAL_API_KEY"])
# Standalone usage
result = tool.invoke("Transfer $500 to vendor account")
if result.decision == "approved":
print(f"Approved! Sig: {result.signature[:32]}...")
# proceed with the action
else:
print(f"Not approved: {result.decision}")
# abortUse the @require_approval decorator
Wrap any function so it requires human approval before running. This is the closest equivalent to HumanLayer's decorator API.
from signedapproval_langchain import require_approval
@require_approval(
api_key=os.environ["SIGNEDAPPROVAL_API_KEY"],
action_template="Execute database migration: {args}",
)
def run_migration(migration_name: str) -> str:
# Only called if a human approves
return f"Migration {migration_name} complete"
# Raises PermissionError if rejected or expired
run_migration("add_users_table")LangGraph Example
Use SignedApprovalTool as a LangGraph node to gate high-risk transitions:
from langgraph.graph import StateGraph, END
from signedapproval_langchain import SignedApprovalTool, ApprovalResult
from typing import TypedDict
tool = SignedApprovalTool(api_key=os.environ["SIGNEDAPPROVAL_API_KEY"])
class AgentState(TypedDict):
action: str
approval: ApprovalResult | None
result: str | None
def request_approval_node(state: AgentState) -> AgentState:
result = tool.invoke(state["action"])
return {**state, "approval": result}
def execute_node(state: AgentState) -> AgentState:
# Only reached if approved
return {**state, "result": f"Executed: {state['action']}"}
def route_after_approval(state: AgentState) -> str:
if state["approval"] and state["approval"].decision == "approved":
return "execute"
return END
graph = StateGraph(AgentState)
graph.add_node("request_approval", request_approval_node)
graph.add_node("execute", execute_node)
graph.set_entry_point("request_approval")
graph.add_conditional_edges("request_approval", route_after_approval, {"execute": "execute", END: END})
graph.add_edge("execute", END)
app = graph.compile()
result = app.invoke({"action": "Deploy main to production", "approval": None, "result": None})Async Support
For async LangGraph nodes or async agent frameworks, install with the async extra and call _arun():
result = await tool._arun("Send invoice to client")The async version uses aiohttp and does not block the event loop.
ApprovalResult reference
| Field | Type | Description |
|---|---|---|
| decision | str | "approved", "rejected", or "expired" |
| signature | str | None | Base64 Ed25519 signature of the canonical payload |
| public_key_id | str | None | Key ID — use with /api/v1/approvals/:id/verify |
| canonical_payload | str | None | JSON payload that was signed |
| request_id | str | UUID of the approval request |
| verified | bool | True when decision == approved |
| comment | str | None | Optional note from the approver |
signature and canonical_payloadfields let you verify the approval without calling SignedApproval's servers. Fetch the public key once from GET /api/v1/approvals/{id}/verifyand verify the Ed25519 signature locally with PyNaCl or Python's cryptography library.require_passkey=True or require_totp=True on SignedApprovalTool to enforce stronger authentication for the highest-risk actions.