Agents

Chat with AI agents via REST. For SDK helpers (sendMessage, run, streaming), see the Agent SDK.

Most integrations use POST /agents/run: it creates a chat when needed, sends the user message, and returns assistant output. For a pure chat-first flow (create a chat, then send messages separately), use POST /chats followed by POST /chats/:id/messages, the same pattern the web app and React AgentChatProvider use for new conversations (client.agent().sendMessage() still uses POST /agents/run). Use the tool endpoints below for client tools, widgets, and human-in-the-loop (HIL) approval.


POST /agents/run

Send a message to a template or ad-hoc agent. Creates a new chat when chat_id is omitted. For a new template-backed chat in the agent's home workspace, the resolved agent version's permissions (for example allow_all_tools) are copied into the chat once at creation.

Request

FieldTypeRequiredDescription
chat_idstringNoExisting chat ID (omit to start a new chat)
agentstringNoTemplate agent ref (namespace/name or namespace/name@version)
agent_idstringNoStored agent ID
agent_version_idstringNoVersion of agent_id to run (full or short ID). Without it the agent's current version runs
agent_configobjectNoAd-hoc agent config (tools, system prompt, core app)
agent_namestringNoDisplay name for ad-hoc chats
contextobjectNoPer-chat context variables (string values). Stored on a new chat, or on an existing chat that has none
inputobjectYesThe message (see below)
streambooleanNoIf true, response is SSE instead of JSON

Name the agent exactly one way: agent, agent_id (with optional agent_version_id), or agent_config. Or name none and send only chat_id to post into a chat that already has its agent. The API returns 400 validation_error when the agent is named more than one way, when agent_version_id comes without agent_id, or when neither an agent nor chat_id is given. An agent_version_id that does not exist or that you cannot read returns 404, and nothing is written.

A body that still carries channel_context is accepted and the field is ignored. See Channel context. trigger_id and tool_invocation_id in a client's body are ignored too: the server sets them for messages that triggers and sub-agent calls send.

input fields (MessageInput):

FieldTypeDescription
textstringUser message text
attachmentsarrayFile refs from the Files API (POST /files): uri, filename, content_type, optional id and size. Images reach the model as image input
imagesarrayDeprecated. Image URIs, read only when attachments is empty
filesarrayDeprecated. File URIs, read only when attachments is empty

Other fields older clients send in input (role, context, system_prompt, context_size, reasoning, tools, settings) are accepted and ignored. Settings, the system prompt, tools and history come from the agent and the chat.

Template agent (new chat):

json
1{2  "agent": "myteam/support-agent@latest",3  "context": {4    "project_id": "proj_456"5  },6  "input": {7    "text": "Summarize open tickets"8  }9}

Continue chat:

json
1{2  "chat_id": "chat_abc123",3  "input": {4    "text": "What about priority ones?"5  }6}

Response

The response body is the DTO under data:

json
1{2  "user_message": { "id": "msg_...", "chat_id": "chat_abc123", "role": "user", "content": [...] },3  "assistant_message": { "id": "msg_...", "chat_id": "chat_abc123", "role": "assistant", "content": [...], "tool_invocations": [...] }4}

See REST overview — response format.

Set stream: true to receive message updates over SSE. See Stream chat updates. Streaming applies when a new agent run starts — if the chat already has an active run, the message is auto-queued instead (no SSE stream).

The turn runs on the server independently of the request. Closing a streaming POST /agents/run connection does not stop the turn: reconnect with Stream chat updates or poll List chat messages to pick it up. Use Stop chat to end a turn.

Queuing while a run is active

When chat_id points to a chat with a non-terminal agent run (status: busy), POST /agents/run and POST /agents/messages do not start a new run. The user message is queued for the agent's next turn, the same behavior as Send chat message.

Each chat has at most one active agent run. Rapid follow-up messages are queued rather than starting a parallel run or stopping the in-progress response — use Stop chat to cancel explicitly.

Remote harness exception. When the active run uses a profile with capabilities.steer: true (for example Claude Code or Codex, as reported at registration), mid-turn messages are forwarded into the running harness turn at once. The user message is ready immediately and is tied to the active run — not held until the next turn. Profiles with steer: false (typical ACP harnesses) keep the queued behavior below. See Remote harness runs — Mid-turn messages.

The response returns immediately with only user_message (status: "queued" for most runs, or "ready" when a steerable remote harness accepted the message mid-turn). assistant_message is omitted. Poll or stream the chat to see when the agent picks up the message and generates a reply.

json
1{2  "user_message": {3    "id": "msg_...",4    "chat_id": "chat_abc123",5    "role": "user",6    "status": "queued",7    "content": [{ "type": "text", "text": "Focus on P0 tickets only" }]8  }9}

Use POST /chats/:id/messages when you only need to send a message without other run parameters. See Chat message status.


Send message (alternate)

POST /agents/messages or POST /agents/message

Both are the same endpoint as Run agent: same request body (including stream) and same response.

json
1{2  "agent_id": "agent_abc123",3  "agent_version_id": "ver_xyz789",4  "input": { "text": "Hello" }5}

Create chat

POST /chats

Create a chat, optionally assigning a template agent before the first message. Pair with Send chat message for a two-step new conversation: create the chat, then post the first user message.

Use POST /agents/run when you want one call that creates the chat, sends the message, and returns assistant output — recommended for scripts, ad-hoc agent_config, per-chat context, attachments, and A2A integrations. Use the chat-first flow when you need a chat ID before sending (for example to open a stream or navigate in a SPA).

Requires conversations:write scope.

Request

FieldTypeRequiredDescription
agentstringNoTemplate agent ref (namespace/name or namespace/name@version)
namestringNoDisplay name for the chat
contextobjectNoPer-chat context variables (string values)

When agent is provided, the API resolves the template and pins agent_id and agent_version_id on the chat (same version pinning as Run agent). When the chat is in the agent's home workspace, the pinned version's permissions (for example allow_all_tools) are copied once into this chat's agent_data when the chat is created; chats another workspace starts with a published agent do not copy those defaults. The chat owns copied values from then on (see Chat settings). Omit agent to create an empty chat and assign an agent later with Set agent.

Response

Returns the chat DTO (same shape as Get chat).

Errors

HTTP statusCodeWhen
400invalid_requestInvalid request body
404not_foundAgent ref not found

Example — two-step new conversation

bash
1# 1. Create chat with agent2curl -X POST https://api.inference.sh/chats \3  -H "Authorization: Bearer inf_your_key" \4  -H "Content-Type: application/json" \5  -d '{"agent": "myteam/support-agent@latest", "name": "Support"}'67# 2. Send first message (starts the agent run)8curl -X POST https://api.inference.sh/chats/chat_abc123/messages \9  -H "Authorization: Bearer inf_your_key" \10  -H "Content-Type: application/json" \11  -d '{"message": "Summarize open tickets"}'

JavaScript SDK: AgentChatProvider / useAgentActions().sendMessage() on a template agent with no bound chat ID calls POST /chats, then POST /chats/{id}/messages; call reset() before the next message to start a new chat. client.agent().sendMessage() still uses POST /agents/run.


List chats

GET /chats or POST /chats/list

Cursor-paginated list of chats. POST /chats/list accepts a JSON body with limit, cursor, direction, and filters (for example status in busy, awaiting_input).

Each item includes the default chat projection: id, name, status, active_run_id, and channel_context when the chat originated from an external messaging channel. Use Get chat for the full metadata shape (output, context, active_run, …).


Get chat

GET /chats/:id

Returns chat metadata — status, structured output, per-chat context, optional channel_context, and active run. Does not include message history or tool invocations.

Response

