Skip to content

Agents and skills

This page configures three linked concepts: agents, the directives that steer them turn by turn, and the named skills an agent can use from routines or supported invocation modes. The read-only product skills catalog still describes stable platform contracts. Use a personal token or service credential for eligible authoring routes; lifecycle settings and credential management use the signed-in dashboard session. See Personal tokens and service accounts.

Authoring and execution are different. Agent skills are created through the agent skill endpoints. Runtime still dispatches through the capability executor behind the named skill.

Draft revisions and private tests

Agent behavior is saved in a private draft. A successful directive, routine, context-variable, or agent-setting write advances the draft generation; it does not change the revision used by new visitor conversations. The dashboard then materializes one immutable candidate for that generation, lets you test it, and publishes it as a separate action.

The revision API is available to workspace bearer credentials with the matching agent permission:

text
GET  /api/v1/agents/{agentId}/revision-state
GET  /api/v1/agents/{agentId}/greeting/draft
PUT  /api/v1/agents/{agentId}/greeting/draft
POST /api/v1/agents/{agentId}/revisions/candidates
GET  /api/v1/agents/{agentId}/revisions?include=published
GET  /api/v1/agents/{agentId}/revisions/{revisionId}
POST /api/v1/agents/{agentId}/revisions/{revisionId}/publish

The greeting wording is part of the draft. GET .../greeting/draft returns the greeting the draft holds as { "exactWordsEnabled": boolean, "exactContent": object | null }; a draft that never had a greeting authored returns exactWordsEnabled: false and exactContent: null. PUT .../greeting/draft takes the same two fields and saves them to the draft. It returns 400 bad_request with the field-level issues in error.details.issues when exactWordsEnabled is true and the content is invalid; with it false, the content is saved as written and the issues come back in the response’s validation object. Like every draft write, a successful PUT advances the draft generation.

POST .../publish requires the draft generation, the expected published revision, and an idempotency key. A stale request returns 409 revision_conflict; an invalid candidate returns 422 revision_invalid. Each successful publication receives the next per-agent version number (v1, v2, and so on). Retrying the same publication command returns that same version. Publishing changes one agent revision across its channels. Conversations that are already running keep their starting revision; new conversations use the published revision.

Private Test Chat selects a candidate or published revision and accepts sample values for the selected context variables. Test conversations and sample values stay operator-only. Test and eval evidence records the exact revision and inputs it used; changing the draft does not relabel earlier evidence. Selected eval cases can be run from Test Chat, and a failed side or case can be retried without replacing successful comparison evidence.

A test turn that fails settles like any other: the next message starts a new turn, and the failed side stays retryable until it does. A message sent while another turn is still running is refused with 409 and the error code test_turn_in_progress; send it again once that turn settles.

Sending a message with POST /api/v1/agents/{agentId}/test-executions/{executionId}/messages, or retrying a side with POST /api/v1/agents/{agentId}/test-executions/{executionId}/sides/{sideId}/retry, returns a text/event-stream. Each side opens with side_started and sends its answer as message_delta events while the agent writes it; in a comparison, the two sides’ deltas interleave. A side ends with side_completed, whose answer is the stored answer, or with side_failed and its code. Show answer in place of the streamed text once it arrives: the stored answer is the final presentation, which can trim whitespace or citation anchors the stream carried. The turn closes with execution_completed, or execution_partial when a side failed. Repeating the request for an attempt that already completed replays its stored answer as one message_delta without running the turn again.

Use GET /api/v1/agents/{agentId}/test-executions?limit=50 to page through private Test Chat history. An execution is listed once it has a user message, sent by the operator or copied from a real conversation, so one holding only its proactive greeting stays off the page. Each row carries turnCount, the number of user messages in the test, counting both those copied from a real conversation it continues and those the operator sent, and firstMessage, the first of them with text, as a label of at most 200 characters ending in … when it is longer; firstMessageClipped says whether it was cut, so a message that really ends in … reads as whole. A greeting is not counted, and firstMessage is null until there is a user message with text. Then use GET /api/v1/agents/{agentId}/test-executions/{executionId} to reopen one execution with its frozen values, side transcripts, and retry evidence. The detail also carries seededTurnCount, the user messages a seed conversation copied in; it is 0 for a test that started fresh. These endpoints require workspace.agents.manage and never add test conversations to public or visitor history.

To continue a real conversation as a private test, start a single execution with seedConversationId set to a conversation in the same workspace and agent. The response is the execution with the side’s history already holding that conversation’s user and assistant messages, and the side resumes the conversation’s active routine, pending clarification, and directive state on its next turn. The source conversation is never changed, a seeded execution skips the greeting, and the request is rejected with 400 when mode is compare or with 404 when the conversation belongs to another workspace or agent.

Agents

Each workspace has a default agent. Chat calls use that agent when agentId is omitted.

Agents own:

  • display name and logo
  • custom instructions
  • suggested-question behavior on the default retrieve skill
  • theme and greeting behavior
  • source scope
  • surface settings for authenticated chat, anonymous chat, and website embed

Retrieval answer configuration lives on the agent’s default-answer retrieve skill. Use the unified skill endpoints to update fields such as query rewrite behavior, reranking, metadata rules, strategy, source scope, suggested questions, and answer instruction for that one agent.

The read-only GET /api/v1/settings/retrieval-defaults endpoint returns those inherited retrieval defaults plus workspace metadata field suggestions for the Skills tab. It does not update retrieval behavior.

