1. Feedback Actions
  2. Update From Voice

Feedback Actions

Update From Voice Feedback Action

Explains the Voice feedback action that runs during and after a live AI voice call. Useful for handling requests from the voice assistant, keeping the conversation history, wrapping up when the call ends, and getting the call recording.

Overview

A live voice call is started with the Start Voice Chat task. While the call runs, and once when it ends, the _update_from_voice feedback action is triggered on the stub.

stubpost.data._incoming_voice_data.event tells you why the action ran:

event delegated When
delegation true The assistant needs the stub's help, eg to look something up or make a booking
interval false Every update_interval_seconds of the call, when there is new transcript. A delegation restarts the interval
session_ended false Once, when the call has ended, however it ended
recording_ready false Once, after session_ended, when the recording of the call is ready. The recording is attached to the stubpost
session_transferred_out false On the stub that had the call, when another stub took it over with Voice Session Transfer
session_transferred_in false On the stub that took the call over, with the whole conversation so far

Each update only carries what was said since the previous update. A sentence that was still being said when an update was sent continues in the next one.

The transcript since the previous update is set to the message of the stubpost, i.e. stubpost.message, as lines like:

        Caller: Hi, do you have a table for two this Thursday at seven
Assistant: Let me check that for you.

      

For session_ended, the message starts with a Voice session ended: <reason> line, and for the transfer events with a Voice session transferred to <stubref> or from <stubref> line. For recording_ready, the message is Voice call recording ready.

All the details are available in the stubpost.data._incoming_voice_data data path.

NOTE

A delegation doesn't say what the assistant needs. Work it out from the messages, eg by running a GPT Chat Task with them.

Handling the events

delegation

  1. Run a GPT Chat Task with the messages.
  2. When its answer arrives, send it to the call with a commentary voice notification and the delegation_id:
loading...

While it waits, the assistant tells the caller it is checking.

interval

Store the messages in the chat history, so the agent has the whole conversation the next time it runs.

session_ended

Wrap up, eg save a summary of the call or send a follow-up. session_ended.reason tells you how the call ended.

The charge for the length of the call comes with this update, and is added to the stubpost's transaction history like the charges of tasks.

After a transfer, the stub that started the call always gets session_ended, with the charges for the whole call. The stub that had the call when it ended gets session_ended too, first, with the last of the transcript and without the charges. session_ended.ended_on_stubref says which stub had the call.

session_transferred_out and session_transferred_in

Another stub took the call over with Voice Session Transfer. The caller stays on the line.

The stub that had the call gets session_transferred_out, with the last of what was said while it had the call. Wrap up its part, eg note the handover. From now on its voice notifications and Voice Commands fail with voice_session_transferred.

The stub that took the call over gets session_transferred_in. messages is empty, the whole conversation so far is in conversation, ready to keep as the chat history. delegation_id is the latest delegation, usually the request that caused the transfer, so answer it with a commentary voice notification.

recording_ready

Every call is recorded. Shortly after the call ends, the recording is uploaded and arrives with one more _update_from_voice, attached to the stubpost: it's in stubpost.attachments, like the attachments of an email or a WhatsApp message.

The recording is a stereo WAV file: the caller on the left channel, the assistant on the right. Phone calls are recorded at 8 kHz telephone quality, webchat and WhatsApp calls at 24 kHz.

Use it like any other attachment, eg keep it with the stub, or pass recording.fileuuid to a task that transcribes or analyses audio:

        "conditions": ["stubpost.data._incoming_voice_data.event = 'recording_ready'"]

      

recording.truncated is true when the recording stopped early, eg the voice service ran low on disk space or restarted during the call. The recording then has the call up to that point.

NOTE

When a call couldn't be recorded at all, no recording_ready follows. Instead, a Voice call recording failed event is added to the stub, under the stubpost that started the call.

Data Structure

Feedback Action Data