FieldTypeDescription
idstringChat ID
namestringDisplay name in the chat list and settings
short_idstringShort id used in public share links (https://1nf.sh/chats/{short_id})
visibilitystringWho can open the chat: private (only you), team (your workspace), or public (anyone with the link)
statusstringbusy, idle, awaiting_input, or completed
agent_dataobjectPer-chat agent state: memory (key–value notes from the agent's memory tools), allow_all_tools (when true, every tool runs without asking), and disable_hooks (when true, lifecycle hooks do not run in this chat). Per-tool approvals are stored as chat rules, not in agent_data
outputobjectStructured result from the agent's finish tool (when set)
contextobjectPer-chat context variables, used as {{context.NAME}} in tool calls and URL templates (see Per-chat context)
channel_contextobjectPresent when the chat came through an external messaging channel (Slack thread, Telegram chat, …): channel_type plus opaque channel_metadata used to route replies back. Omitted for chats started in the app or SDK. See Channel context.
work_dirstringWhen set, the folder a remote harness works in for this chat
harness_session_idstringWhen set, the harness's own session id on the remote (for example to resume with claude --resume)
active_runobjectCurrent agent run, when one is in progress
work_dirstringWhen a remote harness drives this chat, the working directory on that machine (set at launch). Harness sessions are folder-bound; each chat can use a different folder with the same harness agent.
harness_session_idstring | nullWhen a remote harness profile drives this chat, the harness vendor's session id (for example to resume on that machine). Omitted when the platform loop runs the agent.
work_dirstringWhen set, the folder on the remote where the harness works for this chat (from launch or assignment). Omitted when empty.
forked_from_message_idstring | nullMessage id this chat was branched from. Not the same as parent_id on sub-agent chats.

context and channel_context are unrelated: context holds the context variables you pass when starting a chat; channel_context is set by the platform when a chat is created from a messaging channel webhook.

json
1{2  "id": "chat_abc123",3  "status": "awaiting_input",4  "output": null,5  "context": { "project_id": "proj_123" },6  "channel_context": {7    "channel_type": "slack",8    "channel_metadata": { "channel": "C01234567", "thread_ts": "1234567890.123456" }9  },10  "active_run": {11    "id": "run_abc123",12    "state": "awaiting_input"13  }14}

channel_context is omitted on chats that did not originate from an external channel. channel_metadata shape is provider-specific and opaque to callers — treat it as pass-through data for the platform's outbound delivery.

output is populated when the agent calls the internal finish tool. See Structured Output.

For message history and tool invocations, use List chat messages. For live updates, use Stream chat updates or GET /chats/:id/status for status-only polling.

Change name, visibility, per-chat tool approval defaults, or whether lifecycle hooks run with Chat settings. In the workspace, open chat settings from the gear icon in the chat header; see Chat settings. When a chat is created in the agent's home workspace, allow_all_tools is copied from the agent version's permissions.allow_all_tools when set; after that, only chat settings change it for that conversation.

Remote placement on chats and runs

When an agent is bound to a remote harness profile, responses include where thinking happens and which harness session backs the chat:

FieldOnMeaning
harnessAgentWhat drives the agent: inference (platform loop, default for deployed templates) or an agentprotocol registry id (claude, codex, …) for a harness on a remote. The UI uses this to decide what is editable; the registry describes display name, vendor, and capabilities. Harness agents use the harness's own instructions, tools, and versions — not AgentVersionDTO config.
profile_idAgent, active_runHarness profile that thinks for the agent or run. Omitted or null when the platform loop runs the agent.
remote_idAgent, active_runRemote machine id. Terminal and remote internal tools use this host by default.
work_dirChatFolder on the remote for this harness session (from Launch harness chat or the workspace).
harness_session_idChatVendor session id on the remote, stable for the chat's lifetime.
forked_from_message_idChatBranch point when this chat was forked from another conversation.

On AgentDTO, harness, profile_id, and remote_id are deployment fields on the agent record (not on AgentVersionDTO config). Changing them affects new runs only. When harness is not inference, the agent is backed by an external harness — platform instructions, tools, and version history are not used for execution. Harness agents are created on first harness launch per profile; you cannot add new versions to them.

On AgentRunDTO, profile_id and remote_id are a snapshot taken when the run is created. Updating the agent later does not rewrite past runs; the turn driver is chosen from the run record.

Remote harness runs

When active_run.profile_id is set, the API drives a harness session on that profile's remote instead of calling a hosted model in the platform loop. The chat wire format, streaming endpoints, approval interrupts, and queue behavior match a normal agent run — only execution moves to your machine.

PhaseBehavior
StartOpens or resumes the chat's harness session on the remote in work_dir (harness_session_id when resuming), then sends the user prompt. A new session in a chat that already has messages receives prior conversation text first so the harness continues rather than starts over.
StreamingHarness output becomes assistant messages and broadcasts on the chat stream. Tool calls the harness makes on the host appear as tool invocations and tool messages with type: harness — they are recorded for the transcript and are not executed again on inference.sh.
ApprovalsHarness permission requests surface as the standard approval interrupt (input_required). Allow or deny through the same REST/SDK paths as platform tools; the decision is forwarded to the harness once. If the harness withdraws a request before you answer (for example you interrupted the turn, or the harness moved on), it sends approval.resolved while the invocation is still awaiting_approval. The API cancels the tool call, resolves the interrupt, and the run is no longer blocked. Your own allow/deny that arrives after the invocation has already left awaiting_approval is ignored — the harness already handled it.
AttachmentsImages and files on the message go with the prompt. The daemon downloads them to the machine and the harness gets images as images where it supports them (capabilities.images), other files by local path. A remote whose daemon predates belt v1.21.0 (no capabilities.files) gets the files' URLs in the prompt text. See Remotes guide, Attachments.
Mid-turn messagesDepends on the profile's capabilities.steer from registration. When steer is true, follow-up user messages during a working turn are sent to the harness immediately and appear as ready on the active run. When steer is false, messages are queued for the next turn (same as queuing while a run is active).
StopStop chat sends an interrupt to the harness turn in flight and keeps the session for later messages.
EndWhen the harness finishes the turn, the run completes and queued messages drain into the next turn. Session errors fail the run.

Chat rules on harness runs. The chat's permission rules (including rules added with Always allow tool) apply to harness permission prompts the same way as to platform loop tools. If the harness asks to run a tool already covered by a chat allow rule for that harness tool, the run does not enter input_required: the invocation is recorded as in progress and the API answers the harness with remote_session_resolve (decision: allow, scope: session) so it can stop re-prompting for that tool name during the current harness session. Other tools still park for approval. When agent_data.allow_all_tools is true (see Chat settings), every harness permission prompt is auto-approved the same way as platform tool approvals — turn it off again from chat settings when you want prompts back.

When you resolve a parked harness request, the API forwards your decision on the remote WebSocket:

Actionremote_session_resolve.scope
POST /tools/:id/invoke (allow once)once
POST /tools/:id/rejectonce
POST /chats/:chatId/tools/:toolInvocationId/always-allowsession

See Remotes API — remote_session_resolve.

Start failures (the run does not begin; error text is returned to the client):

ConditionTypical message
Remote not connectedthe remote this agent runs on is not connected
Daemon without --agentsthis remote does not run agent sessions; start it with belt remote start --agents
Profile missing, logged out, or not drivablethe harness this agent runs on is not available on its remote

Remote disconnect. When a remote goes disconnected or stopped, every non-terminal harness run on that machine fails with the remote disconnected. Those turns are not retried automatically — the harness may already have changed files locally. Exec runs on the same remote fail the same way (remote disconnected before the command finished).

Setup: Remotes guide — Agent harness sessions, Profile-backed agent chats, and Launch harness chat.


List chat messages

GET /chats/:id/messages or POST /chats/:id/messages/list

Returns cursor-paginated messages for a chat. Requires the conversations:read scope (same as Get chat). The chat must exist and belong to your workspace.

Returns every message in the chat. Access is verified when loading the chat ID. Message rows are not filtered by creator or workspace.

Default sort is order descending (newest first) — load older pages as users scroll up. POST /chats/:id/messages/list accepts the same fields in a JSON body (cursor, limit, direction, sort, filters).

Query / body fieldTypeDescription
limitintegerPage size (default 50). Pass -1 to return up to 10,000 messages in one response — see Cursor pagination.
cursorstringOpaque cursor from next_cursor (ignored when limit=-1)
directionstringforward or backward (ignored when limit=-1)
sortarraySort orders (default [{ "field": "order", "dir": "desc" }])
filtersarrayFilter expressions (same pattern as other list endpoints)

Uses the same cursor pagination shape as List skills. Each items entry is a ChatMessageDTO with role, status, content, order, and optional tool_invocations. Transcripts can include role: "event" rows when lifecycle hooks run — see Chat message roles.

Message order: Integer sequence within a chat (unique per chat_id, allocated by the server on insert). Sort transcripts by order, not created_at — messages written in the same turn can share a timestamp. While status is queued, order is provisional; when the message becomes ready on the agent's next turn, the server may assign a new order so it follows any tool results that completed while it was queued. See Chat message status.

Example (newest page):

bash
1curl "https://api.inference.sh/chats/chat_abc123/messages?limit=50" \2  -H "Authorization: Bearer inf_your_key"

Example response:

json
1{2  "items": [3    {4      "id": "msg_xyz789",5      "chat_id": "chat_abc123",6      "role": "assistant",7      "status": "ready",8      "order": 4,9      "content": [{ "type": "text", "text": "Here is the summary." }],10      "tool_invocations": []11    }12  ],13  "next_cursor": "msg_abc456",14  "has_next": true,15  "has_previous": false,16  "items_per_page": 50,17  "total_items": 1218}

Pass cursor from next_cursor to fetch older messages.

Example (full transcript — no pagination):

bash
1curl "https://api.inference.sh/chats/chat_abc123/messages?limit=-1" \2  -H "Authorization: Bearer inf_your_key"

When limit=-1, has_next is false and up to 10,000 matching messages are returned in items (sorted and filtered as requested). Prefer cursor pagination for UI scroll-back; use limit=-1 for exports or debugging on chats with fewer than 10,000 messages.

JavaScript SDK (@inferencesh/sdk agent module, v0.6.40+): AgentChatProvider loads one page of history using the server default limit (50) when GET /chats/:id omits chat_messages. v0.6.41+: useAgentActions().loadOlderMessages() fetches the next page via cursor and prepends deduplicated messages to useAgentChat().messages; hasOlderMessages is true when another page exists. For custom UIs outside AgentChatProvider, call this endpoint directly with cursor or limit=-1.

For live updates while the agent runs, use Stream chat updates instead of polling this endpoint.


Get chat trace

GET /chats/:id/trace

Returns the execution graph for an agent chat: nodes (assistant turns, tool invocations, sync barriers, and long-running work such as app tasks and remote_exec exec runs), edges (dependencies), and step counts by status. Requires the conversations:read scope (same as other chat endpoints).

Long-running tools add execution nodes linked to their backing resource (for example resource_type task for app tools, exec_run for remote commands). The tool invocation node stays in progress until that work finishes.

Use this to debug agent runs programmatically: see which tools ran, how long each step took, and where failures occurred. In the workspace, open chat settings → agent → Show trace for the same execution graph (Chatting guide).

Response

FieldTypeDescription
graph_idstringGraph identifier (same as the chat ID)
nodesarrayExecution steps (type, label, status, resource_id, started_at, completed_at, duration_ms, …)
edgesarrayDependencies between nodes (from_node, to_node, type)
total_stepsnumberTotal nodes in the graph
completed_stepsnumberSteps that finished successfully
running_stepsnumberSteps still in progress
failed_stepsnumberSteps that failed

Example:

bash
1curl https://api.inference.sh/chats/chat_abc123/trace \2  -H "Authorization: Bearer inf_your_key"

JavaScript SDK: client.chats.getTrace('chat_abc123').

→ Observability · Stream chat updates


Chat status values

StatusMeaning
busyAgent is working — LLM generation, or automated tool execution (apps, sub-agents, hooks, client tools, MCP)
idleAgent finished; chat can accept new messages
awaiting_inputA human must act on this chat: HIL tool approval or an interactive widget
completedTerminal state (archived chat)

Client tools and hooks keep the chat in busy while they execute. Use chat.status === "awaiting_input" when you need a human on the conversation; use tool invocation status for per-tool progress.


Chat message roles

Each message has a role that controls how the platform stores it and whether it is sent to the LLM on later turns.

RoleSent to LLMTypical use
userYesEnd-user input
assistantYesModel output and tool calls
toolYesTool results
systemYesSystem instructions
eventNoSystem info for the transcript (for example a lifecycle hook handler run)
injectionFolded inInternal context; folded into the user turn when context is built, not sent as its own message
compactionReplacedCompaction marker; replaced by its summary when context is built

Messages with injection, compaction or event roles still appear in List chat messages and on the chat stream with status: "ready". Custom UIs can render them; integrations that rebuild LLM context from the transcript must skip event rows and treat injection and compaction as described above. Clients can only create user messages; every other role is written by the platform.

Event content: Event messages use a content block with type: "event". The block includes short text plus structured event data (today event.type is "hook" with a hook object). See Lifecycle hooks — Hook runs in the transcript.

SDK types: ChatMessageRoleEvent, ChatEvent, and ChatHookEvent in @inferencesh/sdk / generated Python types.


Chat message status

Each entry in chat_messages has its own status, separate from the chat-level status above.

StatusMeaning
pendingAssistant message still being generated (LLM stream in progress)
queuedUser message queued while the agent is still running — via Send chat message or auto-queue on run/message — waiting for the next turn
readyComplete — included in LLM context on subsequent turns
failedGeneration failed, or the run could not start
cancelledCancelled — via Cancel message or when the chat run is stopped

Messages from POST /agents/run or POST /agents/messages start as status: ready when no run is active. When a run is already active, they are auto-queued with status: queued instead. When the run cannot start (for example its agent version is gone or you cannot read it), the message is marked failed and the request returns the error. Messages sent via POST /chats/:id/messages (or auto-queue) transition to ready when the agent begins its next turn, before context is built.

Poll List chat messages or stream chat updates to detect queued messages — the stream emits chat_messages events as status transitions from queued to ready, often including an updated order in the same partial update. To withdraw a queued message before the agent consumes it, use Cancel message.

While a message is still queued, sorting the transcript by order alone can place it between an in-flight tool call and its result; that position is corrected when the message is readied.

SDK types: Import ChatMessageStatusQueued from @inferencesh/sdk (JavaScript) or ChatMessageStatus.QUEUED from inferencesh.types (Python) when comparing message status values. JavaScript SDK (v0.6.36+): client.chats.cancelMessage(messageId) and useAgentActions().cancelMessage(messageId) with AgentChatProvider cancel queued messages. Python SDK does not expose cancelMessage yet.


Delete chat

DELETE /chats/:id

Permanently deletes a chat, its messages, and tool history. Requires conversations:write scope. The workspace delete chat action in chat settings calls this endpoint.

JavaScript SDK: client.chats.delete('chat_abc123').


Stop chat

POST /chats/:id/stop

Stop the current agent run and cancel pending tool invocations.

JavaScript SDK: client.chats.stop('chat_abc123') — or agent.stopChat() when using client.agent().


Turn budget

Each chat has a turn budget that caps one uninterrupted stretch of agent work. The runtime counts the messages added since the last user message, ordered by message order. When the count reaches 500, the run ends without dispatching further tool calls.

BehaviorDetail
What countsAssistant and tool messages since the last user message
Limit500 consecutive non-user messages per chat
On exceedPending tool calls are dropped. The run ends the same way as when the model stops calling tools
User messageA new user message resets the counter for that chat
ScopePer chat. Budget does not accumulate across user turns

If the model's last response has no text, the runtime sets it to a message saying that the task was not finished and asking the user how to continue. Sub-agent and agent tool chats are separate chats with their own budgets.

The budget stops tool-calling loops that never converge from running and consuming credits indefinitely. Use Stop chat to end a run earlier.


Context auto-compaction

Long chats can exceed the agent core app's context size (the model window configured on the agent version). Before each LLM turn, the platform estimates how many tokens the next prompt would send — system prompt, tool definitions, any prior compaction summary, and live transcript messages (including reasoning blocks, tool calls, tool results, and file or image references). When that estimate passes 80% of the context size, the runtime compacts the oldest live turns so the newest 25% of live content stays in full.

BehaviorDetail
TriggerEstimated prompt tokens > 80% of the agent's context size
What staysThe newest live messages that fit in ~25% of the context budget
What is summarizedOlder live turns, plus the text of any earlier compaction marker when compacting again
Cut boundaryNever splits a tool call from its tool result — both stay or both retire
When it runsAt the start of a turn, after queued user messages are readied and before the model call
Agent eventcontext.compacted on the agent run event stream (before_tokens, after_tokens in the payload)

Compaction inserts a chat message with role: "compaction" and an extractive summary (user requests and assistant decisions; large tool outputs are dropped). When the agent version has a core app, the platform may call that app with a structured summarization prompt instead; on failure it falls back to extractive text. The marker's compaction_meta records compacted_up_to_order, compacted_count, estimated_tokens, and method (extractive or llm).

List chat messages returns compaction markers in transcript order. BuildContext replaces each marker with its summary text when calling the model — markers are bookkeeping, not an extra user turn.

Lifecycle hooks: agent.pre_compact runs immediately before compaction; a synchronous webhook that returns deny or stop skips compaction for that turn. agent.post_compact fires after a successful compaction. Per-chat disable_hooks turns both off for that chat.

→ Long conversations in the workspace


Set agent

POST /chats/:id/agent

Change which template agent handles an existing chat. Message history and per-chat context are preserved; the next run uses the new agent's tools and system prompt.

Agent switching is explicit — call this endpoint to change agents. Sending a message via POST /agents/run, POST /agents/message, or POST /chats/:id/messages does not change which agent owns the chat, and there is no per-message agent binding.

Requires conversations:write scope. In the JavaScript SDK this is useAgentActions().switchAgent(ref). Only agents the platform loop runs can take over a chat; a chat on a remote harness keeps its agent (409).

Request

FieldTypeRequiredDescription
agentstringYesTemplate agent ref (namespace/name or namespace/name@version)

Response

Returns what the switch changed, not the chat:

FieldTypeDescription
chat_idstringThe chat
agent_idstringThe agent the chat now runs on
agentobjectThat agent
agent_version_idstringThe agent version the next run uses
agent_versionobjectThat version

Merge it into the chat you hold. Messages and the rest of the chat are unchanged.

Errors

HTTP statusCodeWhen
400invalid_requestMissing or empty agent
404not_foundChat or agent not found
409conflictThe chat or the agent runs on a remote harness, or the chat is a review branch

Example

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/agent \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"agent": "myteam/escalation-agent@latest"}'

Chat settings

POST /chats/:id/settings

Change a chat's settings. A field left out is left as it is. Requires conversations:write scope. In the JavaScript SDK this is useAgentActions().updateChatSettings(settings).

Request

FieldTypeDescription
namestringRename the chat. Cannot be empty.
visibilitystringprivate, team or public
allow_all_toolsbooleanRun every tool call without asking. Switching it on also approves the calls already waiting.
disable_hooksbooleanStop the agent's lifecycle hooks firing in this chat
forget_memorystring[]Remove these keys from the chat's memory. A key that is not there is ignored.

Response

Returns the chat's settings after the change, not the chat:

FieldTypeDescription
chat_idstringThe chat
namestringChat name
visibilitystringprivate, team or public
allow_all_toolsbooleanagent_data.allow_all_tools on the chat
disable_hooksbooleanagent_data.disable_hooks on the chat
memoryobjectagent_data.memory on the chat, after forget_memory

Merge it into the chat you hold. Calls approved by switching allow_all_tools on arrive on the chat stream.

Example

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/settings \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"name": "release notes", "forget_memory": ["draft"]}'
json
1{2  "chat_id": "chat_abc123",3  "name": "release notes",4  "visibility": "team",5  "allow_all_tools": false,6  "disable_hooks": false,7  "memory": { "goal": "ship the release" }8}

