@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.
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/coreimport { 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]);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);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.
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.
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.