Executing tools
When an agent calls a tool, the platform calls your adapter's POST /v1/tools/execute. Your adapter runs it against your systems and answers with the tool envelope.
On this page
- An adapter reachable over HTTPS: Adapter overview
- At least one tool declared in your Domain Pack: Tools
The request
The platform calls your adapter with the tool’s name, the parameters the agent chose, and a context that says whose conversation this is. It authenticates with your adapter secret in the x-api-key header. Compare it with the secret you configured and answer 401 when it does not match.
{
"toolName": "find_slots",
"parameters": { "service": "brake-service", "date": "2026-11-09" },
"context": {
"domainId": "cogwheel-cycles",
"partnerId": "riverside",
"sessionId": "8a41c2e0",
"phone": "12025550142",
"countryCode": "1",
"language": "en"
}
}
| Field | Type | Meaning |
|---|---|---|
toolName | string | The tool’s name as declared in your Domain Pack. |
parameters | object | The arguments the agent chose for the tool’s parameters. |
context | object | Who the call is for: partnerId, phone, and when known contactId, sessionId, language, countryCode, timezone, domainId, operationKey. Accept keys you do not know: the object may grow. |
context.phone is the contact’s number as the platform holds it. Look the contact up by it, and build any store key from it, without reformatting it.
Answer with a success envelope
Every adapter reply is wrapped in data. Inside it, content is the text the agent reads. data is optional: a structured result beside the text, which a single-turn run returns to its caller.
if (toolName === "find_slots") {
const slots = freeSlots(shopOf(context.partnerId), parameters.date);
return reply(200, {
data: {
isError: false,
content: `${slots.length} free slot(s) on ${parameters.date}. Offer these times; book only a slot_id from this list.`,
data: { service: parameters.service, slots },
},
});
}
if tool_name == "find_slots":
slots = free_slots(shop_of(context["partnerId"]), parameters["date"])
return 200, {"data": {
"isError": False,
"content": f"{len(slots)} free slot(s) on {parameters['date']}. Offer these times; book only a slot_id from this list.",
"data": {"service": parameters["service"], "slots": slots},
}}
{
"data": {
"isError": false,
"content": "3 free slot(s) on 2026-11-09. Offer these times; book only a slot_id from this list.",
"data": {
"service": "brake-service",
"slots": [
{ "slot_id": "rv-20261109-0930", "date": "2026-11-09", "time": "09:30" },
{ "slot_id": "rv-20261109-1100", "date": "2026-11-09", "time": "11:00" },
{ "slot_id": "rv-20261109-1530", "date": "2026-11-09", "time": "15:30" }
]
}
}
}
The example adapters in examples/cogwheel-cycles implement all five Cogwheel tools this way, in TypeScript and in Python.
Keep what the agent should know
A success envelope may carry store: entries the platform adds to what the agent knows about the contact, so the agent sees them on the next turn without calling a tool again. With isTemporary: true they last for the conversation. With isTemporary: false they are saved in the contact’s durable context under storeKey, which MUST be partnerId/phone exactly as context sent them. facts updates the facts your tools can require.
{
"data": {
"isError": false,
"content": "Booked: Brake service on 2026-11-09 at 11:00, Cogwheel Cycles Riverside, reference CW-1000.",
"store": {
"isTemporary": false,
"storeKey": "riverside/12025550142",
"data": [
{ "key": "registered_user", "value": "- **CONTACT_NAME**: Sam Rider" },
{ "key": "open_booking", "value": "- **OPEN_BOOKING**: Brake service on 2026-11-09 at 11:00, reference CW-1000" }
],
"facts": { "has_open_booking": true, "open_booking_ref": "CW-1000" }
},
"data": { "booking_ref": "CW-1000", "date": "2026-11-09", "time": "11:00" }
}
}
When a call changes something, return the state after the change. An agent that reads stale state after a booking tells the contact the wrong thing. See Tool design rules.
Answer with an error envelope
When the call fails, say why in words the agent can act on. The message reaches the agent as it is, so include what it needs to recover: the next open day, the valid values, the record that already exists. Put anything structured that helps, such as the next free slots, in partial_results.
{
"data": {
"isError": true,
"content": {
"errorCategory": "business",
"isRetryable": false,
"message": "Cogwheel Cycles Old Town is closed on 2026-11-09. It is open Tuesday to Saturday, 10:00 to 19:00.",
"attempted_query": "service=brake-service; date=2026-11-09",
"partial_results": { "nextOpenDay": "2026-11-10", "slots": [] }
}
}
}
errorCategory | Use it when |
|---|---|
business | Your rules refuse the request: closed, fully booked, already booked. |
validation | A parameter is wrong or missing. |
permission | This contact or Partner may not do this, or may not see that record. |
transient | Your business system failed or timed out; the same call may work later. Set isRetryable to true. |
internal | Set by the platform, never by you: your reply did not follow the envelope, or the call failed inside the platform. |
attempted_query is the call’s parameters as key=value pairs separated by ; , or null.
Retries and timeouts
A tool may have side effects. isRetryable tells the agent whether calling again could succeed; it is information, not an instruction to the platform.
If your adapter does not answer in time, the agent receives a transient error saying the call may have completed. Make write tools safe to check: the agent’s next step is to read the current state before it tries again.