Send chat message

POST /chats/:id/messages

Send a user message to an existing chat. If the agent is idle, this starts a new run. If a run is already active (status: busy), the message is queued and picked up on the agent's next turn — the same auto-queue behavior as POST /agents/run with an existing chat_id.

Use this endpoint when you only need to send a message without other run parameters (for example a follow-up while tools are executing, or continuing a chat without specifying agent config again).

Legacy alias: POST /chats/:id/steer — same request and response. Prefer POST /chats/:id/messages.

Requires conversations:write scope and write access to the chat. Someone who can only read the chat (for example through a read share or a public link) gets 403; posting to a chat you cannot read returns 404. JavaScript SDK: used internally by AgentChatProvider / useAgentActions().sendMessage() (client.agent().sendMessage() still uses POST /agents/run). Python SDK: call the REST endpoint directly.

Request

json
1{2  "message": "Also prioritize tickets from enterprise customers",3  "attachments": [4    { "uri": "https://cloud.inference.sh/t/abc123/tickets.csv", "filename": "tickets.csv", "content_type": "text/csv" }5  ]6}
FieldTypeRequiredDescription
messagestringNoUser message text
attachmentsarrayNoFile refs from the Files API, same shape as input.attachments on Run agent

Send message, attachments, or both. A message may be attachments alone.