Example data that is passed to the _update_from_voice feedback action for a delegation:

        "_incoming_voice_data": {
  "event": "delegation",
  "delegated": true,
  "voice_session_uuid": "ae2ca8c6-ddf3-4840-9255-d9520a9cac7e",
  "chat_name": "Restaurant booking",
  "channel": "webchat",
  "messages": [
    { "role": "assistant", "content": "Hi, this is the restaurant assistant. How can I help?" },
    { "role": "user", "content": "Hi, do you have a table for two this Thursday at seven" }
  ],
  "transcript": [
    { "speaker": "assistant", "delta": " Hi,", "start_ms": 1000, "end_ms": 1200 },
    { "speaker": "assistant", "delta": " this", "start_ms": 1200, "end_ms": 1400 }
  ],
  "delegation_id": "item_ETSXQ2Sev4SEHt5NNoRmx",
  "offset_ms": 6200
}

      

When the call has ended:

        "_incoming_voice_data": {
  "event": "session_ended",
  "delegated": false,
  "voice_session_uuid": "ae2ca8c6-ddf3-4840-9255-d9520a9cac7e",
  "chat_name": "Restaurant booking",
  "channel": "webchat",
  "messages": [
    { "role": "assistant", "content": "We're closing the line now, goodbye." }
  ],
  "transcript": [],
  "session_ended": {
    "status": "closed",
    "reason": "stub_hangup",
    "usage_seconds": 52,
    "usage_confirmed": true
  }
}

      

When the recording is ready:

        "_incoming_voice_data": {
  "event": "recording_ready",
  "delegated": false,
  "voice_session_uuid": "ae2ca8c6-ddf3-4840-9255-d9520a9cac7e",
  "chat_name": "Restaurant booking",
  "channel": "webchat",
  "recording": {
    "fileuuid": "ad85f8be-337f-5655-b828-dc889af461dc",
    "duration_seconds": 121,
    "format": { "type": "audio/pcm", "rate": 24000 },
    "channels": { "left": "caller", "right": "assistant" },
    "truncated": false
  }
}

      

The same file is in stubpost.attachments:

        "attachments": [
  {
    "fileuuid": "ad85f8be-337f-5655-b828-dc889af461dc",
    "filename": "voice-call-ae2ca8c6-ddf3-4840-9255-d9520a9cac7e.wav",
    "originalname": "voice-call-ae2ca8c6-ddf3-4840-9255-d9520a9cac7e.wav",
    "contentType": "audio/x-wav"
  }
]

      

When another stub took the call over, on that new stub:

        "_incoming_voice_data": {
  "event": "session_transferred_in",
  "delegated": false,
  "voice_session_uuid": "ae2ca8c6-ddf3-4840-9255-d9520a9cac7e",
  "chat_name": "Sales call",
  "channel": "whatsapp",
  "messages": [],
  "transcript": [],
  "conversation": [
    { "role": "assistant", "content": "Hi, this is the sales assistant. How can I help?" },
    { "role": "user", "content": "Actually I need help with my router" }
  ],
  "delegation_id": "item_ETSXQ2Sev4SEHt5NNoRmx",
  "session_transferred": {
    "from_stubref": "2026-10-15-STUB-SALE",
    "initiating_stubref": "2026-10-15-STUB-SALE",
    "transferred_at": "2026-10-15T10:00:00.000Z"
  }
}

      

Properties

event
string

Why the action ran: delegation, interval, session_ended, recording_ready, session_transferred_out or session_transferred_in.


delegated
boolean

true when the assistant needs the stub's help, i.e. when event is delegation.


voice_session_uuid
string

The id of the voice call, from Start Voice Chat.


chat_name
string

The chat_name the call was started with.


channel
string

The channel of the call: webchat, whatsapp or dial (phone calls).


messages
array

What was said since the previous update, as chat messages. The caller is user and the assistant is assistant. This is the same format as the GPT Chat Task messages. Not set for recording_ready.

Empty when nothing new was said, eg for a session_ended straight after a delegation.


transcript
array

