Skip to main content

Triage button backed by an agent

This example adds a page to the KPI app. Each failed order gets a Triage button. Pressing it runs an agent: a function that, instead of running fixed code, takes several steps with a model (look at the order, check the numbers, decide) and returns a structured answer. Agents are slow (tens of seconds to minutes) and cost money (cents to about a dollar a run), so this page is also about doing that politely: progress lines, a Cancel button, and never running one on page load.

What you will have at the end​

A second page in Test KPI app, "Failed orders (yesterday)", with a table:

OrderReasonAmount
ORD-10442card_declined84.00Triage
ORD-10457out_of_stock219.50Triage

Press Triage and the row shows progress lines in plain words ("Step 1 · reading the order", "Step 3 · checking yesterday's failure rate"), a Cancel button, and then the result:

payments · The card was declined twice at the gateway within a minute, which matches the day's spike in declines. Payments should check whether the gateway rule that changed yesterday is rejecting valid cards.

Every triage is a run on the Runs page with its trace: every step, every tool the agent called, and what it cost.

What the agent will ask you before it builds​

  1. What should the routes be? This example uses payments, stock and other. If your team has different queues, say so now; the agent bakes the list into the output schema so the answer is always one of them.
  2. What may the agent look at? By default: the semantic model (the same orders data the app uses) and the order_kpis function from example 1, so it can compare an order against the day's failure rate. If you want it to read a ticketing or payments system, you name the connection; the agent must not guess connection names. Agents only read; they never change anything.
  3. How much may one triage cost, and how long may it take? The agent proposes the "Standard" preset: at most 20 steps, $1.25 and 5 minutes per run. It can only go lower than your workspace's caps, never higher.
  4. Why an agent and not something cheaper? The agent should say this unprompted: a plain code function cannot judge messy evidence, a one-shot model call cannot look things up, and a classifier only gives a label. Judgement over evidence is the one job an agent is for.
  5. Sample numbers. It runs the "yesterday's failed orders" query and shows you the count and a few rows: "Yesterday the model has 37 failed orders; the top reason is card_declined with 21. Does that match?" Confirm before it goes on.

The pieces it creates​

PieceNameWhat it is
Agent functiontriage_failed_order (kind: agent)Instructions, a task, input {order_id}, output {route, summary}, pinned to the model, allowed to call order_kpis
Change to an existing functionorder_kpis switched on for agentsSo the triage agent may call it (a person does this)
Declared queryfailed_orders_yesterdayYesterday's failed orders with id, reason, amount
App versionTest KPI app v2The new page, and the new import triage_failed_order

Steps, in order​

1. Prove the query (agent)​

query_run for yesterday's failed orders, shown to you as a sample. The agent reads the model's data window first: "yesterday" means the last full day the model has, and it tells you which day that is.

2. Write the agent function (agent)​

The agent creates triage_failed_order and writes its function.json. The heart of it is the agent spec:

{"schema": "bicycle.function/v1", "name": "triage_failed_order", "kind": "agent",
"title": "Triage a failed order", "mode": "async",
"input_schema": {"type": "object", "required": ["order_id"],
"properties": {"order_id": {"type": "string"}}},
"output_schema": {"type": "object", "required": ["route", "summary"],
"properties": {"route": {"enum": ["payments", "stock", "other"]},
"summary": {"type": "string"}}},
"capabilities": [],
"agent": {"spec": {
"spec_version": 1, "name": "triage_failed_order", "display": "Triage a failed order",
"system": "You triage failed orders for the operations team. Look up the order's failure reason first, then compare it with the day's failure rate using the order_kpis tool. Route to payments for card or gateway problems, stock for availability or allocation problems, other for anything else. Never invent facts you did not read.",
"task": "Decide which team should fix this failed order and say why in two sentences.",
"input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}},
"output_schema": {"type": "object", "properties": {"route": {"type": "string"}, "summary": {"type": "string"}}},
"semantic": {"model": "m_retail_demo"},
"capabilities": [],
"budgets": {"max_steps": 20, "max_cost_usd": 1.25, "max_wall_s": 300}}},
"grants": {"functions": ["fn:<tenant>/order_kpis@1"]},
"budgets": {"max_steps": 20, "max_cost_usd": 1.25, "max_wall_s": 300},
"visibility": {"audience": "tenant",
"expose": {"apps": true, "workflows": true, "agents": false, "mcp": false}}}

Things to check in what the agent wrote:

  • system holds the instructions in words you could have written. Connection know-how ("in the payments system, look at field X") goes here too, if you gave it a connection.
  • semantic.model pins the model, so the agent does not waste steps searching for data.
  • grants.functions lists order_kpis pinned to its version. Nothing that writes. No connections unless you named one.
  • The output schema's route is a closed list, so the answer is always one of your three routes.
  • The budget is at or under the Standard preset.

3. Switch order_kpis on for agents: Only a person​

For the triage agent to call order_kpis, that function must be exposed to agents (expose.agents). Turning that on widens who may use the function, so Studio does not let the agent do it.

Only a person: in Studio, open Functions, open order_kpis, and switch on "Chat and agents". Studio will confirm this as a new version if it needs to; if it does, the agent re-pins grants.functions to that version.

