Actors in chat
A Staff actor can be a member of a chat space. It gets its own chat key held on the server, shows with an actor badge, and answers when someone addresses it. Its model, instructions, tools and budget are its own agent config; the chat only decides when it runs and where the answer goes.
How a reply happens
person posts ──► relay ──► bridge (next to the relay, one connection per actor)
│ only mentions, DMs and the actor's channels
▼
platform checks: real signed message, author is a member,
not a duplicate, under the reply ceiling, actor still eligible
▼
the actor runs (its own session, its own tools)
▼
answer posted in the thread, signed with the actor's key
The bridge filters on the relay side, so chatter nobody addressed to an actor never starts a run. An actor never answers another actor, and never answers its own messages.
Attach an actor
1. Prepare the actor
- Hire it and give it an agent config with a model, instructions and budget. See Hire an actor.
- Declare that it acts as the app. A chat reply in the default mode runs as the actor's end-user seat in this identity app, so the actor's stored definition must name the app (
on_invoke.input_data.act_as_identity_app_uuid). Without it, attach refuses withactor_act_as_not_declared. The exception is internal mode. - Give it an identity. An actor needs its agent identity before it can hold a seat. If binding fails with
ACTOR_IDENTITY_MISSING, mint its token first (see Identity and tokens).
2. Bind it to an end-user seat
start_workflow("identity.end-user.bind-actor", {
"identity_app_uuid": "<app>",
"staff_actor_uuid": "<actor uuid>",
"reason": "Support assistant in the app chat"
})
This takes one paid end-user seat in the app. A person in your organization must run it.
3. Attach it to the space
start_workflow("buzz.actor.attach", {
"space_uuid": "<space uuid>",
"staff_actor_uuid": "<actor uuid>",
"display_name": "Acme Assistant",
"triggers": {"mention": true, "direct_message": true, "channels": ["<channel uuid>"]},
"max_replies_per_hour": 30
})
The actor joins the listed channels as a bot, publishes its chat profile, and the output says bridge_sync_required: true.
4. Start or refresh the bridge
start_workflow("buzz.bridge.sync", {"space_uuid": "<space uuid>"})
buzz.bridge.sync runs the bridge next to the relay with the attached actors' keys, and gives the bridge a token that can only deliver chat messages. The token is approved by you, so a person must start it. Run it again after every attach, pause, resume or detach. With no active actors left, it scales the bridge to zero and revokes the token. Pass rotate_token: true to replace the token.
In the console, the Chat → Members → Actors tab (Atores) does steps 3 and 4 and shows each actor's status.
Triggers
| Trigger | The actor answers |
|---|---|
mention: true | A message that mentions it, in any channel it can see |
direct_message: true | Every message in a DM with it |
channels: ["<channel uuid>", ...] | Every message in those channels |
An actor added to a channel with buzz.channel.set-member, or by a member from the chat page, listens there too but answers only mentions and DMs.
The reply ceiling
max_replies_per_hour (default 60) bounds how much one actor writes. It counts chat replies and messages the actor starts from outside the chat together. A message over the ceiling is recorded as skipped with rate_limited and gets no answer. Every reply spends your organization's model budget, so the ceiling is the first guard; the actor's own budget is the second.
Pause, resume, detach
buzz.actor.set-status with space_uuid, attachment_uuid and status (active, paused, detached). Detaching also removes the actor from the relay. Run buzz.bridge.sync afterwards.
Seat mode: what the actor can use
By default an attachment replies in seat mode. The actor runs as its end-user seat in the app, which means:
- its tools are only the workflows your app exposes to end users, the same compositions a signed-in person could run. Organization skills and MCP servers are not loaded;
- it sees data the way an end user of the app would, not the way your organization does;
- it has no memory of earlier conversations with a person (see Limits).
That is the right shape for a customer-facing assistant. For an assistant that answers your own team with organization tools, use internal mode.
DGI responders: answers with cards
By default an actor answers with its Staff configuration, in text. Set its responder to dgi and DGI answers instead, with forms, confirm cards, tables with row actions, charts, KPI tiles, logs, links and live cards, built on the workflows you allow. hybrid tries DGI first and hands the turns it cannot handle to the Staff configuration.
start_workflow("buzz.actor.set-responder", {
"space_uuid": "<space uuid>",
"attachment_uuid": "<from data.buzz.attachment.list>",
"responder": "dgi",
"allowed_workflow_types": ["<a workflow your app exposes>"]
})
What changes with a DGI responder:
- DGI runs as the person who wrote the message, not as the actor's seat, so the actor does not need to declare that it acts as the app (a
hybridactor still does, for its handed-off turns). - Writes need a confirm card. A typed "yes" never confirms.
- Most turns need no LLM: the Jev decision engine routes them. The sections below on progress lines and the tool trace describe Staff answers.
- An organization admin or owner sets it, and no
buzz.bridge.syncis needed.responder: "staff"switches back.
Structured chat is part of DGI (Alpha). Start at Structured chat with DGI, and see the responder reference for every setting.
What the actor is given
For each message, the actor receives the message, the author's display name, the conversation so far (up to 20 earlier messages of the thread or DM, with its own lines marked), the files attached to the message as URLs, and the space's locale. It is told it is replying in a chat, so it answers the person instead of writing a report.
While it works: progress lines
Answers usually take fifteen to thirty seconds. People see:
- a thinking line under the conversation as soon as they address the actor;
- after about five seconds, a short progress line that changes as the run moves: preparing, thinking, "checking list orders", writing the answer. It is one message edited in place, at most five updates, never counted as unread, never notified, and removed when the answer arrives.
Answers arrive complete, not token by token.
On the answer: the tool trace
An actor's answer can carry a collapsed "how I got here" panel: the run time, how many tool calls ran and failed, and one row per tool (up to eight) with its count and duration. It shows tool names and numbers only. Arguments, outputs, error messages and prompts are never on the message. "No tool was used" is shown as such.
Quick replies
An actor may offer up to five short options (48 characters each) under its answer. Tapping one posts the label as the person's own message in the same thread, so it works exactly like typing it. The buttons show only on the newest message of the thread and disappear once the person writes. A client that ignores them loses nothing: the answer stands on its own.
Handing off to a person
When the actor cannot help, the conversation reaches your team instead of ending.
- The ask. A person writes a line such as "talk to someone", "talk to a human", "falar com alguém" or "hablar con una persona" (English, Portuguese and Spanish are all recognized, accents and case ignored), or taps the "talk to someone" option an actor offers.
- The ticket. Instead of running the actor, the platform opens one ticket per conversation in your organization's Tickets, labelled
chatandchat:handoff, severity high. It carries the space, channel, thread, who asked, which actor, and the excerpt the actor saw. Asking again in the same conversation adds a comment to the same ticket. - The team. The first time, it also raises the Staff event
chat.incident.raised, so an actor you subscribed to that event (for example a support manager) wakes up. - The confirmation. The actor posts in the thread that a person was asked, with a short ticket reference.
The person never gets access to your tickets; the handoff runs with the space's organization. If an actor's run fails or comes back empty, its reply says it could not answer (with a short code) and offers the "talk to someone" option, so a failure is never a dead end.
Several actors in one conversation
You can attach more than one actor to a space. The platform keeps them polite:
| Situation | Who answers |
|---|---|
| A message mentions two actors | Both, as two separate replies in the thread |
| A message mentions one actor | Only that one, even inside another actor's thread |
| An unaddressed reply in a thread | The actor whose answer it replies to |
| An actor writes | No actor answers it, so actors never loop |
| Several actors see a handoff request | One ticket, one confirmation |
Each actor is told the names of the other actors in the space and which ones the message addressed.
Messages an actor starts
An attached actor that runs for another reason (a schedule, an event, another workflow) can write first, signed with its own key.
| Workflow | Inputs | Does |
|---|---|---|
buzz.actor.notify | space_uuid, target, content, mention, as_actor_uuid | The simple one. target is #channel, a channel name, @Display Name, a display name or a member uuid. A person gets a DM (opened if needed), a channel gets a post. Ambiguous or unknown names fail with a short list of candidates |
buzz.actor.dm-open | space_uuid, member_uuid (the actor's member), participant_member_uuids (1 to 8) | Opens or reuses a DM and returns its channel_id |
buzz.message.post | space_uuid, member_uuid (the actor's member), channel_id, content, reply_to, thread_root, mentions, ui | Posts in a channel or DM, optionally in a thread and with mentions. ui adds a card: a read view, or a form for one person. See Proactive cards |
buzz.actor.post-view | space_uuid, attachment_uuid, channel_id, workflow_type, input, kind, live | Posts a refreshable read card from one read in the actor's DGI scope. Schedulable. Needs a DGI responder |
start_workflow("buzz.actor.notify", {
"space_uuid": "<space uuid>",
"target": "#orders",
"content": "3 orders are waiting for confirmation"
})
| Caller | May speak as | Where |
|---|---|---|
| The actor itself (its own session) | Only itself, and only while its attachment is active | Channels it is in |
| A person in your organization, or an integration key | Any attached actor of the space, passing as_actor_uuid to notify | The actor's channels, or any open channel |
| An end user, or an agent that is not a Staff actor | Nobody |
These posts count against the actor's reply ceiling. To let an actor use them, give it a workflow skill for the type, for example a skill bound to buzz.actor.notify. They are not available inside a seat-mode chat reply, which runs as an end user.
Watching what actors do
Two reads, both for organization admins and not available to agents:
| Read | Shows |
|---|---|
data.buzz.attachment.list with space_uuid | Each attached actor: name, triggers, status, reply ceiling, replies in the last hour, and its responder (Staff, or DGI with its settings) |
data.buzz.inbound.list with space_uuid, and optionally status, staff_actor_uuid, limit | Every message that woke an actor, newest first: dispatched, replied, skipped or failed, with a reason code. No message content is stored or returned |
Reason codes you will see: rate_limited, duplicate, actor_to_actor_ignored, addressed_to_other_actor, thread_owned_by_other_actor, handoff_requested, actor_act_as_not_declared, and actor_<code> when the actor's seat is no longer eligible. The console's Activity tab (Atividade) shows the same ledger.
Ask your AI assistant
Which actors are attached to my chat space and what wakes each one? Use data.buzz.attachment.list.
Show the last 20 messages that woke actors in my chat space with data.buzz.inbound.list, grouped by status and reason code.
Attach the Staff actor "<actor name>" to my chat space so it answers mentions and DMs, with at most 30 replies per hour. Check that it is bound to an end-user seat first with identity.end-user.actor-binding.get, show me every call, and remind me to run buzz.bridge.sync afterwards.
As the actor "<actor name>", send "Your weekly report is ready" to the #reports channel of my chat space with buzz.actor.notify. Confirm before sending.
For AI agents
| Rule | Detail |
|---|---|
| Order | Seat binding (identity.end-user.bind-actor), then buzz.actor.attach, then buzz.bridge.sync. The last two need a person |
| Check the seat | identity.end-user.actor-binding.get before attaching |
| Speaking first | As an actor, call buzz.actor.notify with a target name. Do not pass organization_uuid or as_actor_uuid for yourself |
| Ceiling | A rate_limited failure means the actor spent its hour. Wait; do not retry in a loop |
| No content | The inbound ledger has no message text. Do not claim to read a conversation from it |
| Handoff | End users never call ticket.*. The platform opens the ticket |
| DGI responder | buzz.actor.set-responder needs an admin or owner signed in as a person, and a non-empty allowed_workflow_types. Prepare the call; do not start it as an agent |
Using the chat
What people get on the hosted chat page. DMs, threads, mentions, reactions, edit and delete, history, search, pinned messages, unread counts, attachments, missed-message email and the mobile layout
Internal support actor
Internal mode lets an attached actor answer your own team with your organization's tools instead of the app's end-user workflows, for an allowlist of people in a space where everyone is on that list
