Build apps
Embedding agents
How to write an agent definition, start AgentRuns from your app and get their results, and handle approvals for external actions.
This page explains how to embed agents in your app. An agent works within the purpose, input, tools, and output in its definition, and external actions are carried out by Hane only after approval. The concepts are covered in Agents.
Define an agent
Section titled “Define an agent”apiVersion: agents/v1alpha1kind: Agentmetadata: name: vehicle-checkspec: purpose: Look up a vehicle's model, inspection expiry, and odometer in the vehicle ledger by registration number examples: - When does the inspection for TEST 300 A 10-02 expire? input: type: object required: [vehicle] properties: vehicle: { type: string, title: Registration number } driver: llm instructions: |- 1. Call data.find_record with type Vehicle and key set to the input vehicle. 2. If found, put the model, inspection_expiry, and odometer_km values into submit_result with status answered. 3. If not found, hand over with request_human_help and set status to handed_over. Never make up values that are not in the ledger. tools: [data.find_record, request_human_help] handover: assignment: fleet-desk limits: maxToolCalls: 4 maxSeconds: 60 output: type: object required: [status, vehicle] properties: status: title: Result oneOf: - { const: answered, title: Answered } - { const: handed_over, title: Handed over } vehicle: { title: Registration number, type: string } model: { title: Model, type: [string, "null"] } inspection_expiry: { title: Inspection expiry, type: [string, "null"] } odometer_km: { title: Odometer (km), type: [integer, "null"] }| Field | What it means |
|---|---|
purpose, examples |
What the agent does and how people ask for it. Reception and the chat agent use these to choose an agent |
input |
JSON Schema of the input. Agents that can be asked from the chat have an input |
driver |
How it runs. See the table below |
tools |
Tools it may call. The Tool Gateway checks on every call |
limits |
Number of tool calls (up to 50), seconds (up to 600), model tokens |
output |
JSON Schema of the result. The submit_result value is checked against it. title becomes the label on screens |
approval |
The assignment rule and DeciderPolicy for approving external actions |
handover |
The assignment rule for Tasks created by request_human_help |
Drivers
Section titled “Drivers”| driver | How it runs | Availability |
|---|---|---|
scripted |
Calls tools in the order written in script |
Available |
llm |
A model chooses tools. Requires instructions |
Available |
opencode |
Runs a CLI in a sandbox; model calls go through the Model Gateway | Preview |
codex, claude |
Runs Codex CLI or Claude Code in a sandbox; tools are called through the Tool Gateway over MCP | Preview Local development environment only |
Don’t let agents make business decisions such as calculations or reconciliation; write those in calculations or Workflows. Agents call them through tools such as calculations.run, and the server decides the values and their value states.
Start an AgentRun
Section titled “Start an AgentRun”const run = await unwrap( hane.POST("/v1/agents/runs", { body: { agent: "vehicle-check", input: { vehicle: "TEST 300 A 10-02" } } }),);// run.status is queued. Read the state with run.run_idRuns are asynchronous. The status moves from queued to running to succeeded, failed, cancelled, or limit_exceeded. When the run ends, result holds the result checked against the output schema.
async function waitRun(id: string) { for (;;) { const r = await unwrap(hane.GET("/v1/agents/runs/{id}", { params: { path: { id } } })); if (r.status !== "queued" && r.status !== "running") return r; await new Promise((ok) => setTimeout(ok, 2000)); }}const done = await waitRun(run.run_id);console.log(done.status, done.result, done.trace.map((t) => `${t.kind} ${t.tool ?? ""} ${t.summary}`));- To show progress on screens, use the conversations of the embedded chat, which stream tool starts and ends and pending approvals over SSE. The API that starts a single AgentRun has no progress stream.
- To learn when runs end in your own systems, subscribe to
agents.run.succeeded,agents.run.failed, and similar events with a webhook (Webhooks). - With
shadow: true, the run performs no external actions or handovers and only leaves records. Use this to check a definition before changing it. - To stop a run, call
POST /v1/agents/runs/{id}/cancel(with body{}). Its next tool call is refused.
Ask in free text (reception)
Section titled “Ask in free text (reception)”When you don’t know which agent to ask, pass free text to reception. Reception chooses an agent from the purposes and examples of the active agents, and picks input values from the text according to the input schema.
const r = await unwrap(hane.POST("/v1/agents/requests", { body: { messages: ["When does contract C-1001 end?"] } }));if (r.outcome === "started") console.log(r.run?.run_id);if (r.outcome === "needs_input") console.log(r.missing.map((m) => m.title)); // ask backif (r.outcome === "unmatched") console.log(r.candidates);After asking back and getting an answer, append it to messages and send again.
External actions and approvals
Section titled “External actions and approvals”When an agent calls a tool for email, SMS, calls, writes to your APIs, or operations performed by a person, Hane creates an ActionRequest that fixes the recipient and content, adds a digest, and creates an approval Task for the approval.assignment group. The agent returns its result without waiting for approval, and Hane sends only after approval.
spec: tools: [data.find_record, communication.send_email, request_human_help] sender: fleet-desk # sender identity. Unusable until an administrator approves it approval: assignment: fleet-approval decider: lease-application # optional. DeciderPolicy that sets deciders and stages by amount, for exampleIn your app, read the ActionRequests linked to a run and show their state.
const actions = await unwrap(hane.GET("/v1/agents/action-requests", { params: { query: { run_id: run.run_id } } }));for (const a of actions.items) console.log(a.tool, a.state, a.parameters.contact?.masked, a.approval_task_id);Someone other than the requester decides the approval as a Task. See Using data and Workflows for how. Before approval, you can cancel it with POST /v1/agents/action-requests/{id}/cancel.
Operations performed by a person
Section titled “Operations performed by a person”For operations a person performs outside Hane, such as mailing or visiting a counter (human.perform), an execution Task goes to the assignee after approval. The assignee does exactly what was approved and records the result with POST /v1/agents/action-requests/{id}/perform. A result that differs from the approval cannot be recorded as accepted; recording it as a deviation creates a Task to get approval again.