🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@looppause/langgraph

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@looppause/langgraph

Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript)

latest
npmnpm
Version
0.1.1
Version published
Maintainers
1
Created
Source

@looppause/langgraph

Native LoopPause human-in-the-loop nodes for LangGraph (TypeScript).

"The missing primitive so agents don't go rogue — or die waiting for approval."

What it does

Provides two factory functions that wrap a LoopPause approval checkpoint into a LangGraph node, plus a router and a webhook-resume helper:

ExportPatternUse when
makePollingGateLong-poll until respondedSimple flows, short timeouts, no checkpointer needed
makeInterruptGateCreates pause → interrupt() → resumes via webhookLong timeouts, durable graphs, multi-turn agents
routeOnGateConditional edge helperBoth patterns
resumeThreadWake a hibernating graphmakeInterruptGate 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.

Installation

npm install @looppause/langgraph @langchain/langgraph

Requires Node.js 18+ (uses global fetch and node:crypto).

Configuration

Environment variableRequiredDescription
LOOPPAUSE_API_KEY✅ YesYour API key (sk_live_…)
LOOPPAUSE_SIGNING_SECRET✅ YesSigning secret for HMAC verification
LOOPPAUSE_API_URLNoOverride API base URL (default: https://api.looppause.com)

Get your keys at looppause.com/dashboard.

Pattern 1: Polling gate

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
}

Pattern 2: Interrupt gate (durable, webhook-driven)

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.

The reentrancy fix

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.

Full example

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);
}

Storing the thread ID

makeInterruptGate does not automatically map pause_idthread_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}`,

Escalation

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" }],
  },
});

API reference

LoopPauseClient

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

Relationship to @looppause/mcp

@looppause/langgraph@looppause/mcp
ForLangGraph TypeScript graphsAny MCP-compatible agent (Claude Code, Cursor)
PatternGraph node + conditional edgeTwo MCP tools: request_approval + check_approval
ReentrancyHandled via idempotency keyNot applicable
VerificationHMAC-SHA256 via verifySignatureEd25519 via verifyLoopPauseProof or verify_proof tool

Local development

# From the monorepo root
cd packages/langgraph
npm run build       # tsc → dist/
npm run type-check  # no emit

License

MIT

Keywords

hitl

FAQs

Package last updated on 14 Jun 2026

Did you know?

Socket

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.

Install

Related posts