← Blog

Build a B2B procurement agent that survives human review

20 May 2026

Most "AI shopping agent" demos end at checkout. The agent finds a product, adds it to a cart, pays, done. That story works for a $40 pair of headphones. It falls apart the moment you point the same agent at a B2B purchase order.

In procurement, the interesting part isn't the buying — it's the gate before the buying:

  • Purchases above a threshold need a human to approve them.
  • That approval might come back in 30 seconds, or after a 2-day Slack thread.
  • Every step has to land in an audit trail — who approved what, against which PO, for which expense category.

An agent that can't pause for a human, and can't survive the process restarting while it waits, isn't a procurement agent. It's a liability. Here's how to build one that can.

The shape: a chain of sub-agents

A procurement flow is really three jobs in sequence:

  1. find-best-price — comparison-shop across merchants, pick the lowest total per line item.
  2. request-approval — attach a PO number, then block at an approval gate if the cart exceeds the threshold.
  3. checkout-and-track — submit payment, subscribe to order updates.

In Agorio that maps directly onto AgentChain — sub-agents wired in sequence, each one's output piped into the next:

import {
  AgentChain,
  ShoppingAgent,
  ClaudeAdapter,
  agorioCloud,
  FileSessionStorage,
} from '@agorio/sdk';
import { createApprovalWorkflowPlugin } from '@agorio/plugin-approval-workflow';
import { createSpendingControlsPlugin } from '@agorio/plugin-spending-controls';
import { createAuditTrailPlugin } from '@agorio/plugin-audit-trail';
import { createProcurementPlugin } from '@agorio/plugin-procurement';

const cloud = agorioCloud({ apiKey: process.env.AGORIO_API_KEY! });
const storage = new FileSessionStorage({ dir: './sessions' });

// Governance plugins, applied as middleware to every sub-agent.
const plugins = () => [
  createSpendingControlsPlugin({ perTransactionLimit: 5_000, dailyLimit: 25_000 }),
  createApprovalWorkflowPlugin({ requireApprovalAbove: 1_000 }),
  createAuditTrailPlugin({ output: 'callback', callback: shipToYourLog }),
  createProcurementPlugin({
    expenseCategories: ['office-supplies', 'it-equipment', 'furniture'],
    requirePoOnCheckout: true,
  }),
];

const chain = new AgentChain()
  .add({ name: 'find-best-price', build: (ctx) =>
      new ShoppingAgent({ llm, tracer: ctx.tracer, onLog: ctx.onLog, plugins: plugins() }) })
  .add({ name: 'request-approval', build: (ctx) =>
      new ShoppingAgent({
        llm,
        tracer: ctx.tracer,
        onLog: ctx.onLog,
        plugins: plugins(),
        sessionStorage: storage,
        sessionId: 'po-2026-04829', // survives a process restart
      }) })
  .add({ name: 'checkout-and-track', build: (ctx) =>
      new ShoppingAgent({ llm, tracer: ctx.tracer, onLog: ctx.onLog, plugins: plugins() }) });

const result = await chain.run(
  'Order 100 ergonomic chairs for the new procurement team',
  { tracer: cloud.tracer, onLog: cloud.onLog },
);

The approval gate the LLM can't talk its way past

The approval-workflow plugin is middleware. It runs onBeforeToolCall, and when the cart total crosses requireApprovalAbove, it blocks submit_payment before it fires. This matters: it's not a prompt instruction the model can be argued out of — it's a hard interrupt in the agent loop. No "ignore previous instructions" gets a $50k order through without a human.

The same goes for spending-controls: per-transaction and rolling daily caps are enforced in middleware, so the LLM never even reaches the payment call once a limit is hit.

Surviving the wait

Here's the part that separates a demo from production. When the agent hits the approval gate, it doesn't busy-wait. It serializes its state — cart, PO number, conversation, plugin state — to SessionStorage and stops.

Days later, when a human clicks Approve, you reconstruct the agent with the same sessionId and it picks up exactly where it left off:

const agent = new ShoppingAgent({
  llm,
  plugins: plugins(),
  sessionStorage: storage,
  sessionId: 'po-2026-04829', // same id → hydrates the paused state
});
await agent.run('Approval granted — complete the purchase.');

The approval-workflow plugin implements the hydrate() hook, so its gate state restores alongside the agent. Use FileSessionStorage for a single box, or @agorio/session-redis when the approver and the worker are different processes.

The audit trail comes for free

Every tool call flows through the audit-trail plugin and, if you wired agorioCloud(), streams to the trace explorer. PO number, vendor, amount, and expense category are first-class fields — exactly the shape an EU AI Act Annex IV record wants. A multi-agent run renders as a tree: each sub-agent's LLM calls, tool calls, and audit events indented under the parent, on one shareable URL.

Run it yourself

The full reference agent — three sub-agents, all five governance plugins, against three mock merchants — runs end-to-end in about ten seconds with no API keys required:

git clone https://github.com/Nolpak14/agorio && cd agorio
npm i
npx tsx examples/procurement/index.ts

Next steps: see the full walkthrough on the procurement landing page, or wire agorioCloud() into your own agent and watch the chain render in the trace explorer.

Build a B2B procurement agent that survives human review — Agorio