Subcore AI Docs
Agents API

Outcomes

The ordered rules that give each of an agent's sessions one outcome in the console. How to read, replace and reset them, and how the defaults are worked out from the agent's deployment.

Every session in the console's Sessions list shows one outcome, such as Booked, Transferred or Answered. Each agent has its own ordered list of outcomes. Each rule is a name and one condition. The last rule whose condition holds names the session, and the fallback names a session that no rule matches. These routes read and change that list. They change only what the console shows: nothing here changes what the agent does.

RouteWhat it doesSuccess
GET /agents/{slug}/outcomesThe list in force: the one saved for the agent, or its defaults200 { "outcomes": Outcomes, "source" }
PUT /agents/{slug}/outcomesReplace the whole list200 { "outcomes": Outcomes, "source": "stored" }
DELETE /agents/{slug}/outcomesRemove the saved list, so the agent is on its defaults again200 { "outcomes": Outcomes, "source": "defaults" }

All three need the manage-agents permission, count against the agent's rate budget, and answer 404 NOT_FOUND for an agent that is not the organization's or is retired. The MCP server's outcome tools call the same handlers.

The Outcomes object

{
  "fallback": "Answered",
  "rules": [
    { "name": "Booked", "when": { "tool": "book_appointment" } },
    { "name": "Message taken", "when": { "tool": "take_message" } },
    { "name": "Transferred", "when": { "event": "transferred" } }
  ]
}
FieldTypeDescription
fallbackstringNames a session no rule matches, the outcome every conversation starts as. 1 to 40 characters once trimmed. Answered until you change it
rulesarrayAt most 30 rules, in order. The last one whose condition holds names the session
rules[].namestringThe outcome as the console shows it. 1 to 40 characters once trimmed, and no two the same, ignoring case
rules[].whenobjectExactly one condition

Every answer pairs it with source:

sourceMeaning
storedSomeone saved this list, here or in the console. A stored list never changes by itself
defaultsNobody has. The list is worked out when it is read, so it follows the agent's deployment and its tools

Conditions

ConditionHolds when
{ "tool": "<tool id>" }The session has a successful call of that tool. The id is 1 to 64 letters, digits, _ and -
{ "event": "transferred" }The transfer tool handed the call to a number
{ "event": "voicemail" }The call reached voicemail
{ "event": "hung_up" }The caller hung up before the agent ended the call
{ "event": "agent_ended" }The agent ended the call

A tool rule holds on any channel, and a successful call is one that returned rather than threw. The tool need not be one the deployment has now: a rule on a tool the agent does not run is stored, and never matches.

The four events are about phone calls only. They come from the session.ended event a deployment sends when a call ends, from kit 0.35.0. A text, chat or email session matches only tool rules, or takes the fallback.

For a transfer, a voicemail or the agent ending a call, prefer the event to the tool. The kit refuses or undoes each of those in some calls by answering the tool call rather than failing it: a transfer with no number to go to, a voicemail a person picked up during, an end_call its voicemail guard refused, or a hangup or transfer the caller spoke over. Each still counts as a successful call of its tool, so a tool rule would name those calls wrongly, and the event would not.

The last rule that holds wins

The list reads as a conversation going deeper. Every conversation, a call, a text thread, a chat or an email, starts as the fallback, and each rule below is further along, so the lowest rule whose condition holds names the session.

With the list above, a call where the agent booked an appointment and then transferred the caller is Transferred, because Transferred comes after Booked. A call that only booked is Booked, and one that did neither is Answered.

The console decides each session's outcome when it reads the session, from the agent's list as it is then. So a change relabels the agent's past sessions too, as far back as their events go. Two kinds of session show no outcome at all: one from a deployment on a kit before 0.35.0, which does not report when a session starts, and a call whose end has not arrived yet.

The defaults

An agent is on its defaults until a list is saved for it. They are worked out each time they are read, never written, and come from the agent's deployment alone:

  1. The fallback, Answered.
  2. Then one rule per entry in deployment.outcomes, the ordered list the deployment's describe document announces from kit 0.35.0, in its order, with its when as given.
  3. Each entry is kept only when the agent runs the tool behind it on at least one channel: its toolId, or with none, the tool its when names.

On a kit 0.35.0 deployment whose project maps book_appointment to booked, and whose lifecycle names its end-call, voicemail and transfer tools, an agent that runs all five tools has these defaults:

