Skip to content

Use Verdict as tools in an agent

@verdict/sdk/tools turns Verdict into ready-made tools for an agent framework. You get eleven tools: the six decisions, four ICP calls and outcome reporting. Each has a description written for the model (what it does, when to call it, its price, what comes back) and a self-contained JSON Schema of its arguments. The tools are generated from the same OpenAPI document as the API reference, so they cannot drift from the API.

Every call goes through the SDK client. The client sends your key, a fresh Idempotency-Key per call (reused across its own retries, so a retried decision is never billed twice) and your spend cap. It retries 429 and 5xx with backoff, and waits out the Retry-After of a 503 decision_unavailable or vendor_unavailable. The model never sees or sets any of this.

Terminal window
npm i @verdict/sdk
Tool Call Price
qualify_firm POST /v1/decisions/qualify_firm $0.005
score_signal POST /v1/decisions/score_signal $0.005
pick_person POST /v1/decisions/pick_person $0.005
triage_reply POST /v1/decisions/triage_reply $0.003
right_person POST /v1/decisions/right_person $0.003
should_send POST /v1/decisions/should_send $0.003
create_icp POST /v1/icps free
get_icp GET /v1/icps/{id} free
update_icp PATCH /v1/icps/{id} free
update_icp_thresholds PATCH /v1/icps/{id}/thresholds free
create_outcome POST /v1/outcomes free

A decision is billed only when it returns an answer. Errors, including a 503 that outlives the retries, are free.

verdictTools(client, options) binds the tools to a client:

  • include / exclude: pick tools by name, for example { include: ["qualify_firm", "create_outcome"] }. An unknown name throws.
  • icpId: your default ICP profile. Every tool that takes icp_id then lists it as optional, and the tool fills it in whenever the model leaves it out. An icp_id the model does pass is used as given. Set it when your agent works for one ICP, so the model never has to find or invent a profile id.
  • maxCostUsd: a spend cap per priced call, sent as X-Max-Cost-USD. A call that would cost more is refused with 402 max_cost_exceeded and nothing is billed. The client’s own maxCostUsd applies when this is not set.
  • idempotencyKey: how each call’s Idempotency-Key is chosen. The default, "per-call", is a fresh UUID for every tool call (the client reuses it across its own retries). Set "from-call-id" when your agent runs on a durable runtime that can replay a step after it finished, such as a workflow engine or a crash-and-resume loop. The key is then derived from the framework’s tool call id (the AI SDK’s toolCallId, Anthropic’s tool_use id, OpenAI’s call_id), so a replayed call gets the stored response instead of a second billed decision. You can also pass your own function of { callId, toolName }.
  • requestOptions: other per-call client options (version pin, timeout, retries, headers).

Arguments that go into the URL (the id of get_icp, update_icp and update_icp_thresholds) are checked before anything is sent. A missing, empty or malformed id fails as invalid_arguments, which the run helpers return to the model as an error result.

This example targets the AI SDK ai 7.x (generateText, stopWhen and isStepCount), the version @verdict/sdk/tools is type-checked against and the one its optional peer range names. ai 5 and 6 use the same tool shape (description, inputSchema, execute) and call the stop condition stepCountIs, which ai 7 still exports, but they are not tested.

The AI SDK reads JSON Schemas only through its own jsonSchema() helper. Pass that helper to toVercelAiTools, so @verdict/sdk needs no dependency on ai:

import { anthropic } from "@ai-sdk/anthropic";
import { Verdict } from "@verdict/sdk";
import { toVercelAiTools, verdictTools } from "@verdict/sdk/tools";
import { generateText, isStepCount, jsonSchema } from "ai";
const verdict = new Verdict({ maxCostUsd: 0.01 }); // reads VERDICT_API_KEY
const tools = toVercelAiTools(
verdictTools(verdict, { icpId: process.env.VERDICT_ICP_ID, idempotencyKey: "from-call-id" }),
jsonSchema,
);
const { text, steps } = await generateText({
model: anthropic("claude-opus-5-5"),
tools,
stopWhen: isStepCount(8),
system:
"You qualify inbound companies. Call qualify_firm for each one. " +
"Act only when answer is fit. When a human approves the next step, call create_outcome with outcome approved.",
prompt: "Should we work northfieldcapital.com and acmeholdings.com?",
});
console.log(text);
for (const step of steps) {
for (const r of step.toolResults) console.log(r.toolName, r.output);
}

