Client tools
Client tools run on your side. When the model calls one, the task moves to interrupted; you send back the result and processing continues.
Unlike assistant tools (which the server calls), client tools are defined by you right in the request. They’re handy when the data or the action is only available on your side: the user’s location, access to your database, an interactive confirmation.
1. Declare the tools
Add client_tools to the POST /api/tasks body:
{
"assistant_id": 1,
"input": [{ "type": "text", "text": "What's near me?" }],
"client_tools": [
{
"name": "get_user_location",
"description": "Returns the user's city",
"input_schema": {
"type": "object",
"properties": { "precision": { "type": "string" } }
}
}
]
}
name and description are required, input_schema (JSON Schema) is optional.
2. Wait for the interrupt
If the model called a client tool, polling GET /api/tasks/{uuid} returns the
interrupted status and the list of pending calls in pending_tool_calls:
{
"uuid": "550e8400-…",
"status": "interrupted",
"pending_tool_calls": [
{ "id": "call_abc", "name": "get_user_location", "arguments": { "precision": "city" } }
]
}
3. Send back the results
Run the tools on your side and send the results. You must cover every call from pending_tool_calls.
{
"tool_results": [
{ "id": "call_abc", "content": "Berlin" }
]
}
Report a tool error like this:
{ "id": "call_abc", "error": "Location unavailable", "reason": "permission_denied" }
Each item carries an id (from pending_tool_calls) and either content or error (plus an optional reason).
4. Continuation
Once the results are in, the task goes back to processing. Keep polling: the
interrupted → tool-results → processing cycle may repeat several times until the task
reaches completed or failed.
POST /api/tasks → pending
GET /api/tasks/{uuid} → interrupted # the model called a client tool
POST …/tool-results → processing # you sent the result
GET /api/tasks/{uuid} → completed # done (or interrupted again)
POST …/cancel → canceled # or you declined to answer
POST /api/tasks/cancel-interrupted # cancel all interrupted tasks at once
5. Give up on an interrupted task
If you have nothing to answer with — the user left, the tool is unavailable, the scenario is no longer
needed — don’t leave the task hanging in interrupted: it stays active and keeps occupying
a slot in the concurrent task limit. Cancel it:
{ "reason": "The user closed the app" } // optional, up to 1000 characters
The task moves to the terminal canceled status, reason is stored in
error (default: Task canceled by the client.), and the slot in the limit is
freed immediately. If callback_url is set, the cancellation is delivered by webhook too.
{ "uuid": "550e8400-…", "status": "canceled" }
You can cancel only a task in the interrupted status — otherwise 409.
A task in pending or processing can’t be canceled: Aist Core doesn’t know about
the cancellation, will finish the turn anyway and send the result, which has already been charged.
Cancel all interrupted tasks at once
Got a 429 and don’t know which tasks are holding the limit? You can see them in the
task list (GET /api/tasks?status=interrupted)
and clear them all with a single request — typical cleanup after a process crash. The body is the same
(reason is optional and applies to all of them):
{ "canceled": 2, "uuids": ["550e8400-…", "660f9511-…"] }
Only your tasks in the interrupted status are affected — pending and
processing keep running. The request is idempotent: if there’s nothing to cancel, you get
{ "canceled": 0, "uuids": [] }, never a 409.
The final tool_call in output
Once the task is complete, the client tool call shows up in output as a regular tool_call (without a prefix):
{
"type": "tool_call",
"tool_call": {
"name": "get_user_location",
"input": { "precision": "city" },
"output": { "type": "content", "content": [{ "type": "text", "text": "Berlin" }] }
}
}
tool_resultsmust cover every pending call, otherwise422.POST /tool-resultsfor a task that isn’tinterruptedreturns409.- An interrupted task counts as active and towards the concurrent task limit — until you answer the call or cancel it.
POST /cancelworks only for a task in theinterruptedstatus, otherwise409.POST /cancel-interruptednever returns409: it cancels whatever there is, even zero tasks.
runTask walks through the whole interrupt cycle
on its own: it calls your handler, sends the result and continues until the task is done. Both kinds of
cancellation are in the SDK too: cancelTask(uuid, { reason }) and cancelInterruptedTasks().