Skip to content

A support agent per customer

Your app creates one agent per customer. It replies in your app and looks up orders with tools in your code. Refunds up to $50 run. Larger ones need approval in Slack. No agent can access another customer's orders. Idle agents stop, so thousands of them cost almost nothing.

~/.config/wardrik/config.yaml
org: shop
http:
trusted_workers: true
orgs:
shop: {policy: support}
channels:
slack:
type: slack
token: ${SLACK_BOT_TOKEN}
signing_secret: ${SLACK_SIGNING_SECRET}
members: {U024BE7LH: user:shop/@maria, U0G9QF9C6: user:shop/@sam}
notify:
targets:
support: {channel: slack, room: "#support"}
roles:
app:
rules: [{verbs: [create, define, ask, read, events, serve], kinds: [agent]}]
support:
rules: [{verbs: [read, events, decide], kinds: [agent]}]
bindings:
- {org: shop, role: app, subjects: [user:shop/@app]}
- {org: shop, role: support, subjects: [user:shop/@maria, user:shop/@sam]}
  • trusted_workers: true marks your app's results as trusted, so a lookup doesn't make every refund need approval.
  • The support policy applies to every agent in the org, regardless of its definition.
~/.config/wardrik/policies/support.yaml
allow:
- workers/list_orders
- workers/refund_order(amount <= 50)
ask:
workers/refund_order(amount > 50): A refund over $50.
support.ts
import { Wardrik } from "@wardrik/sdk";
import { z } from "zod";
import { db, payments } from "./app.ts"; // your app's own
export const wardrik = new Wardrik({ name: "support" });
// Wardrik passes the calling agent's ID. Your app maps it to a customer.
const customerOf = (agent?: string) => db.customers.findBy({ agentId: agent });
wardrik.tool({
name: "list_orders",
description: "List the customer's orders.",
input: z.object({}),
readOnly: true,
run: async (_args, { agent }) => db.orders.where({ customer: (await customerOf(agent)).id }),
});
wardrik.tool({
name: "refund_order",
description: "Refund part or all of one of the customer's orders, in US dollars.",
input: z.object({ order_id: z.string(), amount: z.number().positive() }),
idempotent: true,
run: async ({ order_id, amount }, { agent, idempotencyKey }) => {
const customer = (await customerOf(agent)).id;
return payments.refund({ order_id, customer, amount }, { idempotencyKey });
},
});
void wardrik.work();
chat.ts
import { wardrik } from "./support.ts";
import { db } from "./app.ts";
// The daemon creates the agent and returns its ID. Store the ID on the customer.
async function agentFor(customer: { id: string; agentId?: string }): Promise<string> {
if (customer.agentId) return customer.agentId;
const response = await fetch(`${process.env.WARDRIK_URL}/v1/owners/shop/agents`, {
method: "POST",
headers: { authorization: `Bearer ${process.env.WARDRIK_TOKEN}`, "content-type": "application/json" },
body: JSON.stringify({
name: customer.id,
instructions: "You help this customer with their orders. Be brief and kind.",
tools: ["workers/*"],
notify: { to: ["support"], on: ["parked"] },
}),
});
const { id } = await response.json();
await db.customers.update(customer.id, { agentId: id });
return id;
}
export async function answer(customer: { id: string; agentId?: string }, text: string) {
const agent = wardrik.agent(await agentFor(customer));
for await (const event of agent.stream(text)) {
if (event.type === "wardrik.message.delta") process.stdout.write(event.data.delta);
}
}
systemctl --user restart wardrik
wardrik token create shop-app --subject user:shop/@app # your app's WARDRIK_TOKEN
npm install @wardrik/[email protected] zod

Run your app and ask a customer's agent for a $20 refund. It runs. Ask for $200, and the call is posted to #support with Approve and Deny.

  • One agent per customer, each with its own history. Idle agents stop until the next message.
  • Tools receive the calling agent's ID from Wardrik, not from the model's arguments. A prompt can't make an agent read another customer's orders.
  • Refund limits are in the org policy. No agent definition can bypass them.
  • See Tools in your app.