@decionis/langchain
Gate any LangChain.js tool call or LangGraph.js node on a signed
Decionis Decision Dossier. The agent picks the tool;
Decionis decides whether the call is allowed to fire — and records every
verdict as a verifiable proof artifact.
npm install @decionis/langchain @decionis/sdk @langchain/core
Mirrors decionis-langchain
for Python: same shadow → enforce rollout, same ?source=langchain_agent
verify-URL attribution, same per-tool short-circuit semantics.
Why
LangChain agents can be jailbroken, prompt-injected, or hallucinated into
firing tools that move money, change pricing, send refunds, or delete data.
Wrapping the tool with Decionis means:
- Every tool invocation gets a signed Decision Dossier — the policy
verdict (
APPROVE / REJECT / REVIEW / ESCALATE), the agent identity,
the call arguments, and a public verify URL.
- Blocked calls short-circuit before the inner tool runs — the LLM sees a
structured refusal (
DecionisGateRefusal) carrying the dossier id; the
caller's audit log keeps the proof.
- Shadow mode records verdicts without blocking, so a team can roll out
policy gradually and review the would-have-rejected rate before enforcing.
Quick start — wrap a LangChain.js 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]);
When the LLM picks send_refund, Decionis evaluates first. On APPROVE the
inner tool runs and the result is returned. On REJECT / REVIEW /
ESCALATE (configurable) the wrapper throws DecionisGateRefusal carrying
the dossier id, reason codes, and a public verify URL the LLM can read and
the operator can forward.
Quick start — LangGraph.js node
import { StateGraph, END } from "@langchain/langgraph";
import { decionisGateNode } from "@decionis/langchain";
const graph = new StateGraph<MyState>({
});
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);
The node writes a JSON-serializable record under state.decionis so the
graph stays checkpointable and the conditional edge can branch on allowed
or blocked.
Shadow-mode rollout
Same pattern as the Python wrapper
and the GitHub Action:
ship in shadow first, review the verdict distribution, then flip to enforce.
The end-to-end PLG funnel — pick a surface → install in shadow → watch verdicts → flip —
is walked at
decionis.com/shadow-mode?surface=langchain_js.
DecionisGateTool.wrap({
innerTool: sendRefund,
client: decionis,
orgId,
decisionType: "refund_execution",
shadowMode: true,
});
In shadow mode the gate never throws — even on REJECT — so existing
agent behaviour is unchanged. Verdicts still flow into the dossier ledger so
the rollout team can grade policy fit before enforcing.
Tunables
client | required | DecionisDecisionExecutor from createDecionisNodeSdk(...). |
orgId | required | Decionis org id (UUID). |
decisionType | required | Canonical decision type — usually the workflow key (e.g. refund_execution). |
workflowKey | decisionType | Override when the workflow key needs to differ from the decision type. |
blockOutcomes | ["REJECT","REVIEW","ESCALATE"] | Outcomes that short-circuit the wrapper. Pass ["REJECT"] to let review verdicts through with the dossier recorded. |
shadowMode | false | Record verdicts but never throw / never route to blocked. |
siteBaseUrl | undefined | Base URL used to build a public verify URL (/verify/decision-dossiers/<id>?source=langchain_agent&sig=<dossier_sha256>). |
actor | { type: "ai_agent", framework: "langchain_js" } | Extra actor metadata (model, session id, user id) merged into the dossier context. |
onDecision | undefined | Observer callback (GateResult) => void. Exceptions inside it never break the gate. |
Honesty notes
shadowMode: true is the only switch that lets the inner tool run on
a blocking verdict; the default never silently passes a REJECT. Locked
by tests.
DecionisGateRefusal.verifyUrl is the same artifact link Slack / Teams /
LinkedIn unfurl with the OG card from
/api/og/verify/[id]. Forward it to a reviewer
when you want the proof one click away.
- The wrapper preserves the inner tool's name, description, and args schema
unchanged so the LLM's tool selection behaviour does not drift.
- The observer callback's exceptions are swallowed so telemetry can never
take down policy enforcement.
Compatibility
- Node ≥ 20
@decionis/sdk (workspace)
@langchain/core ≥ 0.3 (peer)
@langchain/langgraph is not required to use decionisGateNode —
the factory returns a plain state => Promise<state> callable, so LangGraph
stays an optional runtime dependency.