Real Cases
Backoffice Agent
An admin workflow with side effects, approvals, audit records, and idempotency.
This pattern is for internal operations agents that can change product state. The harness must treat every write as a product operation with permissions, approvals, idempotency, and audit records.
Scenario
A support lead asks an agent to resolve a billing ticket, issue a refund, and notify the customer. The model can help coordinate the work, but application code owns every restricted operation.
When to Use It
Use this pattern when the agent can:
- issue refunds
- close or transition tickets
- update accounts
- send customer messages
- trigger jobs or webhooks
- access admin-only data
Architecture Shape
| Layer | Responsibility |
|---|---|
| runner | resolve admin actor, ticket, tenant, approval run, trace metadata |
| agent | coordinate the workflow and call tools |
| write tools | validate inputs and call product services |
| approval runtime | store decisions, notify reviewers, wait or resume |
| service layer | permissions, transactions, idempotency, audit |
| storage | ticket state, operation records, audit records, traces |
Code Example
import { AgentBuilder, Message, createHook } from "@anvia/core";
import { model } from "./model";
import { createBackofficeTools } from "./tools";
export async function runBackofficeResolution(input: BackofficeInput) {
const actor = await input.auth.requireAdmin();
const ticket = await input.tickets.getForTenant(input.ticketId, actor.tenantId);
const approvalHook = createHook({
async onToolCall({ toolName, tool }) {
if (!["issue_refund", "send_customer_email", "close_account"].includes(toolName)) {
return tool.run();
}
const approved = await input.approvals.waitForDecision({
actorId: actor.id,
tenantId: actor.tenantId,
ticketId: ticket.id,
toolName,
});
return approved ? tool.run() : tool.cancel("Operation was not approved.");
},
});
const agent = new AgentBuilder("backoffice", model)
.instructions(`
Help resolve backoffice tickets.
Use tools for account data and product changes.
Never claim that a side effect happened unless the tool result confirms it.
`)
.tools(
createBackofficeTools({
actorId: actor.id,
tenantId: actor.tenantId,
ticketId: ticket.id,
billing: input.services.billing,
tickets: input.services.tickets,
messages: input.services.messages,
audit: input.audit,
}),
)
.hook(approvalHook)
.defaultMaxTurns(5)
.build();
const response = await agent
.prompt([
Message.user(`Ticket ${ticket.id}: ${ticket.summary}`),
Message.user(input.instruction),
])
.withTrace({
name: "backoffice-resolution",
userId: actor.id,
metadata: {
tenantId: actor.tenantId,
ticketId: ticket.id,
},
})
.send();
await input.audit.write({
actorId: actor.id,
tenantId: actor.tenantId,
action: "backoffice.agent_run",
targetId: ticket.id,
});
return response.output;
}Write Tool Shape
async execute({ orderId, amount, reason }) {
const operationId = `refund:${scope.tenantId}:${orderId}:${amount}`;
const result = await scope.billing.issueRefund({
actorId: scope.actorId,
tenantId: scope.tenantId,
orderId,
amount,
reason,
operationId,
});
await scope.audit.write({
actorId: scope.actorId,
tenantId: scope.tenantId,
action: "refund.issued",
targetId: orderId,
operationId,
});
return result;
}Failure Modes
| Failure | Fix |
|---|---|
| duplicate refund | idempotency key in service layer |
| model performs write without approval | tool approval metadata or onToolCall hook |
| approval blocks HTTP request too long | store approval and resume asynchronously |
| audit only appears in traces | write product audit records |
| admin data leaks | enforce actor and tenant permissions in services |
Test Checklist
- Test admin auth and tenant scoping.
- Test approval approved, rejected, and timed out paths.
- Test write tools call services with idempotency keys.
- Test audit records for each successful side effect.
- Test runner maps cancellation to a product-safe response.
- Use Studio streaming runs to inspect approval behavior locally.