The timed fragments behind the messages. Each fragment has a speaker (caller or assistant), the delta text, and start_ms and end_ms, the time in milliseconds from the start of the call. Not set for recording_ready.


delegation_id
string

delegation and session_transferred_in. Pass it back with the answer, so the assistant knows which request it answers. On session_transferred_in it's the latest delegation on the call, usually the one that asked for the transfer.


offset_ms
number

delegation only. When the assistant delegated, in milliseconds from the start of the call.


session_ended.status
string

session_ended only. closed, or abandoned when the caller's channel never joined the call.


session_ended.reason
string

session_ended only. How the call ended:

reason Meaning
coms_hangup The caller hung up
coms_disconnected The caller's connection dropped
stub_hangup The template ended the call with hangup
join_timeout The caller's channel didn't join within 60 seconds
expired The call reached the 120 minute limit
content A safety filter ended the call
connection_lost / remote_hangup The connection to the AI provider was lost
gateway_shutdown The voice service restarted during the call

session_ended.usage_seconds
number

session_ended only. The billed length of the call in seconds. Time before the caller joined isn't billed.


session_ended.usage_confirmed
boolean

session_ended only. true when the AI provider confirmed the final usage_seconds. When false, usage_seconds is the last known value.


session_ended.ended_on_stubref
string

session_ended only. The stub that had the call when it ended. Differs from the stub getting the update when the call was transferred away from it.


conversation
array

session_transferred_in only. The whole conversation so far, as chat messages, the same format as messages.


session_transferred.to_stubref
string

session_transferred_out only. The stub that took the call over.


session_transferred.from_stubref
string

session_transferred_in only. The stub that had the call.


session_transferred.initiating_stubref
string

session_transferred_in only. The stub that started the call. It's charged for the call and gets session_ended with the charges.


session_transferred.transferred_at
string

The transfer events. When the call was transferred, as an ISO 8601 date-time.


recording.fileuuid
string

recording_ready only. The recording's file, the same as in stubpost.attachments.


recording.duration_seconds
number

recording_ready only. The length of the recording in seconds.


recording.format
object

recording_ready only. The audio format of the recording, the same as the call's: audio/pcm at 24000 for webchat and WhatsApp calls, audio/pcmu or audio/pcma at 8000 for phone calls.


recording.channels
object

recording_ready only. Which side of the call is on which stereo channel: the caller on the left, the assistant on the right.


recording.truncated
boolean

recording_ready only. true when the recording stopped before the call ended.

Call state

_update_from_voice also carries the state of the stub's voice call in stubpost.data._com_platforms.voice. For the first delegation of a call:

        "_com_platforms": {
  "voice": {
    "active": true,
    "delegation_id": "item_ETSXQ2Sev4SEHt5NNoRmx",
    "voice_session_uuid": "ae2ca8c6-ddf3-4840-9255-d9520a9cac7e",
    "channel": "whatsapp"
  }
}

      

A voice call happens inside the stub's conversation on WhatsApp, webchat or a phone call, so _update_from_voice only sets _com_platforms.voice. The last_incoming_platform and last_from that channel set stay as they are, so replies with auto_platform still go to that channel.

Stub data is merged, so each field is only sent when it changes, and stub.data._com_platforms.voice always has the latest of each.

Use it from any action, eg to answer a WhatsApp message in the call while it's running, and as text otherwise:

        "conditions": ["stub.data._com_platforms.voice.active = true"]

      
voice.active
boolean

true while the stub has a running call. false once the call was told to hang up, with the hangup voice notification or the Voice Command task, on session_ended, and on session_transferred_out, where the call moved to another stub. On recording_ready, which comes after the call ended, it says whether the stub has started another call since.


voice.delegation_id
string

Sent on delegation, and on session_transferred_in with the latest delegation on the call.


voice.voice_session_uuid
string

The call's id, sent on the first update a stub gets for a call: when a new call starts, and on session_transferred_in. Use it from any action on the stub, eg to steer the call with Voice Command or hand it over with Voice Session Transfer.


