
Company News
Free Business Plan Upgrades for Open Source Maintainers
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.
@looppause/langgraph
Advanced tools
Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript).
"The missing primitive so agents don't go rogue — or die waiting for approval."
Provides two factory functions that wrap a LoopPause approval checkpoint into a LangGraph node, plus a router and a webhook-resume helper:
| Export | Pattern | Use when |
|---|---|---|
makePollingGate | Long-poll until responded | Simple flows, short timeouts, no checkpointer needed |
makeInterruptGate | Creates pause → interrupt() → resumes via webhook | Long timeouts, durable graphs, multi-turn agents |
routeOnGate | Conditional edge helper | Both patterns |
resumeThread | Wake a hibernating graph | makeInterruptGate only |
Both patterns fail closed: if LoopPause is unreachable or the response is
rejected, approved is false and the graph routes to "halt".
system_fallback is never treated as human approval.
npm install @looppause/langgraph @langchain/langgraph
Requires Node.js 18+ (uses global fetch and node:crypto).
| Environment variable | Required | Description |
|---|---|---|
LOOPPAUSE_API_KEY | ✅ Yes | Your API key (sk_live_…) |
LOOPPAUSE_SIGNING_SECRET | ✅ Yes | Signing secret for HMAC verification |
LOOPPAUSE_API_URL | No | Override API base URL (default: https://api.looppause.com) |
Get your keys at looppause.com/dashboard.
The node creates a pause and holds the graph open, polling until the human responds or the deadline passes. Simple — no checkpointer, no webhook.
import { StateGraph, Annotation } from "@langchain/langgraph";
import {
LoopPauseClient,
makePollingGate,
routeOnGate,
} from "@looppause/langgraph";
const GraphState = Annotation.Root({
amount: Annotation<number>(),
vendor: Annotation<string>(),
looppause: Annotation<any>(),
});
const client = new LoopPauseClient(); // reads env vars
const approveTransfer = makePollingGate(client, {
agentId: "billing-agent",
actionBuilder: (state) => ({
type: "approval",
description: `Transfer £${state.amount} to ${state.vendor}`,
details: { amount: String(state.amount), vendor: state.vendor },
}),
recipients: [
{ channel: "slack", target: "#finance-approvals" },
],
timeoutHours: 4,
});
const builder = new StateGraph(GraphState)
.addNode("approve_transfer", approveTransfer)
.addNode("execute_transfer", executeTransfer)
.addNode("halt", haltNode)
.addEdge("__start__", "approve_transfer")
.addConditionalEdges("approve_transfer", routeOnGate(), {
approved: "execute_transfer",
halt: "halt",
});
const graph = builder.compile();
const result = await graph.invoke({ amount: 12450, vendor: "Globex Corp" });
// result.looppause.approved === true → transfer executed
// result.looppause.approved === false → graph halted, reason in haltReason
Reading the gate result:
interface GateResult {
approved: boolean; // true only when human authorized AND decision === "approved"
authorizationType: string; // "human" or "system_fallback"
responder: string; // email of the human who responded
comment: string | null; // optional comment from the approver
pauseId: string; // LoopPause pause_id for audit
haltReason: string | null; // set when approved === false
}
The interrupt gate creates a pause, then calls LangGraph's interrupt() to
hibernate the graph in the checkpointer. When LoopPause fires the webhook,
your handler wakes the graph with the signed proof via resumeThread.
This is the right pattern for long-running approvals (hours, days) where you cannot keep a connection open.
LangGraph re-executes the node from the top on resume, so createPause is
called twice for the same action. makeInterruptGate passes a deterministic
Idempotency-Key derived from (thread_id, action) — the second call returns
the existing pause_id rather than creating a duplicate.
import { StateGraph, Annotation, MemorySaver } from "@langchain/langgraph";
import {
LoopPauseClient,
makeInterruptGate,
routeOnGate,
resumeThread,
type Proof,
} from "@looppause/langgraph";
const GraphState = Annotation.Root({
amount: Annotation<number>(),
vendor: Annotation<string>(),
looppause: Annotation<any>(),
});
const client = new LoopPauseClient();
const checkpointer = new MemorySaver(); // use LangGraph Cloud or Redis in prod
const approveTransfer = makeInterruptGate(client, {
agentId: "billing-agent",
webhookUrl: "https://api.yourapp.com/webhooks/looppause",
actionBuilder: (state) => ({
type: "approval",
description: `Transfer £${state.amount} to ${state.vendor}`,
details: { amount: String(state.amount), vendor: state.vendor },
}),
recipients: [
{ channel: "slack", target: "#finance-approvals", fallback_email: "cfo@company.com" },
],
timeoutHours: 24,
});
const builder = new StateGraph(GraphState)
.addNode("approve_transfer", approveTransfer)
.addNode("execute_transfer", executeTransfer)
.addNode("halt", haltNode)
.addEdge("__start__", "approve_transfer")
.addConditionalEdges("approve_transfer", routeOnGate(), {
approved: "execute_transfer",
halt: "halt",
});
const graph = builder.compile({ checkpointer });
// Start the graph — it hibernates at interrupt()
const threadId = "thread_abc123";
await graph.invoke(
{ amount: 12450, vendor: "Globex Corp" },
{ configurable: { thread_id: threadId } },
);
// Later — your webhook handler receives the signed proof from LoopPause:
async function handleLoopPauseWebhook(req: Request): Promise<void> {
const proof = (await req.json()) as Proof;
// threadId must be recoverable from the proof — store it when creating the pause,
// or encode it in the webhook URL: /webhooks/looppause?thread=thread_abc123
await resumeThread(graph, threadId, proof);
}
makeInterruptGate does not automatically map pause_id → thread_id. A
minimal pattern: store the mapping when the graph starts, keyed on the
pause_id returned from the interrupt() value:
// After graph.invoke() returns (or throws NodeInterrupt), the pause_id is in
// the interrupted node's value. A simpler approach: encode thread_id in the
// webhook URL path or query param.
// Webhook URL per thread:
webhookUrl: `https://api.yourapp.com/webhooks/looppause/${threadId}`,
Pass an escalation config to automatically escalate if the primary recipient
does not respond:
makePollingGate(client, {
// ...
escalation: {
after_hours: 2,
recipients: [{ channel: "email", target: "cfo@company.com" }],
},
});
LoopPauseClientnew LoopPauseClient(opts?: {
apiKey?: string; // default: LOOPPAUSE_API_KEY env var
signingSecret?: string; // default: LOOPPAUSE_SIGNING_SECRET env var
baseUrl?: string; // default: https://api.looppause.com
requestTimeoutMs?: number; // default: 10 000
})
makePollingGate(client, config)Returns a LangGraph node function. Polls until responded or deadline.
interface GateConfig {
agentId: string;
actionBuilder: (state: any) => Record<string, unknown>;
recipients: Array<Record<string, unknown>>;
stateKey?: string; // default: "looppause"
requireHuman?: boolean; // default: true — system_fallback is never approved
timeoutHours?: number; // default: 24
escalation?: Record<string, unknown>;
}
makeInterruptGate(client, config)Returns a LangGraph node function. Creates pause → interrupt() → resumes with
signed proof. webhookUrl is required.
routeOnGate(stateKey?)Returns a conditional edge function: "approved" when state[stateKey].approved
is true, otherwise "halt".
resumeThread(graph, threadId, signedProof)Wakes a hibernating graph thread with the signed proof as the interrupt resume value. Call from your webhook handler.
verifySignature(proof, signingSecret)Verifies the HMAC-SHA256 signature on a proof object. Timing-safe.
@looppause/mcp@looppause/langgraph | @looppause/mcp | |
|---|---|---|
| For | LangGraph TypeScript graphs | Any MCP-compatible agent (Claude Code, Cursor) |
| Pattern | Graph node + conditional edge | Two MCP tools: request_approval + check_approval |
| Reentrancy | Handled via idempotency key | Not applicable |
| Verification | HMAC-SHA256 via verifySignature | Ed25519 via verifyLoopPauseProof or verify_proof tool |
# From the monorepo root
cd packages/langgraph
npm run build # tsc → dist/
npm run type-check # no emit
MIT
FAQs
Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript)
We found that @looppause/langgraph demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.