4. Try it before publishing: mostly a person​

The agent should offer to try the agent on one real order before publishing. Today a draft "Try it" run of an agent is done in Studio on the function's page, not through the agent's tools; the agent will tell you so rather than pretend. If you try it: pick one order id from the sample, run, and read the route and summary. Check the summary only says things you can see in the data.

5. Publish the agent: Only a person​

function_publish on the first version of an agent always needs a person; the agent gives you the link.

Only a person: open the link, read what the agent may use (the model, order_kpis, its budget), and confirm. The agent reads the pinned reference fn:<tenant>/triage_failed_order@1.

6. New app version (agent)​

The agent starts version 2 of the app (dataapp_new_version), declares the new query and the new import, and writes the page.

"functions": {
"order_kpis": {"ref": "fn:<tenant>/order_kpis@1"},
"triage_failed_order": {"ref": "fn:<tenant>/triage_failed_order@1"}
}

The Triage button, in outline:

triageButton.onClick = async () => {
triageButton.disabled = true
const started = await bda.fn.call('triage_failed_order', { order_id }, { wait: false })
const watch = bda.fn.watch(started.invocation_id, batch => {
// batch.items: plain-word lines, e.g. { title: "Step 3", detail: "checking the day's failure rate" }
showProgress(batch.items)
// status "queued" with queue_position: show "Queued · N ahead" ("Queued · next" at 0)
})
cancelButton.onClick = () => bda.fn.cancel(started.invocation_id)
try {
const out = bda.fn.outputOf(await watch.done) // { route, summary }
showResult(out.route, out.summary) // as text, never as HTML
} catch (e) {
showError(e.message) // "cancelled", "budget_exceeded", ... in words
}
}

What you can check without reading code:

  • The call passes wait: false, then watches. A triage is never started on page load. (If a product ever needed that, it would pass reuse; this one does not.)
  • There is a Cancel button, and it works (test below).
  • If the workspace's agent slots are all busy, the run waits its turn and the row says "Queued · 2 ahead". The app keeps watching; it does not start a second run.
  • The summary is shown as text. Agent output is untrusted: it may quote a ticket written by anyone, so the app never renders it as HTML and never follows instructions found in it.

7. Publish the app version: Only a person​

This version adds an import, so Studio asks a person to review it. Only a person: open the link and confirm.

How to test it​

  1. Open the app, go to the new page. The table should match the sample from step 1. No triage should have started on its own: check Runs; there should be no triage_failed_order run yet.
  2. Press Triage on one order. Watch the progress lines change. It usually takes 30 seconds to a couple of minutes.
  3. Read the result. The route must be one of your three; the summary must be two sentences that only say things you can see in the data. Check the cost on the Runs page: for one triage on the Standard preset it should be well under $0.50, for example $0.12.
  4. Press Triage on a second order and press Cancel after a few seconds. The row should say it was cancelled, and the Runs page should show that run as Cancelled.
  5. On the Runs page, open the completed triage. Its trace should show a child run of order_kpis (the tool call the agent made), with its own id and its own timing. That is how you know the agent used your function rather than guessing.

How to switch it off​

  • The Triage page. Publish a version without it, or unpublish the app.
  • The agent function. In Studio, open triage_failed_order and disable it (owners and admins; the agent's tool for this is landing). Pinned callers stop being able to run it; nothing is deleted.
  • order_kpis for agents. If nothing else needs it, switch "Chat and agents" back off on the function's page. Note that chat with functions needs it on.

What you learned​

  • An agent is for judgement over evidence; everything else is cheaper and faster.
  • The spec says what the agent may read; a person switches on anything it may call; the first publish is a person's.
  • Slow calls: start, watch, allow Cancel, show the queue, never on page load.
  • The trace shows every step and every tool call, so you can see how the answer was reached.

Read more: Agents for the spec, presets and queue; using functions in apps for progress, Cancel and errors; Runs and traces for reading a trace.

Next: Weekly digest email with a screenshot.

Copy for your agent
Add a page to my "Test KPI app" (a new version) on preview that lists yesterday's failed orders,
each with a "Triage" button. Triage runs an agent function named triage_failed_order that returns
a route (payments / stock / other) and a two-sentence summary. Show progress lines and let me cancel.

How to work with me:
- Before you build, tell me why this should be an agent rather than a code, Ask AI or classify
function. Ask me what the routes should be and what the agent may look at. Do not guess
connection names; if I do not name one, give it only the semantic model and my order_kpis function.
- Run the "yesterday's failed orders" query first and show me the count and a few rows to check.
- Use the Standard preset or lower (20 steps, $1.25, 300 s). Pin the model in the spec. Nothing that writes.
- Steps that need me: switching order_kpis on for agents (tell me where in Studio), the agent's
first publish (give me the link), the app version that adds the import (give me the link). Wait each time.
- Offer a try run on one real order before publishing; if you cannot do it with your tools, say so.
- In the app: call with wait: false, watch progress with bda.fn.watch, a Cancel button that calls
bda.fn.cancel, show "Queued · N ahead" when queued, render the output as text. Never run it on page load.
- Only create things titled "Test ...". Never change an existing app.
- When done, tell me how to test one triage, how to cancel one, and how to switch it all off.