streamText takes the same tools and stopWhen. When a call fails after the client’s retries, execute throws the SDK’s VerdictError and the AI SDK hands it to the model as a tool error, so the agent can move on or try later.

toAnthropicTools gives the tools list. runAnthropicToolUse runs one tool_use block and returns its tool_result block. A Verdict error (for example insufficient_balance, or a 503 that outlived the retries) comes back as is_error: true with a short JSON body (error, status, detail, retry_after_seconds) for the model to read. A missing API key still throws, because that is your bug to fix.

import Anthropic from "@anthropic-ai/sdk";
import { Verdict } from "@verdict/sdk";
import { runAnthropicToolUse, toAnthropicTools, verdictTools } from "@verdict/sdk/tools";
const anthropic = new Anthropic();
const tools = verdictTools(new Verdict(), { icpId: process.env.VERDICT_ICP_ID });
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Is northfieldcapital.com a fit for us?" },
];
for (;;) {
const response = await anthropic.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
tools: toAnthropicTools(tools),
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") break;
const calls = response.content.filter((b) => b.type === "tool_use");
const results = await Promise.all(calls.map((b) => runAnthropicToolUse(tools, b)));
messages.push({ role: "user", content: results }); // every result in one message
}

toOpenAiTools returns Chat Completions function tools, or the flat Responses API shape with { api: "responses" }. It sets strict: true only when a schema meets OpenAI’s strict-mode rules: every property required, additionalProperties: false on every object, and only supported keywords. Today every Verdict tool has optional fields or string length limits, so strict is left off (Chat Completions) or false (Responses). runOpenAiToolCall runs one call and returns the message or input item that carries its result. Errors come back as for Anthropic.

import OpenAI from "openai";
import { Verdict } from "@verdict/sdk";
import { runOpenAiToolCall, toOpenAiTools, verdictTools } from "@verdict/sdk/tools";
const openai = new OpenAI();
const tools = verdictTools(new Verdict(), { icpId: process.env.VERDICT_ICP_ID });
const input: OpenAI.Responses.ResponseInput = [
{ role: "user", content: "Is northfieldcapital.com a fit for us?" },
];
for (;;) {
const response = await openai.responses.create({
model: process.env.OPENAI_MODEL ?? "gpt-5", // any tool-calling model
tools: toOpenAiTools(tools, { api: "responses" }),
input,
});
input.push(...response.output);
const calls = response.output.filter((item) => item.type === "function_call");
if (calls.length === 0) break;
input.push(...(await Promise.all(calls.map((c) => runOpenAiToolCall(tools, c)))));
}

With Chat Completions, pass tools: toOpenAiTools(tools) and answer each message.tool_calls[i] with await runOpenAiToolCall(tools, call), which returns the { role: "tool", tool_call_id, content } message.

verdictTools(client) returns plain objects: { name, description, inputSchema, execute(args) }, plus operationId and priceUsd. inputSchema is a JSON Schema object with no $ref and an object at the top level. execute takes the parsed arguments (or their JSON string) and resolves to the API’s JSON response.

If your agent speaks the Model Context Protocol (Claude Desktop, Claude Code, Cursor and others), you do not need this package: run the Verdict MCP server, @verdict/mcp (bin verdict-mcp). It exposes the decisions and the index as MCP tools with the same descriptions.

Decisions get better for you as you report what happened after them. Give your agent create_outcome, or report from your own code with verdict.approve(id), verdict.skip(id) and verdict.outcomes.report(...). See Outcomes and the feedback loop.