{
  "outcomes": {
    "fallback": "Answered",
    "rules": [
      { "name": "Call ended", "when": { "event": "agent_ended" } },
      { "name": "Booked", "when": { "tool": "book_appointment" } },
      { "name": "Escalated", "when": { "tool": "escalate_to_human" } },
      { "name": "Voicemail", "when": { "event": "voicemail" } },
      { "name": "Transferred", "when": { "event": "transferred" } }
    ]
  },
  "source": "defaults"
}
RuleDetail
NamesFrom the announced outcome: underscores become spaces and the first letter is capitalised, so message_taken becomes "Message taken"
Each name onceAn outcome whose name is already in the list, ignoring case, is left out, so the first one announced keeps it
At most 30The defaults never pass the limit, so they can always be saved as they are
Which tools the agent runsIts tool choices, by the kit's rule. On opt-in tools, each channel's locked tools and its enabledTools. Otherwise, each channel's tools minus its disabledTools, with locked tools always on
Where the description comes fromThe copy the console stored when it last read the deployment, never a live read. The console works its defaults out from the same copy, so both show an agent the same list. After a deploy, the defaults follow the new build once the console has read its description, which refreshing the deployment in the console does
No descriptionAn agent that names no deployment, or whose deployment has no description stored, has the fallback alone. So does one served by a kit before 0.35.0, which announces no outcomes

Nothing is built in. Every rule in the defaults is one the deployment announced. A locked tool always runs, so the kit's own escalation tool gives Escalated to every agent of a deployment that announces it.

Read the outcomes

GET /agents/{slug}/outcomes
curl -H "Authorization: Bearer $SUBCORE_API_KEY" \
  "https://api.subcore.ai/agents/clinic-one/outcomes"

The answer is { "outcomes": Outcomes, "source" }: the saved list with source: "stored", or the defaults with source: "defaults".

Replace the outcomes

PUT /agents/{slug}/outcomes

The body is an Outcomes object, { "fallback", "rules" }, and nothing else.

curl -X PUT -H "Authorization: Bearer $SUBCORE_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.subcore.ai/agents/clinic-one/outcomes" \
  -d '{
    "fallback": "Answered",
    "rules": [
      { "name": "Hung up", "when": { "event": "hung_up" } },
      { "name": "Booked", "when": { "tool": "book_appointment" } },
      { "name": "Message taken", "when": { "tool": "take_message" } },
      { "name": "Voicemail", "when": { "event": "voicemail" } },
      { "name": "Transferred", "when": { "event": "transferred" } }
    ]
  }'

The rules:

  • It replaces the whole list. A rule you leave out is removed, so start from the list in force and send it back with your change.
  • Every name is stored trimmed, and the answer is the list as stored, with source: "stored".
  • A stored list no longer follows the deployment, even one equal to the defaults. DELETE puts the agent back on them.
  • A list is refused whole, by its first problem, and nothing is written. The answer is 400 INVALID_OUTCOMES, and its message names the field, such as "rules[2].name" is "booked", which "rules[0]" already names: names are unique, ignoring case. A list is refused when:
    • fallback or a name is not 1 to 40 characters once trimmed, or holds a NUL character or an unpaired surrogate. Characters are counted as the database counts them, so an emoji is one;
    • rules is not a list, or holds more than 30 rules;
    • a rule is not exactly { "name", "when" };
    • two names are the same, ignoring case and the space around them;
    • a when is not exactly one condition.
  • A body that is not JSON is 400 INVALID_JSON, one that is not an object is 400 INVALID_BODY, one with a field other than fallback and rules is 400 UNKNOWN_FIELD, and one over 32 KiB is 413 PAYLOAD_TOO_LARGE.

PUT /agents/{slug} does not take outcomes. Sent there, it is 400 UNKNOWN_FIELD.

Saves here are last-write-wins. The console's editor checks that the list has not changed since it loaded it, and this route does not, so a PUT replaces whatever is stored, an edit made in the console a moment earlier included.

Go back to the defaults

DELETE /agents/{slug}/outcomes
curl -X DELETE -H "Authorization: Bearer $SUBCORE_API_KEY" \
  "https://api.subcore.ai/agents/clinic-one/outcomes"

Removes the saved list, so the agent is on its defaults again, and answers 200 with those defaults and source: "defaults". An agent already on them gets the same answer, so a repeated call is safe.

In the console

The console edits the same list, under Configuration → Categories → Outcomes, for org admins. Its editor offers only tool conditions, and only for the tools the agent's deployment describes. A rule on an event, stored here or arriving through the defaults, is kept and shown there as a sentence, such as "When the caller hangs up". Reset to defaults in its header does what DELETE does.

On this page