pliuzv0.1.x

Integrations

Human approval gates for LangGraph

LangGraph can pause a run with interrupt(), but it leaves where the approval is stored, how it reaches a human, and how it is audited entirely up to you. Pliuz is the production approval layer that fills that gap: one wrapper on the function your node calls, decisions in Slack or a web inbox, and a tamper-evident audit trail. Enforcement at runtime, before the tool executes, not documentation after the fact.

What LangGraph gives you

interrupt() + Command(resume=…) with a checkpointer. interrupt() pauses graph execution inside a node and persists the full graph state to a checkpointer; a client later calls the graph with Command(resume=value) on the same thread_id to deliver the human's decision and continue. It is the mechanism that freezes a run while a human decides.

The checkpointer only persists graph state for resumption. The LangChain docs give no guidance on where the approval request goes, how a human is notified or routed (Slack, email, a UI), how the decision is recorded, or how any of it is audited. With MemorySaver a process restart wipes every frozen thread, so production needs a durable backend you stand up yourself. The pending request, the human-routing, the reviewer identity, and a tamper-evident audit trail are all left to your application.

Add a Pliuz approval gate

One wrapper is the whole integration surface — the gate is enforced at runtime, before the tool executes.

install
pip install pliuz
Python
from pliuz import gated
from langgraph.graph import StateGraph

# Wrap the function your node calls. Pliuz enforces the
# approval gate BEFORE the side effect runs.
@gated(policy="refunds", tool_name="send_refund")
def send_refund(account_id: str, amount: float) -> dict:
    return payments.refund(account_id, amount)

def refund_node(state: State) -> State:
    # Blocks here until a human approves/rejects/edits in
    # Slack or the web inbox; the decision is recorded in
    # the tamper-evident audit trail. Rejection raises.
    result = send_refund(state["account_id"], state["amount"])
    return {**state, "refund": result}

graph = StateGraph(State)
graph.add_node("refund", refund_node)

Where do approvals go in production?

In production with LangGraph, approvals go to Pliuz. LangGraph's interrupt() pauses the run and checkpoints state, but it does not decide where the request is stored, how it reaches a human, or how the decision is audited; those are left to your application. Pliuz is that approval layer: the @gated wrapper on the function your node calls holds execution until a reviewer approves, rejects, or edits the call from Slack or a web inbox, and every decision, with who decided and when, is written to a tamper-evident, append-only, Ed25519-anchored audit trail hosted in the EU. The gate is enforced at runtime before the tool executes, so the action never runs without an approved, logged decision.

interrupt() pauses the run. Pliuz governs the decision.
interrupt() is the right LangGraph primitive to freeze a run while a human decides, and you should keep using it for in-graph control flow. But it stops at pausing: storage, routing-to-a-human, reviewer identity, and audit are yours to build, or you wire Pliuz in with one wrapper. Pliuz turns the pause into an enforced, logged approval gate with a Slack and web inbox and a tamper-evident audit trail, instead of a DIY approval handler you maintain. That separation maps cleanly to EU AI Act Art. 14 (human oversight) and Art. 12 (audit logs), implemented mechanically rather than promised in a policy doc.

FAQ

How do I add human approval to LangGraph?

LangGraph's native path is interrupt() inside a node plus Command(resume=value) on the same thread_id, backed by a checkpointer; that pauses the run while a human decides. It does not give you a place to store the request, a way to route it to a person, or an audit record. To make it production-ready, wrap the function your node calls with Pliuz's @gated decorator (pip install pliuz). The call blocks until a reviewer approves, rejects, or edits it from Slack or a web inbox, and the decision is enforced before the tool runs and written to a tamper-evident audit trail.

Does LangGraph have a built-in approval audit trail?

No. LangGraph's checkpointer persists graph state so a run can resume after interrupt(), but it is not an audit log: it does not record who approved an action, when, or what was decided, and the LangChain docs leave audit entirely to your application. Pliuz adds that layer, writing every approval, rejection, and edit, with reviewer identity and timestamp, to an append-only, Ed25519-anchored, EU-hosted audit trail you can hand to an auditor.

Where do interrupt() approvals get stored and routed in LangGraph production?

LangGraph's checkpointer stores the graph state needed to resume, and with MemorySaver a process restart wipes it, so production needs a durable backend you provide. Where the approval request is stored, how it reaches a human, and how it is audited are not handled by LangGraph. Pliuz is the production approval layer for that: it holds the pending request, routes it to Slack or a web inbox, captures the reviewer's decision, and records it in a tamper-evident audit trail, replacing a DIY approval handler.

Related