Response

Returns the created message DTO with role: "user". When a run is active, status is "queued" and agent_run_id is set; when idle, a new run starts and the message is "ready". Queued messages transition to ready when the agent begins its next turn, before context is built. See Chat message status.

Errors

HTTP statusCodeWhen
400invalid_requestNeither message nor attachments is set
404not_foundChat does not exist
409conflictHarness-backed chat and the remote profile's harness is below the driver's minimum (profiles[].support.level is older-than-supported). The message explains what is missing and how to upgrade on the host. Nothing is sent to the machine.

Example

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/messages \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"message": "Focus on P0 tickets only"}'

Cancel message

POST /chats/messages/:messageId/cancel

Cancel a queued user message before the agent picks it up — for example a message from Send chat message or an auto-queued message you want to withdraw. The message status becomes cancelled and content is cleared.

Only messages with status: "queued" can be cancelled. Messages that are ready, failed, or already cancelled return 409 not_cancellable.

Requires conversations:write scope. JavaScript SDK: client.chats.cancelMessage('msg_abc123') — or useAgentActions().cancelMessage(messageId) with AgentChatProvider. Python SDK: call the REST endpoint directly.

Response

Returns the updated message DTO with status: "cancelled". The chat stream emits a chat_messages event when the cancellation succeeds.

Errors