voice.channel
string

The call's channel, sent with voice_session_uuid: webchat, whatsapp or dial.

JSONSchema

The JSONSchema of the stubpost.data._incoming_voice_data is:

        {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Incoming Voice Data",
  "type": "object",
  "properties": {
    "_incoming_voice_data": {
      "type": "object",
      "properties": {
        "event": {
          "type": "string",
          "enum": [
            "delegation",
            "interval",
            "session_ended",
            "recording_ready",
            "session_transferred_out",
            "session_transferred_in"
          ],
          "description": "Why the action ran."
        },
        "delegated": {
          "type": "boolean",
          "description": "True when the assistant needs the stub's help."
        },
        "voice_session_uuid": {
          "type": "string",
          "description": "UUID of the voice call."
        },
        "chat_name": {
          "type": ["string", "null"],
          "description": "The chat_name the call was started with."
        },
        "channel": {
          "type": ["string", "null"],
          "enum": ["webchat", "dial", "whatsapp", null],
          "description": "The channel of the call."
        },
        "messages": {
          "type": "array",
          "description": "What was said since the previous update, as chat messages.",
          "items": {
            "type": "object",
            "properties": {
              "role": { "type": "string", "enum": ["user", "assistant"] },
              "content": { "type": "string" }
            },
            "required": ["role", "content"]
          }
        },
        "transcript": {
          "type": "array",
          "description": "The timed fragments behind the messages.",
          "items": {
            "type": "object",
            "properties": {
              "speaker": { "type": "string", "enum": ["caller", "assistant"] },
              "delta": { "type": "string" },
              "start_ms": { "type": "number" },
              "end_ms": { "type": "number" }
            },
            "required": ["speaker", "delta"]
          }
        },
        "delegation_id": {
          "type": "string",
          "description": "delegation only. Pass it back with the answer."
        },
        "offset_ms": {
          "type": "number",
          "description": "delegation only. When the assistant delegated, in ms from the start of the call."
        },
        "session_ended": {
          "type": "object",
          "description": "session_ended only.",
          "properties": {
            "status": { "type": "string", "enum": ["closed", "abandoned"] },
            "reason": { "type": "string" },
            "usage_seconds": { "type": "number" },
            "usage_confirmed": { "type": "boolean" },
            "ended_on_stubref": { "type": "string" }
          },
          "required": ["status", "reason", "usage_seconds", "usage_confirmed"]
        },
        "conversation": {
          "type": "array",
          "description": "session_transferred_in only. The whole conversation so far.",
          "items": {
            "type": "object",
            "properties": {
              "role": { "type": "string", "enum": ["user", "assistant"] },
              "content": { "type": "string" }
            },
            "required": ["role", "content"]
          }
        },
        "session_transferred": {
          "type": "object",
          "description": "The transfer events only.",
          "properties": {
            "to_stubref": { "type": "string" },
            "from_stubref": { "type": "string" },
            "initiating_stubref": { "type": "string" },
            "transferred_at": { "type": "string" }
          },
          "required": ["transferred_at"]
        },
        "recording": {
          "type": "object",
          "description": "recording_ready only.",
          "properties": {
            "fileuuid": { "type": "string" },
            "duration_seconds": { "type": "number" },
            "format": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "enum": ["audio/pcm", "audio/pcmu", "audio/pcma"] },
                "rate": { "type": "number" }
              },
              "required": ["type", "rate"]
            },
            "channels": {
              "type": "object",
              "properties": {
                "left": { "type": "string", "enum": ["caller"] },
                "right": { "type": "string", "enum": ["assistant"] }
              }
            },
            "truncated": { "type": "boolean" }
          },
          "required": ["fileuuid", "duration_seconds", "format", "channels", "truncated"]
        }
      },
      "required": ["event", "delegated", "voice_session_uuid"]
    }
  },
  "required": ["_incoming_voice_data"]
}