Adapter

Gate LangChain.js tool calls before they run.

@decionis/langchain wraps a LangChain.js tool, or adds a LangGraph.js node, so each call asks Decionis first, with the call's own arguments, and runs only on a verdict that allows it. The agent sees the same tool: its name, description and schema do not change.

npm
@decionis/langchain 0.2.0
License
MIT
Needs
@langchain/core 0.3 or later
Source
not public
Checked
2026-09-26

Install

You need an organization API key and your org id. Every decision returns a signed Decision Dossier with a public verify link.

npm install @decionis/langchain @decionis/sdk @langchain/core

Get an API key

Wrap a tool

import { createDecionisNodeSdk } from "@decionis/sdk";
import { DecionisGateTool } from "@decionis/langchain";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";

const sendRefund = new DynamicStructuredTool({
  name: "send_refund",
  description: "Issue a refund. Idempotent on customer_id + amount + day.",
  schema: z.object({ customer_id: z.string(), amount_usd: z.number() }),
  func: async ({ customer_id, amount_usd }) => "refund_id-abc",
});

const decionis = createDecionisNodeSdk({
  baseUrl: "https://api.decionis.com",
  apiKey: process.env.DECIONIS_API_KEY!,
});

const gatedRefund = DecionisGateTool.wrap({
  innerTool: sendRefund,
  client: decionis,
  orgId: process.env.DECIONIS_ORG_ID!,
  decisionType: "refund_execution",
  siteBaseUrl: "https://decionis.com",
});

await agent.bindTools([gatedRefund]);

Gate a LangGraph.js step

Put the node between plan and execute. It writes its verdict under state.decionis, so the graph stays checkpointable and the next edge routes on allowed or blocked.

import { StateGraph, END } from "@langchain/langgraph";
import { decionisGateNode } from "@decionis/langchain";

const graph = new StateGraph<MyState>({
  /* channels */
});
graph.addNode("plan", planNode);
graph.addNode(
  "gate",
  decionisGateNode({
    client: decionis,
    orgId: process.env.DECIONIS_ORG_ID!,
    decisionType: "refund_execution",
    siteBaseUrl: "https://decionis.com",
    extractCall: (state) => ({
      toolName: state.proposedTool as string,
      toolArgs: state.proposedArgs as Record<string, unknown>,
    }),
  }),
);
graph.addNode("execute", executeNode);
graph.addNode("refuse", refuseNode);

graph.addEdge("plan", "gate");
graph.addConditionalEdges("gate", (s) => (s.decionis as { outcome: string }).outcome, {
  allowed: "execute",
  blocked: "refuse",
});
graph.addEdge("execute", END);
graph.addEdge("refuse", END);

What each verdict does

Enforcement

APPROVE runs the tool. REJECT, REVIEW and ESCALATE throw DecionisGateRefusal before it runs, carrying the Decision Dossier id, the reason codes and the verify link.

Shadow

With shadowMode: true the decision is still asked for and recorded, and the tool runs whatever the verdict: start here, and turn it off once the verdicts match the policy you meant.

When Decionis cannot be reached

A timeout, an HTTP error or no connection. In shadow, the tool runs anyway: the gate logs a warning, and onDecision receives the failure as error, with decision: null. The LangGraph.js node routes to allowed and records it under error. In enforcement, the error is thrown and the tool does not run.

Verify a Decision Dossier