HTTP statusCodeWhen
400missing_idmessageId path parameter is missing
404not_foundMessage does not exist
409not_cancellableMessage is not queued

Example

bash
1curl -X POST https://api.inference.sh/chats/messages/msg_abc123/cancel \2  -H "Authorization: Bearer inf_your_key"

Tool invocations

Tool calls on assistant messages expose tool_invocations. Each invocation has an id used by the endpoints below.

Tool invocation status

StatusMeaning
pendingQueued
in_progressExecuting — apps, sub-agents, client tools, hooks, MCP
awaiting_inputInteractive widget or form needs human input
awaiting_approvalHIL approval needed before execution
completedDone
failedError
cancelledCancelled

Status transitions

The API enforces valid transitions server-side. Terminal states (completed, failed, cancelled) cannot change again. A typical successful path is pending → in_progress → completed, with optional stops at awaiting_approval or awaiting_input. Skipping required steps is rejected — for example pending cannot go directly to awaiting_input.

If a chat run is cancelled while a tool is executing, the invocation moves to failed and any late completion attempt from the tool is rejected.

Tool invocation updates are delivered on the chat stream as chat_messages events (the server republishes the parent message when an invocation changes).


Submit tool result

POST /tools/:toolInvocationId

Submit a result for client tools (in_progress) or interactive widgets (awaiting_input).

Request

json
1{2  "result": "{\"temperature\": 72}"3}
FieldTypeRequiredDescription
resultstringYesThe tool's output, as text
attachmentsarrayNoFiles the tool returns: file refs uploaded first with POST /files (uri, filename, content_type, optional id and size). Each needs a uri; at most 20

The tool message keeps the text and the files. Images reach the model as image input when the model accepts images.

json
1{2  "result": "{\"url\": \"https://example.com\", \"note\": \"Screenshot attached\"}",3  "attachments": [4    {5      "id": "file_abc123",6      "uri": "https://cloud.inference.sh/t/abc123/screenshot.png",7      "filename": "screenshot.png",8      "content_type": "image/png"9    }10  ]11}

A request with more than 20 attachments, or an attachment without a uri, returns 400 validation_error.

For widget actions, JSON-serialize { "action": {...}, "form_data": {...} } into result, or use the deprecated POST /tools/:id/widget endpoint.

The JavaScript and Python SDKs call this via submitToolResult / submit_tool_result. See Client Tools.


Approve tool (HIL)

POST /tools/:toolInvocationId/invoke

Approve a tool with status: awaiting_approval and execute it. Returns 400 invalid_state if the invocation is not awaiting approval. Approving, rejecting, always-allowing, and submitting a tool result all require write access to the call's chat; being able to read the chat is not enough.

bash
1curl -X POST https://api.inference.sh/tools/ti_abc123/invoke \2  -H "Authorization: Bearer inf_your_key"

→ Approval settings · Human-in-the-loop


Reject tool (HIL)

POST /tools/:toolInvocationId/reject

Reject a pending approval. The agent continues with the rejection reason.

Request

json
1{2  "reason": "Not authorized to delete files"3}

reason is optional.


Always allow tool

When a tool call is awaiting_approval, always allow approves it once and saves chat allow rules so matching calls in this chat skip the interrupt (platform loop and remote harness runs). List and revoke rules with Chat permissions rules.

On a harness-backed chat, always allow also tells the harness it may stop asking for that tool for the remainder of the harness session (remote_session_resolve with scope: session). A one-time allow via POST /tools/:id/invoke sends scope: once instead.

Requires the conversations:read scope for options and conversations:write to save.

List always-allow options

GET /chats/:chatId/tools/:toolInvocationId/always-allow/options

Returns the choices the API suggests for this call, narrowest first (for example an exact remote_exec command, a command prefix, every command on the remote, or the whole tool). The client does not build rules — each option includes the PolicyRuleDTO rows it would save, a label, optional description (when the engine cannot narrow further), and a stable key for POST .../always-allow.

Response (AlwaysAllowOptionsDTO):

FieldDescription
optionsAlwaysAllowOptionDTO[] with key, scope (exact, prefix, folder, remote, tool), label, optional description, and rules
defaultKey of the narrowest option, or empty when there is none
unavailableWhen options is empty, a sentence explaining why (for example governance allows only admin rules, or no chat rule could allow this call)

For HTTP tools, the engine resolves the request URL to a host. When approval waits on WebFetch(domain:…) (governance ask or default ask on the host), chat rules cannot answer that question — options is empty, unavailable names the host, and POST .../always-allow without option does not write a whole-tool allow. When only the tool's built-in approval asked, the whole-tool option is returned as for other tools.

bash
1curl https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/always-allow/options \2  -H "Authorization: Bearer inf_your_key"

Save always-allow

POST /chats/:chatId/tools/:toolInvocationId/always-allow

Body (AlwaysAllowRequest):

FieldDescription
optionOption key from the options endpoint. If the key no longer matches a current option (the person waited too long or the call changed), the API returns 409 conflict.
tool_nameAccepted for older clients and ignored — the invocation names its own tool.

Omit option to keep the pre-options default: for remote_exec, save allow rules for each piece of the command exactly; for other tools, save a whole-tool allow rule.

Response (AlwaysAllowResultDTO): option (when one was chosen) and rules (the chat rules now in force for it).

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/always-allow \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"option":"exact:RemoteExec:git status"}'

Errors (stale approval): If the invocation is no longer awaiting_approval (already answered, rejected, or timed out), GET .../always-allow/options, POST .../always-allow, and Explain tool call return 400 invalid_request with message Tool is not awaiting approval. POST /tools/:id/invoke and POST /tools/:id/reject still use 400 invalid_state for the same condition.


Explain tool call

POST /chats/:chatId/tools/:toolInvocationId/explain