If an agent has retrievalEnabled: true, assistant chat may use retrieval and return citations. If retrievalEnabled: false, the agent answers through the direct assistant path and returns diagnostics with retrievalInvoked: false.

Delete an agent with DELETE /api/v1/agents/{agentId}. A workspace always keeps at least one agent, so deleting the last one returns 400. Deleting the default agent hands that role to another agent in the workspace.

An agent’s anonymous chat and website embed each carry their own launch token. Rotate either one directly on the agent, independently of the other and of the workspace-wide tokens in Settings:

text
POST /api/v1/agents/{agentId}/anonymous-chat-token/rotate
POST /api/v1/agents/{agentId}/website-embed-token/rotate

Both return the updated agent. The old link or install code stops launching new sessions the moment the token changes. Website embed token rotation requires the website-embed surface extension; without it the route answers 404.

Directives

Directives add steering to matching turns. A directive has a condition, an action, optional priority and relationship fields, and optional scope tags.

In the dashboard you author directives in the agent’s Directives settings, where your own rules are listed one to a line above the collapsed group of built-in Radioso directives you can Customize.

The Directives tab as a list of one-line rows: two directives that each name the situation they apply to, a customized Readable formatting row marked with a layers icon and "Always · using your version", and a collapsed Radioso defaults group below.

GET /api/v1/agents/{agentId}/directives returns the agent’s authored directives and the Radioso builtIns. Each built-in carries its stable name (the value a directive lists in excludes to replace it), a human title such as Readable formatting, its condition, action, priority, and description. Built-ins are read-only; replacing one means creating a directive that lists its name in excludes.

Create a directive with POST /api/v1/agents/{agentId}/directives:

json
{
  "name": "order-status",
  "condition": {
    "kind": "contextual",
    "description": "the visitor asks about an order status"
  },
  "action": "Look up the order before answering. Do not guess order state.",
  "priority": 80,
  "binding": {
    "kind": "skill",
    "skillName": "order_lookup"
  }
}

binding is optional and may be null. The only supported binding kind is skill. The named skill must exist on the agent, be enabled, and use agent_selectable invocation mode. External MCP skills (kind: external_mcp) can claim the terminal turn and produce a user-facing chat reply. Retrieval skills (kind: retrieve) are staged into the agentic retrieval loop as directive-scoped lookup tools. Action-only skill kinds are rejected. Invalid bindings return 400 and name the offending skill.

To change an existing directive, use PATCH /api/v1/agents/{agentId}/directives/{directiveId}. Leave coverageCriteria out to keep its saved values, or send "coverageCriteria": null to remove the answer-coverage condition. A non-null criterion needs at least one coverage value; its optional reasons list is omitted when it has no restrictions.

In the dashboard, the binding is a chip in the directive’s Instruction field: type # to pick from the skills this agent can bind, and the chip you insert is binding.skillName. The menu offers exactly the skills the API accepts, and a directive holds one, so the menu explains itself instead of offering a second while a chip is present. Deleting the chip clears the binding on save. A binding whose skill is disabled or renamed keeps its chip and marks it as an unknown skill, so the rule stays visible instead of disappearing.

The chip is the binding, and binding in the API is what decides which text is a chip when the directive is opened again. Prose reads as prose: an action such as Point the customer at #billing for pricing questions. on a directive with no binding keeps those characters as words, and saving the directive leaves binding at null — even when a bindable skill named billing exists.

Write the chip wherever the sentence needs it, including at the end: Escalate using #issue_refund. binds issue_refund and keeps its period, because a trailing . , ; : ! or ? belongs to the prose. Reopening that directive shows the same chip and the same text, so a save that changes only the priority sends the action back unchanged.

If you type a name the agent has no skill for, the menu offers Create skill ”…”. That opens the same capability picker and configuration form the Skills list uses, narrowed to the two capabilities a directive can bind — MCP tool and Knowledge retrieval — with the name you typed already in the form. Skill names are lowercase identifiers, so RefundLookup is saved as refundlookup and the chip carries that. The skill is created enabled and agent_selectable, which is what the binding requires. Cancel out of either dialog and the field keeps your prose with no chip and no binding. MCP connections are still set up in the MCP channel section: connecting and authorizing a server is infrastructure, separate from authoring behavior over it.

When a directive with an external MCP binding matches a non-routine turn, the turn selector routes to the bound skill if that skill is available as a runtime turn skill. The agent’s model then writes the reply from the tool’s text, read as findings rather than instructions, in the agent’s voice and with the directive action steering it. If the bound skill then fails, times out, returns no answer text, or is kept off by a private test, the agent answers through normal selection without claiming the tool’s results, and the turn trace records the failure reason on the bound skill’s dispatch. When a directive with a retrieval binding matches, the answer loop receives a lookup tool for that skill. The directive action still steers the answer. A non-matching directive has no selection effect.

When multiple matched directives bind skills, one binding wins:

  1. higher priority
  2. higher matcher confidence, with deterministic always matches treated as 1.0
  3. directive name ascending

If a bound external MCP skill is later disabled, removed, no longer turn-selectable, not registered at runtime, or blocked by the workspace capability policy (external skills require the external_skills.invoke capability), Radioso ignores the binding and falls through to normal selection. The directive action still applies. The turn trace records selected, losing, and skipped terminal bindings with their reasons. If a bound retrieval skill is unavailable or blocked by the retrieval.answer capability, it is not staged as an agentic lookup tool.

Agent config export/import preserves directive bindings by skill name. Import does not validate that the target agent currently has the skill: when the imported binding points at a skill the agent doesn’t have, the same fall-through rule above applies — Radioso ignores the binding and uses normal selection — right up until you add and enable a skill with that name.

