Agent launchers
An agent pane is a collapsible chat that sits beside an editor. Every pane, and the command bar, lists the agents enabled for that surface – one launcher tile per agent, with its own name, picture, and a line saying what it's for.
Where an agent appears is part of the agent, not a separate list to keep in sync with it.
- Enabling an agent on a surface
- Surfaces
- The launcher tile
- Which agent is running
- Caller names
- Closing a conversation
- What reaches the conversation
Before 12.0 this was configured on an agent.pane
toolbar whose items each had to name the hosts they appeared
in. That toolbar is gone, and an upgrade removes it. The name survives as the
caller name a pane posts.
Enabling an agent on a surface
An agent is enabled on a surface by having a block for it under components: in the agent's own configuration, edited in the AI tab of its worker record.
components:
automation:
description: Help writing and debugging automations
mail_reply:
description: Draft a reply from the conversation so farAuthoring the block is the opt-in. An agent with no block for a surface isn't enabled there, so adding a new surface to Cerb never turns every existing agent loose on it. A block with no body is valid and means "yes, here, with the defaults".
A surface with no agent enabled on it hides its agent toggle entirely -- it doesn't render a disabled control or an empty pane. Until at least one agent has a block for that surface, there is no agent affordance there at all.
Turning a surface off is disabled@bool: yes rather than deleting the block, so everything you configured for that surface is still there when you turn it back on:
components:
worklist:
disabled@bool: yesSee Everywhere, and per surface for how a surface's settings combine with the agent's own.
Surfaces
| Surface | component |
|---|---|
| Automation editor | automation |
| Automation Scripting Tester | automation_scripting |
| Command bar | commandbar |
| Data Query Tester | data_query |
| Icon Builder | icon |
| Mail Reply | mail_reply |
| Mail Routing | mail_routing |
| Worklist search fields | worklist |
component is functional, not informational. It's what llm.agent: resolves the surface's own UI-command tools from, and which of the agent's per-surface overrides apply.
An agent chat gets its surface's commands as tools automatically, and can drive the surface directly with the uiCommand element. Both come from the interaction.worker.agent trigger, so build a chat that drives its surface on that trigger rather than on interaction.worker. See Hosts for the tools each one offers.
What a surface contributes
Setup » AI » Surfaces lists one card per surface, showing what that surface hands an agent at runtime. It's read from the surface catalog itself rather than written by hand, so it can't drift from what an agent is actually given.
Each card carries:
- The surface's
componentvalue, what it does, and the default launcher text its tile carries. - Tools – the commands this surface contributes, each marked Browser or Server by what answers it, with its parameters and which are required.
- In the transcript – the two labels each tool shows while it runs and after it finishes, so you can match a line in a chat back to the tool that wrote it.
- Bound parameters, shown as "Always sent, never asked of the model". These are pinned by the surface: the model never sees them and can't override them. A search bar's
cerb_set_querypinskey = query, because a field with exactly one writable target is an argument a model can only get wrong. - Reading – the documentation and skills the surface points the agent at.
- Instructions – the surface's own orientation prompt, which is appended to the agent's.
The Reading block uses exactly four kinds of line, across every surface:
| Line | Meaning |
|---|---|
| A mandatory volume | Read before doing a named kind of work – "Before it will write or edit a search query, it reads search-queries." |
| Conditional volumes | Read when the agent needs them – "Reads when it needs one: records" |
| A Documentation pointer | A path into the mounted docs, such as references/docs/search.md |
| The mounted-only caveat | Each of the above appears only when the volume holding it is actually mounted on the agent |
That last one is the important one: a surface's reading list is a description of what's available, not a guarantee. An agent with no filesystems mounted is pointed at nothing, because sending an agent after files it can't read is worse than saying nothing.
Under Instructions, each card can expand Show this surface's role – the full system prompt that surface contributes. It's already in the page, so expanding it costs nothing, and it's the authoritative copy: read it there rather than from documentation that would go stale the next time the prompt is tuned.
The launcher tile
Each tile is derived from the agent record rather than authored, so there's nothing to keep in sync:
| What it shows | Where it comes from |
|---|---|
| The name | The agent's worker name. In the command bar, its @handle instead |
| The picture | The agent's own avatar |
| The second line | The surface block's description:, falling back to a sentence true of any agent running there |
| The chat | The surface block's automation:, falling back to the agent's own, falling back to the built-in chat |
The command bar labels a row with the @handle on purpose: the bar mixes agents with every other shortcut in Cerb, so a row should read as the agent you'd @mention anywhere else rather than as the name of the script behind it.
Which agent is running
A launcher passes the agent it stands for as a reserved input named agent, which is consumed and removed before the automation's own inputs: are validated.
agent is reserved. An automation that declares it in its own
inputs: block fails to run with Unknown inputs: agent.
A script reads the agent from scope instead. The interaction.worker.agent trigger provides agent_*, the AI worker the chat runs as, with key expansion – so agent_name and agent__image_url are the agent's own name and avatar.
Pass it on rather than hardcoding an agent:
await:
elements:
agentPrompt/prompt_agent:
agent@key: agent_idagent@key: agent_id is the form for both llm.agent: and agentPrompt. Which agent is running belongs to the trigger's state, exactly as the active worker does, rather than to anything a script configures – and that's what lets one interaction serve every agent instead of being copied per agent.
Caller names
Two caller names are in play, because the command bar isn't an agent pane:
| Surface | Caller name |
|---|---|
| The six editors, and worklist search fields | agent.pane |
| Command bar | cerb.toolbar.global.menu |
Both are resumable. A parked conversation started from a pane resumes into the same surface it was started on; one started from the command bar resumes there.
Leave callers: unset on an agent chat. An automation with no
callers: block allows every caller, which is what you want here. Naming only
agent.pane is the tempting mistake: it loses the command bar and it breaks
resuming the chat from History. If you do declare a callers: block, it has to
allow both names.
The shipped default chat (cerb.ai.agent.chat) declares no callers: block for exactly this reason. Its whole policy is the command it needs:
commands:
llm.agent:
allow@bool: yesValues available to a policy
A launcher is rendered through the toolbar machinery, so an agent chat's policy is evaluated against a dictionary before its tile is drawn. A policy that denies hides the tile.
| Placeholder | Notes |
|---|---|
caller_name |
The caller posting the launcher |
component |
The surface the pane is mounted on |
worker_* |
The current worker |
worklist_* |
On worklist surfaces only – see Worklist search fields |
This is not audience control. A policy decides whether a tile is drawn, not who may run the agent. There is no per-agent permission model yet, so don't rely on a policy to keep an agent away from a worker who can open a pane.
Closing a conversation
A pane has exactly one conversation control: the Close button in the chat header. Pausing and ending aren't separate buttons – they're the two answers to what Close asks. The button itself never terminates a conversation.
Clicking it opens a small menu with two choices:
| Choice | What it does |
|---|---|
| Continue later | Parks the conversation and gives it the name it will wear under Recent conversations |
| End | Closes the conversation out. It stops being resumable |
How the menu behaves matters as much as what it offers:
- Escape parks the conversation – the same as choosing Continue later. It's captured ahead of the menu's own handling and ahead of anything still focused in the panel, so the reflex key is the safe answer rather than a way to lose work.
- Enter also picks Continue later, since the first item is pre-highlighted and focus is taken off the composer when the menu opens.
- Clicking outside dismisses without choosing, and nothing happens to the conversation at all.
When the pane is floating rather than docked in the sidebar, the chat header is hidden – the dialog's own titlebar already carries a close – and the same prompt hangs off that instead. The wording and icons are identical to the prompt a chat opened from the command bar gives you, deliberately: closing a chat shouldn't mean one thing in the sidebar and another in a popup.
With no conversation running, Close just collapses the pane, as it always did. The prompt only appears when there's something resumable to make a choice about, so an unfinished run or a non-resumable await closes silently rather than offering a choice that wouldn't stick.
Recent conversations
The pane's picker lists your Recent conversations for that surface. Each tile has its own dismiss button, labeled End this conversation – it's per-conversation, not a bulk clear, and it's a separate button rather than a gesture on the tile because the tile's whole surface already means resume this.
Ending a conversation doesn't delete it. It marks the conversation finished so it drops out of the resumable list -- nothing more. The transcript and its token accounting are kept and stay readable by an administrator at Setup » AI » Transcripts. Ending is tidying the sidebar, not erasing the record of what was said.
Pause is parking, plus a name
Continue later changes no state. The conversation was already parked the moment the pane closed – the await it was sitting on re-emits its own state either way. What the choice adds is the label the conversation will carry in the list, so you can tell three parked chats apart later. Only End is a state change.
That's also why a parked pane conversation doesn't reappear in the command bar: it comes back under the pane's own Recent conversations, on the surface it was started from.
What reaches the conversation
A running conversation reads its surface from caller_params:
| Key | Notes |
|---|---|
component |
The surface, one of the eight values above |
ui_capabilities |
The commands that surface answers |
Those two are the whole set on seven of the eight surfaces. A worklist search bar adds two more.
Which page the worker is on is deliberately absent. It would be a snapshot taken at
launch and would go stale the moment they navigate. The command bar's
cerb_get_page tool reads it live instead.
For the same reason, don't interpolate a changing value into a chat's system_prompt:. The composed prompt is built on the first turn and reused unchanged after that – it's rebuilt only when you edit the automation – and a value that changes on its own rebuilds it every turn, throwing away the most cacheable part of the request. Your system_prompt: is appended to the prompt Cerb composes for the surface, rather than replacing it, so it's for what you want to add.
Command bar
The command bar is the one surface that isn't tied to a screen at all. It follows the worker from page to page, so it has no document to read and everything it offers is one-way: it can put something in front of a worker, but nothing comes back through the bridge. An agent there knows where they are, never what's on the screen, and never what a search it opened matched.
The command bar's ordinary non-agentic shortcuts are unaffected. Those stay interaction.worker items and keep working alongside a chat.
Worklist search fields
A search field's agent is the one surface that isn't an editor. This covers more than the search bar above a worklist: the same field appears in popups throughout the interface – the quick-search popup, record choosers – and every one carries the same agent under the same component, with no state var distinguishing them.
A worklist launcher's policy additionally receives:
| Placeholder | Notes |
|---|---|
worklist_id |
The worklist being searched |
worklist_record_type |
The record type alias (ticket) – what data.query expects after of: |
worklist_record_context |
The record type's context ID |
worklist_query |
The current search query |
worklist_query_required |
The worklist's required query, which a search can't escape |
worklist_page |
The current page |
worklist_limit |
Rows per page |
Only two of those reach the running conversation, as caller_params.worklist_id and caller_params.worklist_record_type. The rest exist for the policy that decides whether the tile is drawn.
An agent reads what it's actually looking at – including the current query – with the surface's own cerb_get_fields tool, which stays current as the worker keeps typing. See Worklist search fields.