Plain-language summary of a call that is awaiting_approval (similar to Claude Code's permission explainer): what it does, why the agent appears to be making it, and how risky it is. Requires conversations:write.

The model is the chat agent's core app (the same model family the loop runs on). Harness-backed chats have no platform model for the harness itself, so explain is not available there.

The answer is generated once per invocation and stored; asking again returns the cached result and does not bill twice. Concurrent requests share one generation. The call runs as the person asking, under their usage policy and team billing.

Response (ToolExplanationDTO):

FieldDescription
risk_levellow, medium, or high
explanationWhat the call does, in one or two sentences
reasoningWhy the agent appears to be making it
riskWhat could go wrong, in a few words

Arguments are redacted before they reach the model; chat history is not included.

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/tools/ti_abc123/explain \2  -H "Authorization: Bearer inf_your_key"

Agent and remote permission rules

Agents and connected remotes can carry their own allow, ask, and deny rules. Edit them in the web app (Permissions on the agent editor; Rules on the remote detail page) or with the REST routes below. Requires agents:read / agents:write for agents and remotes:read / remotes:write for remotes. Writing an agent's rules needs write access to the agent; writing a remote's rules is for the remote's owner only (a teammate who can see the remote gets 403).

SubjectListAddUpdate enforcementRevoke
AgentGET /agents/{id}/rulesPOST /agents/{id}/rulesPUT /agents/{id}/rules/{ruleId}DELETE /agents/{id}/rules/{ruleId}
RemoteGET /remotes/{id}/rulesPOST /remotes/{id}/rulesPUT /remotes/{id}/rules/{ruleId}DELETE /remotes/{id}/rules/{ruleId}
User (your own)GET /me/rulesPOST /me/rulesPUT /me/rules/{ruleId}DELETE /me/rules/{ruleId}

Response (PolicyLayerDTO): rules[] on that subject plus rule_kinds[] describing which kind values you may target (same shapes as chat rules). Agent rules may use RemoteExec, Workspace, Harness, or Tool; remote rules RemoteExec, Workspace, or Harness.

Scope:

  • Agent rules apply in every chat that runs the agent. An agent allow rule works only in the agent's own workspace; elsewhere that layer may ask or deny but not allow.
  • Remote rules apply to every remote_exec command and harness call on that machine (selector defaults to the remote id when omitted on remote-owned rules).

Add and revoke behave like chat rules: only narrowing allows are stored; duplicate allows are ignored; invalid allows return 400. POST /agents/{id}/rules/explain and POST /remotes/{id}/rules/explain test a call against that subject's rules together with yours, with the same body as Explain a call (there is no explain route on /me/rules). The rules API also has POST …/rules/preview and GET …/rules/recent on chats, agents, and remotes, and GET /rules/suggestions; they are not described here.

bash
1curl https://api.inference.sh/agents/agent_abc/rules \2  -H "Authorization: Bearer inf_your_key"

The agent's allow_all_tools template flag (see Chat settings) is separate from this layer: it is saved on the agent version and copied into new chats in the agent's own workspace when they start.


Chat permissions rules

Stacked allow, ask, and deny rules decide whether a call runs, pauses for approval, or is refused. Layers are evaluated most specific first: chat (editable in this conversation), then agent and user, then the governance layer in force (team or org rules written by admins). Chat rules can only narrow what layers above allow — they cannot loosen a governance ask or deny, and when governance is the only layer that may allow, chat rules may ask or deny but not allow.

The workspace Permissions pane in chat settings shows the same layers as GET /chats/:id/rules.

Requires write access to the chat (same check as Chat settings).

List rules

GET /chats/:id/rules

Requires the conversations:read scope.

Response (ChatRulesDTO): layers[] ordered most specific first. Each layer has subject_type (chat, agent, remote, user, team, org), subject_id, label, governance, editable (only the chat layer), and rules[].

Each rule (PolicyRuleDTO) has:

FieldDescription
idRule id
effectallow, ask, or deny
enforcementHow the rule participates: default (decides in its layer unless a more specific admin layer overrides), enforced (admin-only — final governance rule), evaluate (never decides; the decision feed records what it would have done), or disabled (stored but ignored). Omitted on older rows means default.
kindRemoteExec, Workspace, Harness, or Tool (chat-editable kinds in this release)
selectorOptional remote id, or tag:<name> to match remotes by tag
specifierKind-specific pattern (see below). Empty means the whole kind (for example every harness tool).
labelThe rule in words (for example git commit commands on Laptop), the same wording always-allow options use
created_atSet for stored rules; null for synthesized rows such as legacy always_allowed_tools entries or the allow all tools setting
created_byUser id for stored rules; empty for synthesized rows
bash
1curl https://api.inference.sh/chats/chat_abc123/rules \2  -H "Authorization: Bearer inf_your_key"

Add a chat rule

POST /chats/:id/rules

Requires the conversations:write scope. Body (ChatRuleCreateRequest):

FieldRequiredDescription
effectYesallow, ask, or deny
kindYesRemoteExec, Workspace, Harness, or Tool
selectorNoRemote id or tag:<name>
specifierYesPattern for the kind (may be empty only when the kind allows a whole-kind rule)
enforcementNodefault, evaluate, or disabled on chat rules. enforced is reserved for admin governance layers (usage policy).

Specifier examples (typed form Kind[selector](specifier)):

KindExample specifierMatches
RemoteExecnpm test:*Commands starting with npm test on the selected remote(s)
RemoteExecrm:*Commands starting with rm
Workspace~/inference/**Paths under that folder on the remote
ToolsearchOne platform loop tool by name
HarnessBash(git status:*)Harness Bash calls matching git status
HarnessReadThe harness Read tool (any target)

Compound commands. A remote_exec line is split into pieces at &&, ||, ;, newlines, and |. Every piece must be allowed for the line to run; a deny or ask on any piece wins. Prefix rules such as git clone:* apply per piece. Commands the policy engine cannot fully analyse ask for approval. Workspace rules also apply to the paths a command touches; see Remotes — security model for how ~ and symlinks are resolved.

Returns the created PolicyRuleDTO. Duplicate allows for the same target are ignored (idempotent).

Validation (400): Chat allow rules that could not take effect are rejected — for example an allow while governance is the only layer that may allow, or an allow for a target a higher layer already asks about or denies.

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/rules \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"effect":"allow","kind":"Tool","specifier":"search"}'

Update a rule's enforcement

PUT /chats/:id/rules/:ruleId
PUT /agents/{id}/rules/{ruleId}
PUT /remotes/{id}/rules/{ruleId}
PUT /me/rules/{ruleId}

Requires the same write scope as adding a rule on that subject (conversations:write, agents:write, remotes:write, or user:write for /me/rules). Body (PolicyRuleUpdateRequest):

FieldRequiredDescription
enforcementYesdefault, enforced (governance layers only), evaluate, or disabled

Only enforcement changes in place. To change a rule's target or effect, delete it and add a new rule. Governance routes on teams, orgs, and members use the same PUT .../rules/{ruleId} shape — see Usage policy API.

Returns the updated PolicyRuleDTO.

bash
1curl -X PUT https://api.inference.sh/chats/chat_abc123/rules/rule_xyz \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"enforcement":"evaluate"}'

Revoke a chat rule

DELETE /chats/:id/rules/:ruleId

Requires the conversations:write scope. Only rules stored on the chat layer may be deleted (not synthesized legacy rows — remove those by changing chat settings or clearing the legacy list).

json
1{ "status": "ok" }

Explain a call

POST /chats/:id/rules/explain

Requires the conversations:read scope. Test how the stacked rules would treat one call without running it. Body (ChatRuleExplainRequest):

FieldDescription
kindRemoteExec, Workspace, Harness, or Tool
targetThe call: shell command (RemoteExec), path (Workspace), harness tool call such as Bash(git status) or Read (Harness), or loop tool name (Tool)
remoteOptional remote id when testing selector-scoped rules
cwdOptional working directory for relative paths

Response (ChatRuleExplainDTO): effect, the rung and rule that decided (both null when no single rule decided: the kind's default, a command the engine cannot analyse, or every piece of a compound command allowed), a one-line reason, and would_have when rules set to evaluate would have decided otherwise.

bash
1curl -X POST https://api.inference.sh/chats/chat_abc123/rules/explain \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"kind":"RemoteExec","target":"rm -rf /","remote":"r_laptop"}'

Defaults (no matching rule)

CallDefault when nothing matches
Platform loop tool with require_approvalAsk (approval interrupt)
Platform loop tool without require_approvalAllow
remote_execAsk — commands do not run until approved unless a chat or governance allow rule matches
Harness permission promptAsk, unless a matching allow rule (or allow all tools) auto-answers

→ Approval settings · Human-in-the-loop workflow


Human-in-the-loop workflow

  1. Run an agent with tools marked require_approval (see Approval settings).
  2. Poll or stream the chat until status is awaiting_input.
  3. In List chat messages (or the chat stream), find invocations with status: awaiting_approval.
  4. Call POST /tools/:id/invoke to approve or POST /tools/:id/reject to reject.
  5. Poll or stream again until the agent reaches idle or completed.

List chats with status in busy or awaiting_input to find conversations needing action. The belt CLI wraps this flow with belt chat pending, belt chat approve, and belt chat reject — see CLI Setup.


Interrupt gates

When an agent run needs external input — tool approval, widget input, missing credentials, or a lifecycle hook gate — the platform creates an interrupt record. Interrupts provide a unified gate model alongside the tool-invocation endpoints above.

→ Lifecycle hooks — gate handlers for hook-driven interrupts

GET /chats/:id includes active_run when a run is in progress. When the run is interrupted, active_run.state is input_required or auth_required and active_run may include:

FieldDescription
interrupt_reasonWhy the run paused (see Interrupt reasons)
interrupt_tool_idRelated tool invocation ID (when applicable)
interrupt_metaAdditional context (JSON)

List pending interrupts for a run

GET /agent-runs/:runId/interrupts

Returns pending interrupts for the given agent run. Requires the conversations:read scope.

Example:

bash
1curl https://api.inference.sh/agent-runs/ar_abc123/interrupts \2  -H "Authorization: Bearer inf_your_key"

JavaScript SDK: client.agents.listRunInterrupts('ar_abc123')

CLI: belt interrupt list ar_abc123 — see CLI setup — Interrupt.

Python SDK: use the REST endpoint with typed responses — import InterruptDTO from inferencesh.types (see Python SDK — Interrupt gates). Client helpers are not yet exposed in Python.

Resolve interrupt

POST /interrupts/:id/resolve

Resolve a pending interrupt with allow or deny. Requires the conversations:write scope and write access to the interrupt's chat; read access (for example a read share) is not enough. When all pending interrupts for the run are resolved, the agent run resumes automatically.

Request

json
1{2  "decision": "allow",3  "data": { "reviewer": "security-bot" }4}

decision must be "allow" or "deny". Optional data (any JSON object) is stored on the interrupt as resolved_data and returned in the response — useful for audit metadata on hook gate resolutions.

Example:

bash
1curl -X POST https://api.inference.sh/interrupts/int_abc123/resolve \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"decision": "allow"}'

JavaScript SDK: client.agents.resolveInterrupt('int_abc123', 'allow') — or agent.resolveInterrupt() when using client.agent().

CLI: belt interrupt resolve int_abc123 allow (or deny).

Python SDK: POST /interrupts/:id/resolve with {"decision": "allow"} or "deny" — type the response as InterruptDTO from inferencesh.types.

For tool approval interrupts (reason: tool_approval), resolving with allow is equivalent to POST /tools/:id/invoke; deny is equivalent to POST /tools/:id/reject.

To answer an interrupt from an external system, call POST /interrupts/:id/resolve with credentials that can write to the chat. A resolve_interrupt trigger answers only with the interrupt's default resolution on its schedule (this is how gate expiry works); see Triggers API — resolve_interrupt action.

InterruptDTO fields

FieldTypeDescription
idstringInterrupt ID
run_idstringAgent run ID
chat_idstringChat ID
reasonstringWhy the run paused
sourcestringGate origin (runtime component)
resource_idstringRelated resource ID (for example a tool invocation)
resource_typestringtool_invocation (tool approval gates) or hook_event (lifecycle hook gates)
statusstringpending, resolved, expired, or cancelled
resolutionstringallow or deny (set when resolved)
resolved_dataobjectOptional JSON from the resolve request data field
expires_atstringRFC3339 expiry (if set)
metaobjectAdditional context (JSON). For hook_gate, contains the hook payload.

Interrupt reasons

ReasonWhen
tool_approvalTool requires human approval before execution
client_toolClient-side tool must run in your app or browser
widgetInteractive widget needs user input
authMissing credentials
confirmationAgent is waiting for explicit user confirmation
hook_gateLifecycle hook gate awaiting an external decision

→ Approval settings · Lifecycle hooks


List internal tool definitions

GET /agents/internal-tools

Returns the registry of built-in internal tool categories (plan, memory, widget, finish, skills, artifacts, agent, remotes, knowledge, meta). Use it to build agent configuration UIs. Each item includes the default the runtime applies when that category's flag is unset.

Requires the agents:read scope.

Response:

json
1[2  {3    "id": "plan",4    "name": "Plan",5    "description": "Task planning tools for creating, updating, and tracking multi-step plans. Helps the agent organize complex tasks into manageable steps.",6    "tools": ["plan_create", "plan_update", "plan_load"],7    "scope": "all",8    "default_enabled": true9  },10  {11    "id": "agent",12    "name": "Agent",13    "description": "Run a copy of this agent on a self-contained side task and get its answer back, ...",14    "tools": ["agent"],15    "scope": "all",16    "default_enabled": false17  }18]
FieldTypeDescription
idstringCategory key in internal_tools (for example plan, meta, knowledge)
namestringDisplay name
descriptionstringWhat the category does
toolsstring[]Tool names in this category
scopestringall, top_level, or sub_agent
default_enabledbooleanWhat the runtime applies when the agent's flag for this category is unset. false for the opt-in categories (meta, artifact, agent, remote, knowledge), true for the rest

→ Internal Tools · JavaScript SDK: list internal tool definitions


List hook events

GET /agents/hook-events

Returns the canonical list of lifecycle hook events with metadata. Requires the agents:read scope.

Response:

json
1[2  {3    "event": "agent.tool_call",4    "description": "Fires before a tool is executed",5    "can_gate": true6  },7  {8    "event": "agent.tool_result",9    "description": "Fires after a tool completes",10    "can_gate": false11  }12]

Use can_gate to choose between a synchronous webhook (decision: deny) and a gate handler (type: "gate") that creates an interrupt. See Lifecycle hooks.


cURL example

bash
1# Start or continue a chat2curl -X POST https://api.inference.sh/agents/run \3  -H "Authorization: Bearer inf_your_key" \4  -H "Content-Type: application/json" \5  -d '{6    "agent": "myteam/support-agent@latest",7    "input": { "text": "Hello!" }8  }'910# Get chat (includes tool invocations)11curl https://api.inference.sh/chats/chat_abc123 \12  -H "Authorization: Bearer inf_your_key"1314# Approve a tool awaiting HIL15curl -X POST https://api.inference.sh/tools/ti_abc123/invoke \16  -H "Authorization: Bearer inf_your_key"

Messaging channel webhooks

Connect external chat platforms so users can talk to an agent where they already work. Inbound platform events create agent messages; agent replies are delivered back through the same channel.

Receive events

POST /agents/{id}/channel/{provider}

ParameterDescription
idAgent ID
providerIntegration provider slug (currently slack)

Legacy route (Slack only): POST /agents/{id}/slack — same behavior as provider=slack.

No API key is required. The platform verifies the request (for Slack, using your app's Signing Secret from the Slack integration). URL verification challenges (for example Slack url_verification) are answered automatically.

Example (Slack event subscription Request URL):

code
1https://api.inference.sh/agents/agent_abc123/channel/slack

On a valid inbound message, the API acknowledges immediately (200 with {"status":"ok"}) and processes the message asynchronously. Thread history is fetched when the channel supports it, so the agent has prior context.

Supported providers are registered per deployment; check integration guides for availability. See Chatting with Agents for the web app workflow.

Channel context

channel_context records where a conversation lives outside inference.sh so assistant replies can be delivered back through the same transport (Slack thread, Telegram chat, a wearable tag, …).

FieldTypeDescription
channel_typestringProvider slug — for example slack, telegram, teams, discord
channel_metadataobjectTransport-specific routing data (JSON object on the wire)

Webhook ingress. Inbound messaging channel webhooks set channel_context on the first user message automatically. The API persists it on the chat; later turns in that chat inherit routing without resending metadata.

Set by the platform only. channel_context comes only from messages that arrive through the platform's channel integrations. POST /agents/run, /agents/messages and /agents/message ignore a channel_context field in the body: the run goes ahead and the chat gets no channel.

For Slack, channel_metadata matches the fields the platform stores when events arrive through the channel webhook (channel, thread_ts, and optionally team_id, user_id, event_ts). Other providers use their own metadata shape; replies are dispatched only when a registered transport exists for channel_type.

GET /chats/:id returns channel_context on channel-originated chats. Chats started in the web app or through the API omit the field.

→ Slack event subscriptions


Get A2A agent card

GET /agents/{id}/card
GET /agents/{namespace}/{name}/card

Returns an Agent2Agent (A2A) protocol agent card for agent discovery and marketplace listings (for example GCP AI Agents Producer Portal). Requires agents:read.

The response is raw A2A JSON — not wrapped in the usual { "success": true, "data": … } envelope. Resolve the agent by ID or by namespace/name ref.

bash
1curl https://api.inference.sh/agents/myteam/support-agent/card \2  -H "Authorization: Bearer inf_your_key"

Example response:

json
1{2  "name": "myteam/support-agent",3  "description": "Customer support assistant",4  "version": "1.0.0",5  "url": "https://api.inference.sh/agents/myteam/support-agent/a2a",6  "documentationUrl": "https://app.inference.sh/agents/myteam/support-agent",7  "protocolVersion": "1.0",8  "preferredTransport": "JSONRPC",9  "provider": { "name": "inference shell", "url": "https://inference.sh" },10  "capabilities": { "streaming": true, "pushNotifications": false },11  "defaultInputModes": ["text/plain"],12  "defaultOutputModes": ["text/plain", "application/json", "application/a2ui+json"],13  "skills": [14    { "id": "search-tickets", "name": "Search tickets", "tags": ["app"] }15  ],16  "securitySchemes": [{ "scheme": "bearer" }]17}

url is the agent's A2A endpoint (see A2A protocol); documentationUrl is its page in the web app. Skills are derived from the agent's current version tools (apps, sub-agents, MCP servers, HTTP hooks) and attached knowledge skills. In the web app, open an agent and use a2a card on the detail header to copy this JSON to the clipboard.

JavaScript SDK (@inferencesh/sdk ≥ v0.6.12): client.agents.getA2ACard('agent_abc123') — returns the same raw card object. See JavaScript SDK — Agent SDK.


A2A protocol

Every agent speaks A2A under its own path. The agent is the resource; the protocol is a dialect of it:

code
1/agents/{id}/a2a2/agents/{namespace}/{name}/a2a
MethodPathScope
POST/agents/{ref}/a2aagents:execute — JSON-RPC 2.0 entry point (message/send, message/stream, tasks/get, tasks/cancel…)
POST/agents/{ref}/a2a/message:sendagents:execute
POST/agents/{ref}/a2a/message:streamagents:execute — SSE
GET/agents/{ref}/a2a/tasksconversations:read
GET/agents/{ref}/a2a/tasks/{runID}conversations:read
POST/agents/{ref}/a2a/tasks/{runID}:cancelconversations:write
GET/agents/{ref}/a2a/tasks/{runID}:subscribeconversations:read — SSE

Requests may carry an A2A-Version header; the response echoes the supported version. An A2A task maps to an agent run, not to a platform task.

bash
1curl https://api.inference.sh/agents/myteam/support-agent/a2a/message:send \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"message": {"messageId": "m1", "role": "user", "parts": [{"text": "hello"}]}}'

OpenAI-compatible dialect

The same idea for OpenAI clients: /agents/{namespace}/{name}/openai/chat/completions runs one turn of the agent over the Chat Completions wire format, with channel, continue, wait and max_chars for clients that only send their latest utterance. See OpenAI-Compatible API — Agents.


Create or update agent template

POST /agents

Creates a saved agent template, or adds a new version when an agent with the same name already exists in your workspace's namespace. Requires the agents:write scope.

This mirrors app deploy semantics: the namespace is assigned server-side from your workspace username, so redeploying only requires the agent name and a version payload. You do not need to look up the agent ID first, and the namespace field in the request is optional.

FieldTypeRequiredDescription
namestringYesAgent name (unique per workspace namespace)
namespacestringNoOptional. The server uses your workspace username
idstringNoAgent ID — include to create a new version of a known agent (alternative to name-based upsert)
versionobjectNoVersion config (description, system_prompt, core_app, tools, skills, context, internal_tools, permissions, hooks, output_schema, …). version.description is limited to 200 characters

When name matches an existing agent, the API creates a new version and sets it as current (same outcome as POST /agents/{id} with a version payload). Omitting visibility on redeploy preserves the existing visibility setting.

Agent version permissions

Set defaults on the version under permissions. They describe what new chats in the agent's home workspace may do without asking; each value is copied into the chat when it is created there (POST /chats, POST /agents/run without chat_id, and other chat-creation paths). Chats another workspace starts with a published agent do not copy these defaults. The chat owns copied settings from then on — the same allow_all_tools switch in chat settings and POST /chats/:id/settings. Team and org policy layers can still ask or deny over a chat's rules. Allow rules on the agent template apply only in the agent's home workspace; in another workspace's chat, template ask and deny rules still apply but allow rules do not. See also Approval settings — Agent template defaults.

FieldTypeDescription
allow_all_toolsbooleanWhen true, each new chat in the agent's home workspace starts with allow all tools on so unattended runs (webhooks, cron, triggers) are not blocked on tool approvals. Existing chats are unchanged when you deploy a new version
json
1"permissions": {2  "allow_all_tools": true3}

In agent YAML (belt agent deploy), permissions is a top-level key next to system_prompt.

Example — first deploy:

bash
1curl -X POST https://api.inference.sh/agents \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{5    "name": "support-agent",6    "version": {7      "description": "Customer support assistant",8      "system_prompt": "You help users with billing questions.",9      "core_app": { "ref": "openrouter/claude-sonnet-46" }10    }11  }'

Example — unattended agent (allow every tool in new chats):

bash
1curl -X POST https://api.inference.sh/agents \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{5    "name": "oncall-runner",6    "version": {7      "system_prompt": "Run the playbook when paged.",8      "core_app": { "ref": "openrouter/claude-sonnet-46" },9      "permissions": { "allow_all_tools": true }10    }11  }'

Example — redeploy same name (creates new version):

Send the same request shape with updated version fields — no agent ID lookup required.

To update by ID instead (for example when you already have agent_abc123), use POST /agents/{id} with a version object. To change visibility or store images without a new version, use POST /agents/{id}/visibility or include only agent-level fields in POST /agents/{id}.

CLI: belt agent deploy <path> and belt agent create both use this endpoint. See CLI setup — Agents.

This endpoint is for new versions of an existing template. To fork a template into a separate agent (new ID, new version row, default private visibility), use Duplicate agent instead.


Duplicate agent

POST /agents/{id}/duplicate

Creates a new saved agent from a template you can read. Requires the agents:write scope. Returns the new Agent object.

BehaviorDetail
NameFirst free <name>-copy, then <name>-copy-2, … in your workspace namespace
VisibilityAlways private on the copy
TitleAppends (copy) when the source has a title
VersionCopies the source's current version config into a new version row; the original agent does not gain a version
WorkspaceThe copy is created in the caller's active workspace. When the source lives in another workspace (for example a public agent), project, profile, and remote bindings are omitted

Unlike POST /agents with an existing name (which appends a version to the same agent), duplicate always creates a distinct agent ID — the same outcome as Duplicate in the web app. Flows use the same -copy naming rule; see Duplicate flow.

JavaScript SDK

typescript
1const { data: copy } = await client.agents.duplicate('agent_abc123');

Delete agent template

DELETE /agents/{id}

Permanently deletes a saved agent template (workspace definition). Requires the agents:write scope. This does not delete chat history from past runs.

bash
1curl -X DELETE https://api.inference.sh/agents/agent_abc123 \2  -H "Authorization: Bearer inf_your_key"

CLI equivalent: belt agent delete <ref> (accepts agent ID or namespace/name). See CLI setup — Agents.


we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.