Bindings on directives scoped with routine:<id> or step:<routineId>:<stepId> are accepted but inert during routine turns, because routine flow bypasses terminal turn selection.

Agent skills

GET /api/v1/agents/{agentId}/skill-capabilities returns the capability registry projection for the agent. Each capability includes its targets, whether it requires a target, input schema, outcomes, supported invocation modes, and availability.

Every agent has three built-in answer skills: Clarification answer, Retrieval answer, and Direct answer. They are always present in the Skills list and cannot be edited or removed. Direct and clarification answers stay on. Turn Retrieval answer off in the dashboard to set the agent’s existing retrievalEnabled setting to false; the agent then uses the direct answer path instead of workspace retrieval.

GET /api/v1/agents/{agentId}/skills keeps its existing flat skills array of persisted agent skills, including the default-answer retrieve skill. It also adds a read-only platformSkills array for the built-in answer skills. Each platform row has only owner: "platform", name, displayName, and description; it cannot be updated or removed through the agent skill routes. Persisted rows in skills retain their existing behavior and fields:

  • name
  • capability
  • target
  • config
  • invocationMode
  • enabled

Create skills with POST /api/v1/agents/{agentId}/skills. In the dashboard, Add new skill first opens a capability picker; enabled tiles then open the configuration form for that capability. The form shows only the skill name, enabled state, required target or tool fields, and essential capability settings by default. Invocation mode and advanced retrieval tuning live under Advanced. Input binding, outcomes, and advanced JSON live under Routine integration because they only matter when a routine or the agent selects the skill. The form and API use the same model for retrieve, mcp_tool, email, slack_post, webhook_call, and notify.

A mcp_tool skill (and the external_mcp directive binding below) is Radioso calling out to someone else’s MCP server as a tool. That’s the opposite direction from Radioso’s own MCP server, which a client like Cursor or Claude Desktop connects into.

Each row in the Skills list says which authored surfaces reach it — “Used by 2 directives and 1 routine”, or “Not used by a directive or routine”. A skill nothing references never fires, and the count is the fastest way to spot one after a rename or a deleted directive. The dashboard derives it from GET /api/v1/agents/{agentId}/directives and GET /api/v1/agents/{agentId}/routines; the skill envelope itself carries no usage field. A routine counts once however many of its steps call the skill.

Update skills with PATCH /api/v1/agents/{agentId}/skills/{skillId}. Use config for a shallow merge into the existing skill config. Use replaceConfig when sending the complete config from an editor and omitted keys should be removed, such as clearing a retrieval override so the skill inherits the default again. Do not send both fields in the same request.

Retrieval skill settings

The retrieve capability descriptor includes field help, inherited defaults, and dependency hints. In the dashboard, an unset field shows the effective system default inline. Saving still stores only overrides. Clearing a field removes that key from the skill config so the agent inherits the system default again.

The main tuning fields are:

  • retrievalStrategy: fixed runs one search pass, reasoning lets the model plan and run multiple searches, and auto lets Radioso choose per query.
  • vectorTopK: how many chunks are fetched from the vector index before filtering and reranking. After filters, boosts, and merging, only the top rerankTopK candidates (at least 12) go on to the answer, so a deeper fetch matters when filters or boosts lift lower-ranked chunks into that group.
  • rerankEnabled: whether the fetched chunks are re-scored with the reranker model. Off by default because it adds a model call to every retrieval.
  • rerankTopK: how many top candidates go on to the answer, reordered by the reranker when it’s enabled. Values below 12 act as 12; the max is 50. The answer itself still uses up to 12 passages, at most 2 per document. When temporal structured lookup finds passages in the period a question names, the answer holds up to 20: those passages plus up to 4 of the best matches from outside the period. A match dated to another period keeps its place only when the question is about a named topic and search ranks the match above everything in the period; otherwise its place goes to the period’s own passages, and it comes in only to fill out a sparse period.
  • queryRewriteEnabled: whether the user message is rewritten into search queries before retrieval.
  • semanticRewriteInstructions: the instructions used to rewrite the user message into the semantic vector search query. A non-empty value replaces the default. The dashboard shows the default read-only until you choose Override, which copies it into the editor so you can adjust it. It is relevant when query rewrite is enabled.
  • lexicalRewriteInstructions: the instructions used to rewrite the user message into the lexical keyword search query. It behaves the same way as the semantic instructions. It is relevant when query rewrite is enabled.
  • suggestedQuestionsEnabled: whether the assistant offers follow-up question suggestions after each answer.
  • suggestedQuestionsCount: how many follow-up questions to suggest. It is relevant when suggested questions are enabled.

Context variables

Context variables are workspace declarations that an agent may enable per agent. They are separate from skills. A skill is something the assistant can do; a context variable is data the turn may resolve and use.

The catalog endpoints create and update host-defined variable declarations:

  • name
  • description
  • valueType: string or json
  • trustTier: unverified or signed
  • sensitivity: normal or sensitive
  • defaultSurfacing: always, on_reference, or operator_only

The per-agent enablement endpoints choose how a catalog variable is wired for one agent:

  • source: pushed or resolver
  • surfacing: always, on_reference, or operator_only
  • enabled

The dashboard exposes the pushed source. Resolver-backed variables are part of the API contract; the operator UI does not configure them. A resolver enablement also supplies resolverSkillId, which must name an enabled skill on the same agent. Disabling that skill disables its resolver enablements; re-enabling the skill does not opt those variables back in automatically. Deleting the skill removes its resolver enablements.

