Skip to content

2026-03-02

OpenAI Agents SDK + Intended: Adding Authority to Every Tool Call

Developer Relations · Developer Experience

OpenAI Agents SDK + Intended: Adding Authority to Every Tool Call

Runnable. This tutorial uses the published @intended-inc/sdk and its real createIntendedSdk().guard(action, onAllow) primitive — the callback runs only on an ALLOW decision (fail-closed). See the TypeScript SDK reference for the full surface. (Python users: the intended package ships an intended.openai_agents.IntendedOpenAIGuard helper with the same fail-closed behavior.)

The OpenAI Agents SDK provides a streamlined way to build AI agents that use tools, follow instructions, and hand off tasks between specialized agents. It handles orchestration, tool calling, and multi-agent workflows out of the box. What it does not handle is authorization.

When an agent in the OpenAI SDK calls a tool, the call executes immediately. There is no policy check, no risk assessment, and no audit trail. The agent has a tool, so it uses the tool. For development and testing, that is fine. For production, where agents handle real data and real operations, you need governance.

This tutorial shows you how to add Intended authority governance to an OpenAI Agents SDK application with minimal code changes.

What You Will Build

You will build a multi-agent customer operations system with three specialized agents: a lookup agent that retrieves customer information, an actions agent that can modify accounts and process refunds, and a router agent that delegates to the appropriate specialist. Intended will govern the actions agent, ensuring that account modifications and refunds are authorized against your policies.

Prerequisites

You need Node.js 20 or later, an OpenAI API key, and a Intended API key. Install the dependencies:

bash
npm install @openai/agents @intended-inc/sdk

Step 1: Define Your Tools

Start with the standard OpenAI Agents SDK tool definitions:

typescript
import { tool } from "@openai/agents";
import { z } from "zod";

const lookupCustomer = tool({
  name: "lookup_customer",
  description: "Look up customer information by ID or email",
  parameters: z.object({
    identifier: z.string().describe("Customer ID or email address"),
  }),
  execute: async ({ identifier }) => {
    const customer = await db.customers.findByIdOrEmail(identifier);
    return JSON.stringify(customer);
  },
});

const processRefund = tool({
  name: "process_refund",
  description: "Process a refund for a customer order",
  parameters: z.object({
    orderId: z.string(),
    amount: z.number(),
    reason: z.string(),
  }),
  execute: async ({ orderId, amount, reason }) => {
    const result = await billing.processRefund(orderId, amount, reason);
    return JSON.stringify(result);
  },
});

const updateAccount = tool({
  name: "update_account",
  description: "Update customer account details",
  parameters: z.object({
    customerId: z.string(),
    updates: z.record(z.string()),
  }),
  execute: async ({ customerId, updates }) => {
    const result = await db.customers.update(customerId, updates);
    return JSON.stringify(result);
  },
});

Step 2: Govern the high-risk tools

Create the SDK once, then put the authority check inside each high-risk tool's execute. sdk.guard(action, onAllow) runs the real work only on an ALLOW decision; ESCALATE, DENY, and any engine error all fail closed (the callback never runs).

typescript
import { createIntendedSdk } from "@intended-inc/sdk";
import { tool } from "@openai/agents";
import { z } from "zod";

const sdk = createIntendedSdk({
  apiKey: process.env.INTENDED_API_KEY!,
  tenantId: process.env.INTENDED_TENANT_ID!,
  baseUrl: "https://api.intended.so",
});

const protectedRefund = tool({
  name: "process_refund",
  description: "Process a refund for a customer order",
  parameters: z.object({ orderId: z.string(), amount: z.number(), reason: z.string() }),
  execute: async ({ orderId, amount, reason }) => {
    const result = await sdk.guard(
      { action: `process refund of $${amount} for order ${orderId}`, actor: "customer-ops-agent" },
      async () => billing.processRefund(orderId, amount, reason), // runs only on ALLOW
    );
    if (result.status === "allowed") return JSON.stringify(result.result);
    if (result.status === "pending_approval")
      return `Refund held for human approval (escalation ${result.decision.escalationId}).`;
    return `Refund denied by policy: ${result.decision.summary ?? "not authorized"}.`;
  },
});

const protectedUpdate = tool({
  name: "update_account",
  description: "Update customer account details",
  parameters: z.object({ customerId: z.string(), updates: z.record(z.string()) }),
  execute: async ({ customerId, updates }) => {
    const result = await sdk.guard(
      { action: `update account ${customerId} fields: ${Object.keys(updates).join(", ")}`, actor: "customer-ops-agent" },
      async () => db.customers.update(customerId, updates),
    );
    if (result.status === "allowed") return JSON.stringify(result.result);
    if (result.status === "pending_approval")
      return `Update held for approval (escalation ${result.decision.escalationId}).`;
    return `Update denied by policy: ${result.decision.summary ?? "not authorized"}.`;
  },
});

