interaction.worker.agent
The interaction.worker.agent automation trigger extends interaction.worker in two ways: it hands an agent its host editor's commands as tools automatically, and it adds the uiCommand form element for driving that host directly.
It's the trigger for an agent chat that sits beside an editor and can read and write the document in front of it.
Everything from interaction.worker – its inputs, outputs, and form elements – is available here as well, plus agent_*, the AI worker this chat runs as.
The agent running the chat
A launcher tells the interaction which agent it stands for, and the trigger exposes that as agent_* – a worker record with key expansion, so agent_name and agent__image_url are the agent's own name and avatar.
Pass it on rather than naming an agent in the script:
await:
elements:
agentPrompt/prompt_agent:
agent@key: agent_idThe same form works on llm.agent:. Which agent is running belongs to the trigger's state, exactly as the active worker does, and that's what lets one interaction serve every agent instead of being copied per agent. agent_* is unset when no agent launched the interaction.
The launcher passes the agent as a reserved input named agent, consumed
before your inputs: are validated. An automation that declares
agent in its own inputs: block fails to run with
Unknown inputs: agent. Read agent_* from scope instead.
Choosing a trigger
Three interaction triggers look similar from the outside. The difference is who the conversation belongs to, and whether it can reach the editor beside it.
| Trigger | Use it for |
|---|---|
| interaction.worker | An ordinary worker interaction. No UI component access in the browser. |
| interaction.worker.agent | A worker's agent chat that drives the editor it was launched beside. |
| interaction.internal | Automations Cerb runs on its own behalf – record choosers, autocomplete helpers, the dialogs inside the builders. |
interaction.internal exists so a worker can filter Cerb's own plumbing out of an automation worklist and never think about it again. Agent panes are the opposite: they're the worker's own conversations, they park as resumable continuations, and they appear in the pane's History and the command bar's Resume list.
Why a separate trigger
uiCommand round-trips a command to the host editor the interaction was launched beside. That only works when the caller provides a command bridge, so the element is advertised on this trigger alone.
Gating it this way means no caller without the bridge is ever handed a capability it can't fulfill.
You don't have to keep track of it yourself: saving an automation whose await:form: uses an element its trigger doesn't offer is rejected, and the error names the trigger you need.
Tools
A chat on this trigger is handed its host's commands as tools automatically. Nothing in the script declares them: the trigger resolves the host its pane is mounted on and contributes that host's commands to the turn, the same way mounts: provisions an agent's terminal. A chat opened beside the automation editor, a worklist search bar, or the Icon Builder can read and rewrite it with an empty tools: block.
Two families, differing only in who answers the call:
| Family | Answered by | Notes |
|---|---|---|
ui_command/<tool> |
The browser | A round-trip to the host editor, the same bridge a uiCommand element uses |
ui_server/<tool> |
Cerb | Answered on the server with no round-trip – for what the installation knows rather than what's on screen |
The Icon Builder's cerb_list_icons is a server tool: naming an existing icon is most of that job, and the browser has no way to say what the whole set contains.
Your own tools: entries are unaffected. Declare llm.tool automations and hand-written tool/ definitions as usual; they sit alongside the host's. A tool you declare with the same name as one of the host's wins, which is how a host command is overridden.
Because the host contributes them rather than your script copying them, an editor that gains a command reaches every existing chat on its next turn – including chats already saved.
Gating a tool behind a human
An on_tool: branch still runs before every host command, so that's where a call is intercepted. Answer it with tool.return: and the editor is never touched:
on_tool:
decision/tool:
outcome/confirm_write:
if@bool: {{__tool.name == 'set_script'}}
then:
await:
form:
title: Overwrite the script?
elements:
say/warning:
content: The agent wants to replace the whole script.
# Falling through runs the command; `tool.return:` here would answer insteadTool names and command names differ
The name a model calls is not the name the bridge dispatches on, and one isn't derivable from the other. Each host names its tools to read naturally in that place: the Data Query Tester offers setEditorValue to the model as cerb_set_query, while the Scripting Tester offers the same command as cerb_set_script.
Each host's table below lists both. Use the tool name when you're reasoning about what the model sees – a system prompt, an on_tool: branch, a transcript – and the command name in a uiCommand element's command:.
A host can also pin an argument the model never sees. A worklist search bar has exactly one writable field, so its cerb_set_query tool takes value alone and the bridge's key is supplied for it – an enum of one is an argument a model can only get wrong.
uiCommand
A uiCommand element drives a host directly from a form, rather than waiting for the model to call a tool. It's the manual path: automatic tools cover a host's catalog, while uiCommand runs a command of your choosing at a moment of your choosing – when a form opens, after a worker answers a question, or as part of a chat that dispatches commands itself.
The element names a command for the host editor to run, and the key you give it is the return variable:
await:
form:
elements:
uiCommand/current_value:
command: getEditorValue| Key | Notes |
|---|---|
command: |
The name of the host callback to run |
params: |
Optional parameters passed to the callback |
disabled: |
When true, render inert – emit an empty return variable with no round-trip |
Unlike most awaits, a uiCommand fills its value synchronously from the host's callback. It does not submit the form on its own; the form's own submit: posts it along with everything else.
See the uiCommand element reference for the full command vocabulary of each host, the shape of returned values, and the ui_capabilities input that makes one automation portable across editors.
Gating several commands in one form
Because a disabled uiCommand emits an empty return variable instead of round-tripping, one await:form: can carry several commands and let the agent's request decide which actually runs:
await:
form:
elements:
uiCommand/read_result:
command: getEditorValue
disabled@bool: {{__tool.parameters.command != 'getEditorValue'}}
uiCommand/write_result:
command: setEditorValue
params:
value: {{__tool.parameters.value}}
disabled@bool: {{__tool.parameters.command != 'setEditorValue'}}Hosts
A host provides the command bridge. Which agents each host offers is configured on each agent rather than on the host. The hosts today are:
| Host | 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 |
An agent appears on a host by having a block for that host's component under components: in its own configuration – see agent launchers. The host reports its component in caller_params, and that value is functional rather than informational: it's what llm.agent: resolves this host's tools from, and which of the agent's per-host overrides apply.
There's no shared command vocabulary. Each host declares its own, so read ui_capabilities at runtime rather than hard-coding a list – that's what makes one automation portable across hosts. Every return value is a string; commands that need structure return JSON the automation has to decode.
In the tables below, Command is the bridge name a uiCommand element sends and Tool is the name the model calls. The two differ on several hosts. Every built-in tool is named with a reserved cerb_ prefix, so an agent tool you create can never shadow one – a tool record whose name starts with cerb_ is refused when it's saved.
No host exposes results. The search bar can start a search and the testers can set a
query, but neither hands anything back through the bridge. An interaction reads results
server-side on its next turn with a data query instead.
Don't go looking for a getResults.
Caller policy
Two caller names launch these interactions: the six editor panes and worklist search fields post agent.pane, while the command bar posts cerb.toolbar.global.menu.
Leave callers: unset. An automation with no callers: block
allows every caller, which is what an agent chat wants. Naming only agent.pane
is the tempting mistake -- it loses the command bar and breaks resuming the chat from
History. A callers: block here has to allow both names.
Automation editor
component: automation. The only field-keyed code host: key: selects script or policy.
| Command | Tool | Params | Returns |
|---|---|---|---|
getFields |
cerb_get_fields |
JSON {name, description, trigger, script, policy}. trigger is the raw extension ID, not a label. |
|
setField |
cerb_set_field |
key: one of name, description, trigger, script, policy; value: |
ok. Wholesale replace. |
editField |
cerb_edit_field |
key: script or policy; old:; new: |
ok. old: must match exactly once – zero or several matches is an error naming the count. Unfolds the editor and flashes the changed range. |
grepField |
cerb_grep_field |
key: script or policy; query:; limit: (default 25) |
JSON array of {line, path, text}. line is 1-based; path is the KATA key path of the row; text is trimmed and truncated to 200 characters. |
getDiff |
cerb_get_diff |
key: script or policy (default script) |
JSON {key, tracked, hunks} against the last save. See the caveat below. |
changeTab |
cerb_change_tab |
tab: one of run, policy, log, visualization, usage |
ok |
highlightLine |
cerb_highlight_line |
line: (1-based) |
ok. Always the script editor – it takes no key:. |
highlightKey |
cerb_highlight_key |
key: script or policy (default script); path: |
ok. Pairs with grepField, which reports the path to jump to – more robust than counting lines. |
Only the script editor tracks changes, so getDiff with key: policy always reports tracked: false and no hunks. It doesn't error; it simply never has anything to say.
Automation Scripting Tester
component: automation_scripting. A single-editor host – no key: anywhere.
| Command | Tool | Params | Returns |
|---|---|---|---|
getEditorValue |
cerb_get_script |
The raw editor text. | |
setEditorValue |
cerb_set_script |
value: |
ok. Wholesale replace. |
editField |
cerb_edit_script |
old:; new: |
ok, with the same exactly-once rule as above. |
grepField |
cerb_grep_script |
query:; limit: (default 25) |
JSON array of {line, path, text}. path is always empty here – only the KATA editor knows key paths. |
highlightLine |
cerb_highlight_line |
line: (1-based) |
ok |
getDiff |
cerb_get_diff |
JSON {tracked, hunks} – no key, since there's one editor. The baseline is the last test run, not the last save, so nothing is tracked until the script has been run once. |
Command bar
component: commandbar. The one host with no document. It's app-wide rather than tied to a screen – it follows the worker from page to page – so it has no getFields, and everything it offers is one-way.
| Command | Tool | Params | Returns |
|---|---|---|---|
getPage |
cerb_get_page |
JSON {page_uri, page_title, page_id, url, open_popups}. page_uri is the path Cerb routed (e.g. profiles/ticket/1234), which is what the worker is looking at even when url says otherwise. |
|
openSearch |
cerb_open_search |
record_type: a record type alias (ticket); record_query: an optional query to prefill |
ok. Opens a search popup in front of the worker. An unknown alias opens nothing at all. |
Neither command reads anything back. getPage reports where the worker is, never what's on the screen, and openSearch shows them results without reporting what matched or how many. A chat here should say what it searched for and let the worker read the answer.
An agent that needs to know what a query would match reads it server-side on its next turn with a data query instead – and to write a query for a record type it hasn't seen before, it can look the type's fields up through its cerb command line rather than guessing at names.
The command bar's ordinary shortcuts are untouched by any of this. They stay interaction.worker items and keep working beside a chat.
Data Query Tester
component: data_query. Identical to the Automation Scripting Tester for all five of its commands – same params, same returns, same empty path – minus getDiff, which this editor doesn't track.
| Command | Tool | Params | Returns |
|---|---|---|---|
getEditorValue |
cerb_get_query |
The raw editor text. | |
setEditorValue |
cerb_set_query |
value: |
ok |
editField |
cerb_edit_query |
old:; new: |
ok |
grepField |
cerb_grep_query |
query:; limit: (default 25) |
JSON array of {line, path, text} |
highlightLine |
cerb_highlight_line |
line: (1-based) |
ok |
Icon Builder
component: icon. See the Icon Builder.
| Command | Tool | Params | Returns |
|---|---|---|---|
getGeometry |
cerb_get_geometry |
The editor's raw SVG inner geometry. Not JSON. | |
setGeometry |
cerb_set_geometry |
geometry: |
ok. Pushes a new revision onto the history ring, so it stays navigable with the history arrows, and repaints the preview. |
getIconGeometry |
cerb_get_icon_geometry |
name: an icon name without its cerb-icon- prefix |
That shipped glyph's geometry, or empty for an unknown name. This is how an agent looks up how a comparable icon is drawn. |
The Icon Builder also offers a server-answered tool, with no bridge command behind it:
| Tool | Params | Returns |
|---|---|---|
cerb_list_icons |
filter: an optional substring to match names |
Every icon name in the set, one per line |
Naming an existing icon is most of the work here, and cerb_get_icon_geometry can only look one up once you already know it exists – so the set has to be enumerable. Because it's answered in Cerb rather than by the editor, there's no command: for it: a uiCommand element can't reach it.
Mail Reply
component: mail_reply. Available on the popup reply editor.
| Command | Tool | Params | Returns |
|---|---|---|---|
getFields |
cerb_get_fields |
JSON {to, cc, bcc, subject, format, content}. format is normalized on read to markdown or plaintext. |
|
setField |
cerb_set_field |
key: one of to, cc, bcc, subject, content, format; value: |
ok |
Setting format: to markdown switches the editor to Markdown; any other value means plaintext. There's no validation, so a typo silently selects plaintext rather than erroring.
Mail Routing
component: mail_routing. Available on both editors that write a routing document: the standalone mail routing rule record, and a group's Mail: Incoming tab.
One component rather than two, because the grammar, the schema, and the evaluator are shared. What differs is where in the pipeline the document runs, and cerb_get_routing reports that as a scope rather than there being a second host to tell apart.
| Command | Tool | Params | Returns |
|---|---|---|---|
getFields |
cerb_get_routing |
JSON {scope, routing_kata}, plus {group_name, buckets} when the scope is a group. scope is rule for a standalone rule that decides the group, or group for a group's own rules that decide the bucket. |
|
setField |
cerb_set_routing |
value: the complete new document |
ok. Replaces the whole document. The bridge's key: is pinned to routing_kata and never shown to the model. |
editField |
cerb_edit_routing |
old:, new: |
ok. Undo-safe search and replace. old must match exactly once; zero or several matches is an error telling the agent to expand the context. |
grepField |
cerb_grep_routing |
query:; limit: optional, default 25 |
JSON [{line, path, text}] – line numbers and the KATA key path for each hit, so a location never has to be guessed. |
getDiff |
cerb_get_diff |
JSON {tracked, hunks:[{status, line, endLine, added, removed}]} with 1-based line numbers. tracked: false means there's no baseline to diff against. |
|
highlightLine |
cerb_highlight_line |
line: 1-based |
ok. Flashes a line to point the reader at the rule under discussion. |
highlightKey |
cerb_highlight_key |
path: a colon-delimited KATA key path, e.g. rule/billing:then |
ok. The robust sibling of highlightLine – it targets the path, so it survives edits that shift line numbers. |
Mail Routing also offers a server-answered tool, with no bridge command behind it:
| Tool | Params | Returns |
|---|---|---|
cerb_test_routing |
routing_kata: the complete document to test; placeholders: the sample message, as a KATA document |
Which rule matched, if any |
placeholders: recognizes subject, body, recipients (a list of To/Cc addresses), sender_email, spam_score (0.0 to 1.0), and headers (a map with lowercase keys).
It's answered in Cerb rather than by the editor for two reasons. The tester is an HTTP round trip and a bridge command has to answer synchronously, so it couldn't be one. And taking the document as an argument rather than reading the editor is what lets a candidate rule be tested before it's written in. Being server-answered means there's no command: for it: a uiCommand element can't reach it.
Both editors mark changed lines in the gutter against the last save, whoever made the change, and Save and continue takes a fresh checkpoint. cerb_get_diff reads that same baseline, which is what lets an agent's account of an edit be checked against what it actually did.
Worklist search fields
component: worklist. The one host that isn't an editor, and the only one that can act rather than only read and write.
This isn't only the quick search bar above a worklist. The same search field is included in popups throughout the interface – the quick-search popup, record choosers – and each one carries the same agent and reports the same component. An item gated to worklist appears in all of them, and there's no state var that tells them apart.
| Command | Tool | Params | Returns |
|---|---|---|---|
getFields |
cerb_get_fields |
JSON {query, record_type, record_context, view_id}. record_type is the alias (ticket) – what data.query expects after of: – while record_context is the full context ID. |
|
setField |
cerb_set_query |
key: only query; value: |
ok, and focuses the field with the caret at the end. Any other key is an error. |
runSearch |
cerb_run_search |
ok. Asynchronous – it takes the same path as pressing Enter, so it can only report that the search started. |
Three commands rather than one per field is deliberate: the agent learns the field's shape from getFields, so state added later becomes a new key rather than a new command.
A search bar has exactly one writable field, so the cerb_set_query tool takes value alone – the bridge's key: is pinned to query and never shown to the model. A uiCommand element writing the same field still has to send it.
Return values and failures
Conventions across the current hosts:
| Result | Notes |
|---|---|
| Readers | Return raw text, or JSON where the host has several fields |
| Writers | Return the literal string ok |
| Errors | Lead with error:, unknown , or invalid |
| Unknown command | Returns an empty string on every host except worklist, which names it |
That error prefix is a contract, not a convention. An agent pane checks a mutating command's return against it to decide whether the editor now has unsaved changes, so a failure that doesn't lead with one of those three words leaves the editor falsely marked dirty. Anything you build a new host for should follow it.
An empty string is ambiguous. It means the command was unrecognized, or the editor was genuinely empty, or the host threw, or there was no command bridge at all. Failures here are silent -- the interaction proceeds either way, and never reports why. Branch on it accordingly when it matters.