Host backends push runtime values through PUT /api/v1/context-variables/{id}/values. The request identifies the scope:

json
{
  "scope": { "type": "session", "id": "public-session-id" },
  "data": {
    "items": [{ "sku": "course-101", "quantity": 1 }]
  }
}

Values resolve from most specific to least specific scope: session, customer, agent, then workspace. Use the narrowest scope that matches the data. For example, a cart usually belongs to a session or customer; a market-wide promotion may belong to an agent or workspace.

Contact requests

An agent can offer a “contact a human” option in chat. This is backed by a notify skill, commonly named contact_human. When the skill is enabled, the assistant can collect the visitor’s email and message, then deliver the request out of band.

notify is config-only. It does not bind to a connection target, so create it with target.kind set to notify_delivery and target.id set to null.

Configure delivery in the notify skill config:

  • recipientEmails: up to 25 email addresses. Each is emailed a copy of the request. When the list is empty, Radioso falls back to the workspace owner.
  • webhook: an optional { "url": "https://..." }. When set, Radioso also POSTs the request to that URL. Email and webhook both fire when both are configured.

An agent can have more than one notify skill — for example contact_sales and contact_support, each with its own delivery config. A routine step that calls a specific skill by name (#contact_sales) delivers through that skill’s own recipients and webhook. A routine step that reaches the chat-only contact flow without naming a skill, or names a skill that turns out to be disabled or missing, delivers through the contact_human skill when one is configured, then through the agent’s own delivery setting, then through the workspace owner.

An external recipient confirms before it receives a contact request or operator notice. Radioso sends the confirmation message as soon as an address is added to a notify skill or the agent’s contact settings, so the address can confirm before the first notice is due; an address that missed that message gets it at its first notice instead, and that notice skips the address. When no other address or webhook can take the notice, it goes to the workspace’s owners and admins instead (see When no address can receive a notice). Confirmation applies to one normalized address in one workspace. A verified login address for an active workspace member counts as confirmed for as long as that person stays a member. The Skills form shows Waiting for confirmation, Confirmed, Notifications stopped by the recipient, or Suppressed after a bounce or complaint beside each address; owners and admins can resend a waiting confirmation from there, at most once a day per address. Every confirmation message, including a resend, counts against a daily allowance of 25 per workspace and 50 across all workspaces of an account. A confirmation the mail transport never dispatched does not count and leaves the earlier link working.

Addresses grandfathered by the confirmation rollout carry a recorded 14-day deadline. Their first notice also sends the confirmation message, and they keep receiving notices until they confirm or until both the recorded deadline and 14 days after that message have passed, so a quiet agent’s address is never cut off before it was asked. After that, an address that has not confirmed is skipped; its link keeps working. Every delivered contact or notice email includes a one-click unsubscribe link; using it stops notifications to that address only in the workspace that sent the message. The page that confirms the stop offers Resume notifications. Radioso never asks a stopped address to confirm again, so only the recipient can resume.

Example notify skill config fragment:

json
{
  "delivery": {
    "recipientEmails": ["support@example.com", "ops@example.com"],
    "webhook": { "url": "https://example.com/hooks/contact" }
  },
  "exposedInputs": { "message": true, "email": true }
}

Webhook payload

The webhook receives a POST with a JSON body:

json
{
  "name": "Ada",
  "email": "ada@example.com",
  "message": "Please call me back.",
  "workspaceId": "...",
  "conversationId": "...",
  "requestId": "..."
}

Each request carries Idempotency-Key, X-Radioso-Timestamp, and X-Radioso-Signature headers. The signature is sha256=<hex HMAC-SHA256> over <timestamp>.<raw request body>, keyed with the exact webhook URL. Keep that URL private, verify the signature against the unparsed body, reject stale timestamps, and use the idempotency key to de-duplicate at-least-once retries. Operator-notification webhooks use the same headers and signing scheme.

Radioso sends the request with POST, follows redirects only to publicly routable hosts, and applies a short delivery timeout. Point the webhook at a stable public endpoint.

Only workspace owners and admins can add, remove, or replace recipient email lists on an agent or notify skill. Recipient changes write an audit event with recipient counts and one-way hashes; the event does not contain the addresses.

Outbound delivery is bounded twice. A conversation can dispatch at most five contact requests or operator notices. Recipient email attempts also share a per-workspace UTC-day allowance: 100 on Comet, 1,000 on Satellite, 5,000 on Planet, and 100 by default on self-hosted installations. When the daily email allowance is exhausted, Radioso skips email but still calls the configured webhook. When the conversation allowance is exhausted, it skips both. Each skip records workspace_daily_email_cap or conversation_dispatch_cap for the operator without recording an address. A notice that goes to the owners and admins in place of its addresses counts one email per owner or admin against the same daily allowance. Once the daily allowance stops an email, the routine editor says so under the notice’s destination, and the destinations endpoint and the Operator MCP operator_notice_destinations tool report dailyEmailAllowance.reachedToday.

Resend hard-bounce and complaint events add the address to the deployment-wide notification suppression list. Suppression applies to contact messages, operator notices, and recipient confirmations. Authentication mail such as password resets and login-address verification keeps its separate delivery path. When some of the addresses are skipped, the others still receive the email and the delivery log records each address on its own. When every email recipient is skipped, the delivery outcome names why: notification_recipient_unconfirmed while any address still has to confirm, then notification_recipient_revoked when a recipient stopped notifications, then notification_recipient_suppressed. Logs and metrics carry only the reason and recipient count.

Visitor content is bounded before delivery: contact names use at most 200 characters, contact messages 4,000, and each routine slot value in a notice 1,000. A visitor-supplied address is used as Reply-To only when it is a valid email address of at most 320 characters.

Operator notifications

Radioso also tells a person when a conversation needs one, or when a routine finished something they act on: a routine reaches a handoff, the agent hands off on a retrieval miss, a routine finishes on an ending with Notify the team on, or a routine step pauses for an approval decision. A routine’s hand-off and finish endings can each name a notify skill for their own notice; when an ending names one, Radioso sends through that skill’s own recipientEmails and webhook, whatever the skill is called. A hand-off on a retrieval miss, an approval, and any ending that names no skill resolve their destination the same way an unnamed contact request does: through the contact_human skill when one is configured, then the agent’s own delivery setting, then the workspace owner. Turning contact_human off silences a notice that names no skill, along with contact requests that name none. A notice whose ending names a skill that was later deleted or turned off falls back to the same chain, but passes over a turned-off contact_human, so it still reaches the agent’s delivery setting or the workspace owner.

GET /api/v1/agents/{agentId}/operator-notice-destinations returns where a routine ending’s notice would go right now: a default destination plus one entry per eligible notify skill. Each entry keeps recipientEmails and adds recipients, whose email and state report confirmed, grandfathered, waiting_for_confirmation, revoked, suppressed, or confirmation_not_required. It also says whether the addresses came from the workspace owner, whether a webhook is configured (never the URL itself), and via names the rule that picked the recipients — named_skill, contact_human, contact_human_off, agent_setting, workspace_owner, or none. The top-level dailyEmailAllowance gives the workspace’s limit, the recipient emails sentToday, and reachedToday, which turns true once the allowance is used up or has stopped a send that UTC day; a smaller notice may still fit afterwards. It needs workspace.agents.read and a workspace session; a machine API token gets 401, because recipient emails are personal data.

GET /api/v1/agents/{agentId}/skills/{skillId}/notification-recipients returns the same recipients list, with email and state, for one notify skill whatever its invocation mode; it takes the same permission and workspace session.

An owner or admin can call POST /api/v1/agents/{agentId}/notification-recipient-confirmations/resend with { "email": "support@example.com" }. The address must be the agent’s default destination or a recipient of one of its notify skills. A 202 response returns { "sent": true } when the confirmation was admitted, or false when confirmation is disabled, the recipient is already confirmed, stopped notifications, or is suppressed, the address was asked within the last 24 hours, or the daily confirmation allowance is exhausted.

Delivery log

The notify skills’ recipient list tells you who a notice is meant for. The delivery log tells you what happened to it: one entry for each address of each contact request or operator notice, whichever skill or default destination it went through.

GET /api/v1/notification-deliveries lists them newest first. It needs workspace.agents.read and a workspace session; a machine API token gets 401, because the entries name recipient emails. Narrow the list with agentId or outcome, and page through it with limit (1 to 100, default 50) and the nextCursor of the previous page passed back as cursor.

json
{
  "items": [
    {
      "id": "...",
      "agentId": "...",
      "conversationId": "...",
      "kind": "handoff",
      "recipientEmail": "front-desk@example.com",
      "outcome": "skipped_unconfirmed",
      "createdAt": "2026-11-07T10:00:00.000Z",
      "updatedAt": "2026-11-07T10:00:00.000Z"
    }
  ],
  "nextCursor": null
}

kind says what raised the notice: contact_request, approval, handoff, or completion. outcome is one of:

OutcomeWhat happened
sentThe mail provider accepted the email.
skipped_unconfirmedThe address has not confirmed yet.
skipped_revokedIts owner stopped notifications from the workspace.
skipped_suppressedAn earlier email to it bounced or was reported as spam.
skipped_capThe daily email allowance or the conversation’s dispatch allowance stopped it.
not_dispatchedThe deployment has no mail provider, so the email was written to the log.
failedThe email was attempted and did not go out, or the address’s state could not be read.

A retry of the same notice updates its entries rather than adding new ones, so an address that failed and then went out reads sent; an entry that reads sent is never changed afterwards. The log lists email only: a webhook post is not an entry. A notice for a conversation with no agent, or whose agent has since been deleted, has agentId set to null. Entries go with their conversation, so deleting a conversation, or a retention window erasing it, deletes them too.

In the dashboard, the same entries show under Recent notifications at the bottom of the agent’s Skills page, with a link to the conversation and, for an address that has not confirmed, Resend confirmation. Ray reads them with list_notification_deliveries, and so does an Operator MCP client.

When no address can receive a notice

A request still reaches a person while its address waits to be fixed. When every address of a contact request or operator notice is skipped because it is waiting for confirmation, stopped notifications, or is suppressed, and no webhook is configured, Radioso emails the notice to the people who can fix the address: the organization’s owners and admins, and members who are admins of that workspace through a workspace grant. These are the people the alert below reaches, chosen the same way.

Each of them gets the notice as written, with its subject and its Reply-To, so a reply still goes to the visitor. One line on top says why it came to them:

text
This notification came to you because front-desk@example.com cannot receive it: the address has not confirmed yet that it wants notifications. Fix it on the agent's page: https://app.example.com/w/acme/agents/...

Each copy carries the same one-click unsubscribe link any notice email does, for the owner’s or admin’s own address; using it leaves them out of these copies and of the alert for that workspace. Their copies count against the daily email allowance like any notice email, and a retry sends each of them the same message once. In the delivery log, the address reads as skipped and each owner or admin appears as a sent entry with the same conversation, kind, and time; nothing else marks those entries. The action records partially_dispatched with the skip reason, because the address the notice was meant for still missed it.

A notice stopped by the daily or conversation allowance stays stopped, and an email that failed in transit retries to its own address. A notice with no destination configured at all goes to the workspace owner through the destination chain above, so it never needs this step. Slack is a separate destination: a workspace with an escalation channel and an address that has not confirmed gets the notice in Slack and by email to its owners and admins.

Alert to owners and admins

When a notice skips an address because it has not confirmed, its owner stopped notifications, or it bounced, Radioso emails the people who can fix it: the organization’s owners and admins, and members who are admins of that workspace through a workspace grant. The email names the address, says why it was skipped, names the agent, and links to the agent’s Skills page, where the address can be replaced or asked again. Only active members with a verified login address receive it, and an address that itself bounced, or whose owner stopped notifications from the workspace, is left out. When the notice itself went to them because no address could receive it, that copy already names the address and why, so no separate alert follows.

Each address triggers at most one alert a day for a workspace, however many notices it misses. A skip because of a daily allowance, and an email that failed in transit, send no alert: neither is something the address’s owner or the operator fixes by changing the recipient. The alert uses the deployment’s mail transport. Without a mail provider it is not sent, the log shows only that an alert was due, and the day’s alert is spent anyway, since a retry would send nothing either.

Each notice is emailed to the resolved recipients as plain text and, when a webhook is configured, posted to it as JSON. Here is the notice a “Book accommodation” routine sends when it finishes with a notice of its own:

json
{
  "workspaceId": "...",
  "agentId": "...",
  "conversationId": "...",
  "reason": "routine_completed",
  "routine": { "id": "...", "name": "Book accommodation" },
  "collected": {
    "program": "Yoga retreat",
    "arrival": "2026-10-12",
    "departure": "2026-10-16",
    "guest_name": "Alex Rivera",
    "guest_email": "alex@example.com"
  },
  "aiSummary": "The guest needs step-free access and expects to arrive after 22:00.",
  "subject": "New booking: Alex Rivera, 2026-10-12",
  "intro": "Please confirm the room with Alex Rivera.",
  "replyTo": "alex@example.com",
  "dashboardUrl": "https://app.radioso.ai/w/support-abc/activity?tab=all&filter=chat&itemKind=chat&itemId=...",
  "requestId": "..."
}

reason says what raised the notice: routine_handoff when a routine reached its hand-off ending, routine_stuck when a routine ended on a step its visitor stayed stuck on, or retrieval_miss — all three for a handoff — and routine_completed for a routine that finished and kept the conversation with the agent. routine names the routine whose ending raised the notice, with name set to null when the routine has since been deleted; it is null for a retrieval-miss handoff. collected holds the routine’s declared values keyed by slot key, in the order the routine declares them (in storage order once the routine has been deleted), and is empty for a retrieval-miss handoff. Text, number, and boolean values are included; text is capped at 1,000 characters and a slot holding a structured value is left out. The email version of the notice reads as plain prose: it lists the same values under a Collected: heading and, when the conversation has one, the page it started on. The JSON body carries collected as structured data instead of that heading, and has no field for the entry page.

subject and intro are the text the routine’s author wrote for that ending, with each {{slot.<key>}} replaced by the collected value, or — when the routine did not collect it. Each is null when the author wrote none; the email then uses the default subject (Book accommodation: completed, or Book accommodation: needs a human for a handoff) and no intro.

aiSummary is present when the ending has Include an AI summary on and the background summary succeeds. It describes relevant conversation details outside collected, in the agent’s configured language or the conversation’s language. A generation error or timeout leaves the field out and the notice still sends.

replyTo is the address a reply to the notice should go to: the value the visitor gave in the email field the ending’s author chose under Replies go to (operatorNotice.replyToSlot on the routine), here guest_email. The email sets it as its reply-to header, so reception answers the guest by hitting Reply; a webhook receiver that writes back to the visitor uses the same address. It is null when the ending names no field, when the visitor never gave that field, when the collected value is not a valid address of at most 320 characters, and for a retrieval-miss handoff. Radioso never picks an address on its own, even when collected holds one.

An approval carries handle instead — the value you pass to POST /api/v1/agents/{agentId}/decisions/{handle}/resolve, described in Human takeover. That is how a receiver tells an approval from a routine or retrieval-miss notice. dashboardUrl opens the conversation in the dashboard; it is null when the link cannot be resolved, for example because the workspace has since been deleted, so a receiver that needs a link should fall back to conversationId. The same Idempotency-Key and delivery rules as contact requests apply.

Move an agent between workspaces

In the dashboard, an agent’s Profile page carries Move this agent → Export bundle, which downloads the file. To bring one in, open Agents in the sidebar, choose New agent, then Import a bundle; the dialog shows what the file contains before it creates anything, and reports what needs your attention afterwards. The API below is the same pair of operations.

GET /api/v1/agents/{agentId}/bundle reads one agent — its configuration, routines, context-variable enablements, and named skills — into a single portable document. POST /api/v1/agents/bundle reads that document back and creates a new agent from it. Use the pair to back up an agent, promote one from a staging workspace into production, or hand a configured agent to a teammate’s workspace.

Export needs workspace.agents.read; import needs workspace.agents.manage, because it creates an agent.

bash
curl -sS https://api.radioso.ai/api/v1/agents/<agent-id>/bundle \
  -H "Authorization: Bearer $RADIOSO_API_TOKEN" \
  -o support-agent-bundle.json

The bundle looks like this, trimmed:

json
{
  "bundleVersion": 1,
  "agent": { "schemaVersion": 4, "name": "Support", "customInstruction": "..." },
  "routines": [
    {
      "name": "refund-flow",
      "version": 3,
      "definition": {
        "name": "refund-flow",
        "enabled": true,
        "steps": [{ "stableStepId": "step_refund", "kind": "tool", "toolRef": "issue_refund", "...": "..." }],
        "...": "..."
      }
    }
  ],
  "contextVariables": [
    { "variableName": "cart", "source": "resolver", "resolverSkillName": "cart_lookup", "enabled": true }
  ],
  "agentSkills": [
    {
      "name": "cart_lookup",
      "capability": "mcp_tool",
      "invocationMode": "agent_selectable",
      "enabled": true,
      "config": {},
      "omittedConfigKeys": [],
      "target": { "kind": "mcp_connection", "id": { "__ref": "agentSkillTarget" } }
    }
  ]
}

routines carries the agent’s routines, each with its own enabled flag, so a routine parked out of service arrives parked rather than going live in the destination workspace. A routine names its own skills and context variables (toolRef, slot bindings) rather than a database id, so nothing in it needs remapping on import. contextVariables re-keys each enablement to variableName and resolverSkillName in place of the ids the source database used. surfaceSettings.websiteEmbed.voiceCallsOffered always exports as false, so an imported agent’s website widget offers no voice calls until an operator in the destination workspace turns them on. agent.contactRequestDelivery is always redacted: its recipients and webhook URL stay in the source workspace, because an imported agent that kept them would quietly route contact requests to the original workspace’s people and endpoint. agentSkills carries only the config fields the skill’s capability marked portable — a webhook URL or delivery recipient stays home — and replaces target.id, the workspace connection the skill points at, with a { "__ref": "agentSkillTarget" } placeholder, because that connection is where the credential lives. Each skill also carries omittedConfigKeys: the names — never the values — of the settings that stayed behind, so import can tell you exactly what to re-enter.

Authored directives travel with the agent configuration, including any coverageCriteria. Import preserves those values, so a directive conditioned on an unanswered answer keeps the same condition in its destination workspace.

Move the file wherever you’re importing into — a different workspace, a different Radioso deployment — and post it:

bash
curl -sS -X POST https://api.radioso.ai/api/v1/agents/bundle \
  -H "Authorization: Bearer $RADIOSO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d @support-agent-bundle.json

A successful import returns 201 with an importId, a new agentId, and an unresolved array. Read it before treating the agent as ready:

json
{
  "importId": "a6e21c3e-2f6a-4d1a-9e3c-8f1a2c9a11de",
  "agentId": "b6e21c3e-2f6a-4d1a-9e3c-8f1a2c9a11de",
  "replayed": false,
  "unresolved": [
    {
      "kind": "skill_target_unbound",
      "element": "skill:cart_lookup",
      "detail": "Bind \"cart_lookup\" to a mcp_connection in this workspace, then enable it."
    },
    {
      "kind": "context_variable_missing",
      "element": "contextVariable:cart",
      "detail": "No context variable named \"cart\" exists in this workspace. Create it, then enable it on the agent."
    }
  ]
}

For an automated retry, add an idempotencyKey to the posted bundle. The key is scoped to the destination workspace. Repeating a completed key returns 200 with that same importId, agentId, unresolved report, and replayed: true; a key that is still applying returns 409 with agent_bundle_import_in_progress. A failed or compensated attempt does not hold the key, so you can retry it as a fresh import.

Use the import id when an operator needs to inspect an attempt after the request:

bash
curl -sS https://api.radioso.ai/api/v1/agents/bundle/imports/a6e21c3e-2f6a-4d1a-9e3c-8f1a2c9a11de \
  -H "Authorization: Bearer $RADIOSO_API_TOKEN"

The job state moves from queued to applying, then to applied, failed, or compensated. compensated means the worker removed an agent left behind by an interrupted import; the response keeps the original agent id so support can correlate the cleanup. Set AGENT_BUNDLE_IMPORT_ORPHAN_AGE_MS to control how old an active job must be before that worker sweep runs. The default is 15 minutes. Imports normally take seconds: if an import exceeds that age, cleanup fences the request, the request returns a retryable 409, and the cleanup owns deleting its agent.

Each kind names one thing to go fix, and where:

  • skill_target_unbound: the skill’s connection — a webhook, an MCP server, a mailbox — holds a credential and stayed in the source workspace. Point the skill at a connection here, then enable it.
  • skill_capability_unknown: the bundle names a capability this deployment doesn’t register. The skill wasn’t created, so anything that called it stays unbound.
  • context_variable_missing: no context variable with this name exists in the destination workspace. Create it, then re-enable the enablement on the agent.
  • resolver_skill_missing: the enablement’s resolver skill isn’t on the imported agent — often because that skill itself hit skill_capability_unknown. Re-enable the variable once the skill exists.
  • routine_invalid: the routine could not be written into the destination workspace. A completion export pointing at a webhook destination that only exists in the source workspace is the usual cause. The detail names the reason; create what it names, then add the routine through the routine endpoints.
  • document_source_unresolved: the source agent answered from a specific set of documents. Document ids don’t carry across workspaces, so the imported agent starts with no sources selected — pick its sources under Knowledge.
  • surface_credential_unbound: a chat surface (public chat link, website embed) was on, but its access token stayed in the source workspace. The surface imports off; turn it on to mint a new token.
  • mcp_connection_unbound: an external MCP connection, or a skill built on one. Connect the server under Skills → Manage MCP connections with its credential, or use a connection the workspace already has, then re-create the skill.
  • asset_not_portable: the agent’s logo lives in object storage, not in the bundle. Re-upload it.
  • contact_delivery_unbound: contact requests are on, but the recipient list and webhook stayed in the source workspace — an imported agent must never deliver to another workspace’s people. Set a destination under the agent’s contact settings.
  • chat_model_unavailable: the source agent ran on a chat model this workspace can’t pick — on a deployment whose shared OpenAI key is an Azure OpenAI key, a model with no deployment there. The agent imports without its chat model override and answers on the workspace’s chat model. Pick a chat model on the agent from the ones listed under Settings → Providers, or add the workspace’s own key for that provider first.
  • directive_binding_unbound: the directive is bound to a skill that did not come across, so it imports switched off rather than being dropped. Rebind it to a skill here, then enable it.
  • skill_config_not_portable: the source agent set skill settings whose values stay in their own workspace — a delivery webhook URL, a recipient list, a document-source scope. The detail names the exact settings; re-enter them on the imported skill. Where those settings are all a skill has, the skill itself is reported rather than created, and the detail says so — create it here and fill in its settings.

Every one of these is a safety default, and the pattern is the same throughout: import never grants an agent more access than you explicitly restore. A skill without its connection imports disabled, not live and broken. A surface without its token imports off, not serving on a token it never had. A source scope that named documents imports with none selected, not defaulted to every document in the new workspace.

Import reads the bundle from the request body, which is capped at 1 MB like every other Radioso API request. An agent with a very large number of routines can exceed that; split the move by importing the agent first and adding the remaining routines through the routine endpoints.

Both bundleVersion and the bundle’s agent.schemaVersion are checked against what this deployment reads, and the accepted versions are declared rather than inferred: an older agent.schemaVersion is accepted only while every field it predates defaults to the behaviour that version had. A bundle on a version this deployment does not read is rejected with 400 rather than partially applied.

Both routes record an audit event — agent.bundle.exported, agent.bundle.imported — with the agent id and the routine, skill, context-variable, and unresolved-reference counts, never the bundle’s contents: a bundle carries the agent’s custom instruction and every directive’s action text.

Read-only skills catalog

The skills catalog is read-only. It helps API, SDK, and assistant clients understand what work is available and which stable contract owns it.

For example, retrieval.answer points to the retrieval answer API. It does not require callers to switch to a generic POST /skills/{name}/execute contract.

Skill entries include:

  • name and description
  • optional display metadata, such as a UI title or icon hint
  • availability
  • contract references
  • required capabilities
  • diagnostic fields exposed by the implementation
  • skill-owned outcomes, each mapped to a normalized status such as completed or failed

Endpoint reference

Agent, directive, skill, and context-variable bodies are documented field by field in the API Reference.

text
GET    /api/v1/agents
POST   /api/v1/agents
GET    /api/v1/agents/{agentId}
PUT    /api/v1/agents/{agentId}
DELETE /api/v1/agents/{agentId}
POST   /api/v1/agents/{agentId}/anonymous-chat-token/rotate
POST   /api/v1/agents/{agentId}/website-embed-token/rotate
GET    /api/v1/agents/{agentId}/assistant-logo
POST   /api/v1/agents/{agentId}/assistant-logo
DELETE /api/v1/agents/{agentId}/assistant-logo
POST   /api/v1/agents/{agentId}/default
GET    /api/v1/agents/{agentId}/bundle
POST   /api/v1/agents/bundle
GET    /api/v1/agents/bundle/imports/{importId}
GET    /api/v1/agents/{agentId}/skill-capabilities
GET    /api/v1/agents/{agentId}/operator-notice-destinations
GET    /api/v1/agents/{agentId}/directives
POST   /api/v1/agents/{agentId}/directives
POST   /api/v1/agents/{agentId}/directives/draft
PATCH  /api/v1/agents/{agentId}/directives/{directiveId}
DELETE /api/v1/agents/{agentId}/directives/{directiveId}
GET    /api/v1/agents/{agentId}/skills
POST   /api/v1/agents/{agentId}/skills
PATCH  /api/v1/agents/{agentId}/skills/{skillId}
DELETE /api/v1/agents/{agentId}/skills/{skillId}
GET    /api/v1/context-variables
POST   /api/v1/context-variables
GET    /api/v1/context-variables/{id}
PATCH  /api/v1/context-variables/{id}
DELETE /api/v1/context-variables/{id}
GET    /api/v1/agents/{agentId}/context-variables
PUT    /api/v1/agents/{agentId}/context-variables/{variableId}
DELETE /api/v1/agents/{agentId}/context-variables/{variableId}
GET    /api/v1/context-variables/{id}/values
PUT    /api/v1/context-variables/{id}/values
DELETE /api/v1/context-variables/{id}/values
GET    /api/v1/skills
GET    /api/v1/skills/{skillName}

Common failure modes

  • 400 on agent create or update usually means a field failed validation.
  • 401 means the personal or service credential is missing, invalid, expired, or outside the route’s accepted credential scope.
  • 404 means the agent or skill name does not exist in the current workspace context.
  • A skill can be listed even when the work is executed through another route. Use contractReferences to find the stable execution path.
  • 400 on a bundle import usually means an unsupported bundleVersion or agent.schemaVersion — re-export the agent from a deployment this one reads.
  • 409 with agent_bundle_import_in_progress means another request with the same idempotency key is still applying. Query its importId or retry after it reaches a terminal state.