You don't pass an intent code or risk hints: the engine reads risk from the action text you build (the refund amount, the fields being changed), classifies it against the Open Intent taxonomy, and decides. Nothing executes unless the verdict is ALLOW; result.status discriminates "allowed" / "pending_approval" / "denied".

Step 3: Build the Multi-Agent System

Now create the agents using the protected tools:

typescript
import { Agent, run } from "@openai/agents";

const lookupAgent = new Agent({
  name: "Customer Lookup",
  instructions:
    "You look up customer information. Use the lookup tool to find " +
    "customers by ID or email. Return the information clearly.",
  tools: [lookupCustomer], // a read; left ungoverned here
});

const actionsAgent = new Agent({
  name: "Customer Actions",
  instructions:
    "You process customer account changes and refunds. Always confirm " +
    "the details before taking action. If an action is blocked or " +
    "escalated, explain why to the user.",
  tools: [protectedRefund, protectedUpdate],
});

const routerAgent = new Agent({
  name: "Customer Ops Router",
  instructions:
    "You are a customer operations assistant. Route lookup requests " +
    "to the lookup agent and action requests to the actions agent.",
  handoffs: [lookupAgent, actionsAgent],
});

Step 4: Configure Policies

Policies live in Intended, not in your agent code — author them in the console or via the policy API and CLI (policy-pack-install). The conservative starter policy pack already gives safe defaults (benign → allow, production-affecting → escalate, privileged/sensitive → deny), so you get fail-closed behavior before writing a single rule.

From there you tune by action. For this example you'd allow refunds under \$100, escalate \$100–\$1,000, and deny refunds over \$1,000 (manual processing); and escalate account updates that touch sensitive fields like email, billing address, or payment method. Because the engine classifies from the action text your tool builds (process refund of $500 for order …, update account … fields: email), the amount and the changed fields are exactly what it scores — no separate rule-DSL call from the SDK is required.

Step 5: Run the Agent

typescript
async function handleCustomerRequest(userMessage: string) {
  const result = await run(routerAgent, userMessage);
  return result.finalOutput;
}

// Example interactions
await handleCustomerRequest(
  "Look up the account for customer cust_12345"
);
// -> Allowed. Returns customer info.

await handleCustomerRequest(
  "Process a $75 refund for order ord_98765, reason: item damaged"
);
// -> Allowed. Under $100 threshold. Refund processed.

await handleCustomerRequest(
  "Process a $500 refund for order ord_44321, reason: service outage"
);
// -> Escalated. Between $100-$1000. Held for human approval.

await handleCustomerRequest(
  "Update the billing email for cust_12345 to newemail@example.com"
);
// -> Escalated. Email is a sensitive field. Held for human approval.

Handling Escalations

When the engine escalates, your guarded tool already sees it: sdk.guard(...) returns result.status === "pending_approval" and the callback never runs. That's where you wire your own notification — for example, post the pending approval to Slack:

typescript
const result = await sdk.guard(
  { action: `process refund of $${amount} for order ${orderId}`, actor: "customer-ops-agent" },
  async () => billing.processRefund(orderId, amount, reason),
);

if (result.status === "pending_approval") {
  await slack.postMessage({
    channel: "#customer-ops-approvals",
    text: `Refund needs approval — escalation ${result.decision.escalationId}, ` +
          `risk ${result.decision.riskScore}. Review it in the Intended console.`,
  });
}

The escalation sits in the operator's approval queue in Intended; when a reviewer approves or denies it there, the decision is recorded in the audit chain.

The Audit Trail

Every tool call — allowed, denied, or escalated — is written to the tamper-evident audit chain. Each GuardDecision carries an intentId (and correlationId) you can use to look the record up through the audit API or the console, and you can verify chain integrity from the SDK. Each ALLOW additionally carries a signed authority token you can verify independently against the public JWKS — the proof stands on its own, without calling back to Intended.

What You Gain

Your OpenAI Agents SDK application now has production-grade governance. Customer lookups flow through without friction. Low-risk refunds auto-approve. High-risk refunds and sensitive account changes require human approval. Every decision is recorded and cryptographically signed.

The integration required putting sdk.guard() inside each high-risk tool and configuring policies. Your agent logic, your multi-agent routing, and your tool implementations are unchanged. The governance layer is transparent to the agents and visible to your security and compliance teams.

Your agents are making decisions that affect real customers and real money. Now every one of those decisions is authorized, recorded, and provable.

OpenAI Agents SDK + Intended: Adding Authority to Every Tool Call | Intended