Autonomous resolution agent
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Autonomous resolution agent
An AI agent with tools — it resolves what it can, hands off what it can't, and stops when the workspace budget says stop.
The autonomous agent is the difference between "chatbot that drafts replies" and "chatbot that closes tickets." It has tools, a bounded loop, an allow-list of what it may call, and an escape valve to a human agent.
It runs in workspaces where you want end-to-end ticket deflection — order lookups, FAQ answers, ticket filing — without an agent in the loop.
Warning — Always pair it with a copilot fallback
Even the best autonomous setups hand off a large share of inbound to humans. You don't want those humans starting from scratch — keep the copilot on for the rest of the team so the hand-off feels seamless.
When to enable it
Good fit:
Comprehensive KB covering the top FAQ topics.
Stable, well-documented internal APIs you can call as custom HTTP tools (orders, billing, account state).
Volume that justifies it.
Bad fit:
Brand-new workspace with a thin KB.
High-stakes flows (medical, legal, financial advice) — let it draft, don't let it send.
Every ticket is bespoke (e.g. enterprise B2B sales).
Bots are configured per workspace under Bots (/bots), and routed to conversations by ai_bot_routes. A bot's status is active, paused or draft; paused and draft bots are skipped.
The loop
run queued (autonomous_runs row + BullMQ `autonomous_run` job)
│
▼
claim the run, mark `running`
│
▼
load transcript — last 20 chat-y messages + the kickoff message
│
▼
RAG — retrieveRelevant() on the latest customer turn,
injected into the system prompt
│
▼
pick a provider: workspace OpenAI BYOK → workspace Anthropic BYOK
→ internal AI service (single-shot, no tools)
│
▼
tool loop ── up to `maxToolCalls` iterations
│ each iteration: thread + tool defs → LLM
│ tool calls execute in their own short transaction
│ hitting the cap forces a "no more tools, final reply" turn
▼
guardrails on the final reply
│
▼
persist a bot-authored message + `completed | failed` on the run rowEach phase runs in its own short Postgres transaction, and the LLM loop holds none — LLM calls take seconds, and holding a connection across them exhausts the pool.
The limits that actually exist
Parameters
Name | Type | Description |
|---|---|---|
|
| Maximum tool-loop iterations before the agent is forced to produce a final reply. Editable in the bot editor, clamped to 1–32. Default: |
|
| The allow-list. |
|
|
|
|
|
|
|
| Structured rules. Trigger: |
Warning — There is no per-turn wallclock or per-conversation dollar cap
The bounds on a run are the tool-loop iteration cap, the escalation rules, the guardrails, and the workspace AI spend ceiling. There is no wallclock timeout on a turn and no per-conversation USD cap. If you need a hard money bound, set it at the provider — your OpenAI or Anthropic account is where the tokens are actually billed. See cost metering for what the plan ceiling does and does not bound.
Tools
Built-in catalog
These are the keys a bot's enabledTools list can hold. One list, shared by the dashboard picker and the worker's executor, so what you tick is what runs.
Built-in tools
Name | Type | Description |
|---|---|---|
|
| Search the workspace knowledge base. Scoped by the bot's |
|
| Read one attribute off the contact. |
|
| Write one attribute onto the contact. |
|
| Apply a tag. Capped at 64 characters. |
|
| Move the conversation between statuses. |
|
| Assign the conversation to a team. |
|
| Hand off. Routes through the API's escalate endpoint so the |
|
| Open a ticket linked to this conversation. |
|
| Uses the workspace's existing Linear integration install. |
|
| Uses the workspace's existing Slack integration install. |
|
| Re-read recent turns in the thread. |
|
| Offered to every workspace; answers |
Info — The allow-list is the gate, not a permission field
Tools do not carry a permission string. An autonomous bot has no user actor to check a permission against, so a permission-shaped field would gate nothing — and a control that looks like a control but isn't is worse than none. What a bot may call is the workspace admin's
enabledToolslist, and nothing else.
Custom HTTP tools
You wire your own systems as HTTP tools under Bots → AI tools (/ai-tools), or over the API:
GET /v1/ai/tools — Bearer token
POST /v1/ai/tools — Bearer token
PATCH /v1/ai/tools/{id} — Bearer token
DELETE /v1/ai/tools/{id} — Bearer token
Reads need ai:read; writes need ai:write — teaching the agent to POST somewhere with a stored credential is materially more powerful than editing a canned reply.
{
"name": "orders_find",
"description": "Look up an order by ID. Use when the customer asks about a specific order.",
"method": "GET",
"urlTemplate": "https://api.acme.com/orders/{{orderId}}",
"headers": { "Accept": "application/json" },
"secretHeaders": { "Authorization": "Bearer sk_live_..." },
"bodyTemplate": null,
"parameters": [
{
"name": "orderId",
"type": "string",
"required": true,
"description": "The order number the customer gave you"
}
],
"timeoutSeconds": 20,
"enabled": true,
"requiresApproval": false,
"approvalThresholdParam": null,
"approvalThresholdValue": null
}Notable fields
Name | Type | Description |
|---|---|---|
|
| AEAD-encrypted at rest and never returned by a read — reads project the key names only. Omit the key on a PATCH to keep the stored value; send |
|
| The host must be static. A template whose host contains a placeholder is rejected at write time, so the model can never choose where a credentialled request goes. The URL is checked against the outbound guard both when saved and when called. |
|
| At most 16. Server-supplied parameters are omitted from the schema the model sees. |
|
| Per-call timeout. Default: |
|
| Hold every call for a human decision before it runs. Default: |
|
| Conditional approval — hold only when this numeric argument exceeds the value. Small refunds pass, large ones wait for a person. |
Pending approvals are listed at GET /v1/ai/tool-calls and decided with POST /v1/ai/tool-calls/{id}/decide.
Info — Custom tools are resolved per workspace, per call
They are never registered into a shared process-wide registry. A tool carrying one workspace's credentials must not be visible to another workspace's agent in the same process, and a shared map is exactly how that happens.
Order lookup preset
Writing that JSON by hand is the reason most stores never do it, so the AI tools screen ships an order lookup preset. One field — the URL of your own endpoint — and one click creates an ordinary custom tool. There is no separate "preset" object: the row it writes is indistinguishable from a hand-written one, so everything on this page applies to it unchanged.
On Shopify, connect the Shopify integration and tick lookup_shopify_order instead. The preset exists for WooCommerce, BigCommerce, Squarespace and bespoke backends, which have no integration to connect.
Chatly calls your endpoint like this:
GET https://api.yourstore.com/chatly/order-lookup?order_number=1042&email=ada%40example.com
Accept: application/json
Authorization: <your shared secret, if you set one>Both values are percent-encoded, and the two parameters are appended with & if your URL already carries a query string. Answer HTTP 200 with a small JSON object:
{
"found": true,
"order_number": "1042",
"status": "shipped",
"placed_at": "2026-08-20T09:14:00Z",
"items": [{ "name": "Blue mug", "quantity": 2 }],
"total": "34.00",
"currency": "USD",
"tracking_url": "https://track.example.com/XYZ",
"estimated_delivery": "2026-08-30"
}Anything past 8,000 characters is truncated before the model sees it, so answer the question rather than returning the order record.
Warning — Match on the email, not only the order number
Order numbers are sequential and guessable. An endpoint that answers on the number alone will read somebody else's order out loud to whoever tries
#1041. Answer withfound: falsewhen the number and the email disagree.The
contacts.emailand is omitted from the schema the model sees, so a visitor cannot talk the agent into sending a different one. Its assurance is the channel's — a pre-chat form, anidentify()call, or the address an email conversation arrived from. Treat it as an ordinary support-agent level of confidence, not proof of purchase.
Warning — Return a non-2xx when your order system is down
Never answer 200 with
found:falsefor an outage. The model cannot tell an outage from an empty result, and it will tell the customer their order does not exist while sounding completely certain. A non-2xx flipstoolFailed, so atool_failureescalation rule can fetch a human instead.
A conversation with an anonymous visitor has no contacts.email, and the call is refused before it dials with missing required argument(s): email handed back to the model, which then asks the customer to identify themselves. Capture an email in the pre-chat form if you want the agent to answer on the first turn.
Handoff
The agent hands off by calling escalate_to_human(reason). That routes through the API's escalate endpoint, so the escalation row, the idempotency and the realtime envelope are identical to a visitor- or agent-initiated handoff. When the AI service cannot reach the API, the run still flags the handoff — the visitor-facing reply switches to a transfer message either way — but records handoffSubmitted: false so a degraded handoff is distinguishable from a completed one.
Escalation rules fire the handoff without waiting for the model to choose it. Configure them per bot; the trigger types are sentiment_negative, keyword, tool_failure and after_turns.
Running out of AI budget also forces a handoff: the run escalates, the visitor gets a reply and a person, and the workspace gets a notice. The escalation sets the conversation to pending, which is the state bot-routing stands down on — so the next inbound message does not start another run.
What customers see
Bot replies are authored as
botmessages and rendered with their own bubble style in the widget.Below the last bot message that ends an AI exchange, the widget shows two deflection chips: "This answered my question" and "Not really — talk to a human." The second one escalates.
Bot replies do not carry inline KB citations today. The retrieved chunks ground the answer; they are not rendered as clickable footnotes to the customer.
Observability
Autonomous runs are a durable ledger, not just logs. autonomous_runs records every invocation, and the dashboard reads it at Bots → (a bot) → Logs:
GET /v1/autonomous-agents/runs — Bearer token
GET /v1/autonomous-agents/runs/{id} — Bearer token
All four run endpoints require a signed-in session carrying triggers:read. The run list filters by status, date range and conversation id. The detail drawer shows duration, input and output tokens, cost, the final reply or failure, and every tool call with its raw arguments and result. GET /v1/autonomous-agents/runs/_tools and .../_sparklines back the 7-day tool-usage and run-volume panels.
This is the panel you use to debug "why did the bot say X?"
Cost
Autonomous runs are priced per model against the rate table and recorded on the workspace's BYOK spend meter, and the per-run cost is shown in the run detail. See cost metering.
With no model pinned, the worker's autonomous loop defaults to claude-sonnet-4-5 on an Anthropic key and gpt-4o-mini on an OpenAI key — the loop is customer-facing, so the Anthropic default is the mid-tier model rather than the cheap one.
Troubleshooting
Warning — The bot answers with text and never uses a tool
Check the bot's enabled tools list. An empty list is indistinguishable from a bot deliberately configured without tools, and the model simply has nothing to call. Confirm the keys match the catalog above.
Warning — The bot escalates everything to humans
Look at the escalation rules first — an
after_turnsorkeywordrule that matches too broadly will fire before the model gets a chance. Then check Bots → Logs for runs that failed, and for the AI-budget escalation reason.
Info — Loops on the same tool call
The bot is chasing a changing result. Check the tool's idempotency — it should return the same answer for the same args within a turn. Failing that, lower
maxToolCalls; hitting the cap forces a final reply rather than another hop.
Info — Tools are ticked but the bot has no memory of the KB
RAG only injects context when the workspace has an embedding provider. Without one, retrieval degrades to keyword search — see RAG + Knowledge Base.