Workers
Named specialist workers with their own persona, model, policy, and memory. How routing picks a worker by @name or channel binding, and how one worker hands a self-contained task to a colleague, one level deep and gated by its own policy.
One agent is the default, and most deployments never need more.
When you want a specialist that answers some conversations in its own right, define it under workers: in the config.
Each worker gets its own persona, an optional model, its own policy, and its own memory, so nothing one learns leaks into another's prompt.
The default worker is tomo itself, and it needs no entry.
Add a workers: block and you have a small workforce that tomo serve routes between.
What a worker carries
workers:
research:
persona: You dig up sources and summarize. Cite what you find.
model: anthropic/claude-fable-5
policy:
write: deny # this one only reads and reports
channels: ["slack:C0RESEARCH"]
personais extra system-prompt text that sets its role.modelis aprovider/modeloverride; leave it empty to use the default model.policyis the worker's own gate, merged over the top-level policy: a field it sets wins, a field it leaves unset falls back to the default, and rules merge with the worker's winning.channelsis a list ofchannel:chatkeys whose messages route to this worker.
Each worker keeps its own memory subtree under <data_dir>/workers/<name>/memory, with its own curator and its own skills.
So the research worker above never sees what the default worker remembered, and the reverse holds too.
The name tomo is reserved for the default worker; using it as a worker name is an error.
How routing picks a worker
For each incoming message, tomo decides who handles it in this order:
- An explicit
@nameprefix wins. Start a message with@research ...and it goes toresearch, with the prefix stripped before the model or the ledger sees it. An unknown@namefalls through rather than erroring. - A
channel:chatbinding is next. If the chat this message arrived on is bound to a worker in itschannelslist, that worker handles it. A givenchannel:chatcan only be bound to one worker. - Otherwise the default worker, tomo, takes it.
So a Slack channel bound to research routes there by default, but you can still reach the default worker in that same channel with an explicit prefix, and reach another colleague with theirs.
Handoff
With more than one worker defined, each worker gets a handoff tool.
A worker uses it to hand a self-contained task to a colleague and get their answer back, when another worker is better suited to a piece of the work.
The handoff is one level deep on purpose:
- The colleague starts fresh and sees nothing of the calling conversation, so the handing worker must include everything the task needs in the message.
- The colleague runs one stateless turn and returns its reply as the tool result.
- The colleague's agent is built without a handoff tool of its own, so it cannot hand off again or loop back.
- The colleague runs with no one to approve an ask, so it is gated by its own policy, and anything that would need approval is declined, exactly like an unattended run.
A solo deployment has no colleagues, so there is no handoff tool at all.
A worked example
Say you want a research specialist that only reads and reports, living in one Slack channel, while everything else stays with the default worker.
policy:
read: allow
net: allow
write: ask
exec: ask
workers:
research:
persona: >
You dig up sources and summarize what you find.
Cite every claim. You do not write files or run commands.
model: anthropic/claude-fable-5
policy:
write: deny
exec: deny
channels: ["slack:C0RESEARCH"]
Messages in Slack channel C0RESEARCH route to research, which inherits read: allow and net: allow from the top level but has write and exec denied outright, so it can fetch and summarize but never change the machine.
Anywhere else, the default worker answers.
If the default worker decides a task is better handled by research, it can hand it off; the delegate reports back and never gets to write, since its policy denies it and an unattended ask is declined anyway.