Bespoke agents Declared in TOML Launched from the portal

An agent
for that
job.

Every company has work that is too specific for a generic assistant and too repetitive for a person. Bot Rot builds you a Slack colleague for exactly that job: you describe it, its capabilities are declared in a single file, the portal connects your data and your workspace, and it starts the next morning. It keeps its records in files you own.

agents/fieldops/agent.toml
[agent]
name = "fieldops"
display_name = "Field Ops"
model = "gemini-2.5-flash"
tools = ["get_requests", "upsert_request"]

[[data]]
name = "requests"
source = "git"
binding = "data_store"
path = "requests.csv"

[daily]
tasks = ["check_overdue_requests"]
No slop Lives in Slack Est. 2026

The portal

From a sentence
to a colleague

The portal walks the whole install. Nothing to deploy on your side.

  1. 01

    Describe the job

    Tell us what the agent keeps, whom it reminds, what it must never do. That becomes its brief and its first capability set.

  2. 02

    Declare the capabilities

    We write the agent's TOML: its tools, data sources, cadence and limits. You read it; it is the whole contract.

  3. 03

    Connect your data

    In the portal, paste the repository, spreadsheet or drive the agent should use. It tests access before anyone talks to it.

  4. 04

    Connect Slack, go live

    An "Add to Slack" link for your admin, a preflight that must pass, then the switch. Your organization gets its own API key to manage it.

Capabilities

Capabilities are sections

Everything an agent can do is a section of its agent.toml. Add a section, gain a capability. The snippets are real.

01 [agent]

An identity and a brief

Every agent is one folder: a TOML file, a system prompt, and the message templates it speaks with. The prompt is yours to write; the engine renders your organization's names and timezone into it.

  • A primary model and a fallback in a separate quota pool, so a rate limit never means silence.
  • Tools are listed by name. An unknown name fails at startup, on purpose.
  • Prompt placeholders ({boss}, {company}, {timezone}) come from your organization, never from the product.
agent.toml
[agent]
name = "fieldops"
display_name = "Field Ops"
instruction_file = "instruction.txt"
model = "gemini-2.5-flash"
fallback_model = "gemini-2.5-flash-lite"
tools = [
  "get_current_date",
  "get_requests",
  "upsert_request",
  "report_feedback",
]

02 [slack]

Its own Slack identity

Each agent is a separate Slack app with its own name, icon and credentials, delivered over the Events API to a URL that is hers alone. People DM it or mention it in a channel, and it edits its own placeholder while it works.

  • Credentials live encrypted beside the definition; a single deployment key decrypts them.
  • The same agent is also an MCP server at /mcp/<agent>: your own assistants reach its tools with your organization's API key, and every call is attributed to the key it came through.
  • Placeholder texts are the agent's voice while it thinks, one picked at random.
  • The verified sender's identity comes from their Slack profile, never from the message body.
agent.toml
[slack]
icon = "icon.png"
bot_token_env = "FIELDOPS_SLACK_BOT_TOKEN"
placeholder_texts = [
  "_Checking the log…_",
  "_One sec…_",
]

03 [[data]]

Records you can read without us

Declare each thing the agent keeps. A git source is a CSV file in a repository you own; a spreadsheet source is a tab in a Google Sheet you already use. Rows are typed, writes are gated, and the history is the commit log.

  • Records are upserted in place by key, so one row per id and git holds every edit.
  • editors restricts writes to named people or the symbolic "boss"; reading stays open.
  • A binding names the organization's own repo or sheet, so the definition never hardcodes a customer.
agent.toml
[[data]]
name = "requests"
source = "git"
binding = "data_store"
path = "requests.csv"
row_type = "RequestRow"
writable = true
editors = ["boss", "[email protected]"]

[[data]]
name = "sites"
source = "spreadsheet"
binding = "sites_sheet"
tab = "Sites"
row_type = "SiteRow"

04 [[reminders]]

Reminders that follow a cycle

Describe a cadence once and the agent runs it for every item it tracks: day one of a cycle, two days before the next one starts, the morning after one ends. Each reminder has a template, a context type, and what a reply does.

  • day and week are cycle-relative; negative days fire before the cycle starts.
  • action decides what a threaded reply means: nothing, a filed update, or a row appended to a tab.
  • tasks run once per item when the reminder goes out, for things like creating a channel.
agent.toml
[schedule]
timezone = "America/New_York"
day_of_week = "mon"
hour = 8
minute = 0

[[reminders]]
id = "day1-kickoff"
day = 1
week = 1
template = "reminders/day1_kickoff.txt"
data = "ReminderContext"
action = "post_update"
tasks = ["open_project_channel"]

[[reminders]]
id = "wrap-up"
trigger = "cycle_end"
day = 2
week = 1
template = "reminders/wrap_up.txt"
data = "ReminderContext"
action = "none"
cc_boss = true

05 [daily]

Work that runs on a clock

Name the jobs that run every morning: a threshold check that DMs the right person, a monthly order list, a post to a channel on Mondays. A missed day heals itself, and failures find someone instead of a log file.

  • Tasks are plain functions in the agent's folder, registered by name.
  • Idempotent by design: reruns are safe, and sent messages are recorded so nobody is told twice.
  • A test override redirects every send to you before anything goes live.
agent.toml
[daily]
tasks = [
  "check_overdue_requests",
  "monthly_site_summary",
]

[limits]
daily_turns = 300

06 [bindings]

One definition, many organizations

An agent definition is the product. Which repository, which spreadsheet, which channel, who the admins are: those are bindings, supplied per organization from the portal. Declare what the agent requires and what it defaults.

  • required is what onboarding must collect before the agent can go live.
  • defaults let an optional capability switch itself off cleanly when an organization has no value for it.
  • The same TOML serves every customer; nothing of one customer ever reaches another.
agent.toml
[bindings]
required = ["data_store", "sites_sheet"]

[bindings.defaults]
alerts_channel = "general"
warehouse_database = ""

07 [sessions]

Memory, bounded

Conversations keep their thread for the day. Reply anchors remember which message a reminder was, so answering it in-thread files the update where it belongs. Both live in collections the agent owns, namespaced per organization.

  • DM sessions reset daily; channel threads keep context as long as the thread lives.
  • A daily turn budget stops runaway loops and alerts the admin once.
agent.toml
[sessions]
backend = "firestore"
firestore_collection = "fieldops-sessions"

[reminder_state]
backend = "firestore"
collection = "fieldops-reminder-actions"

[data_cache]
ttl_seconds = 60

08 [your_section]

Sections of your own

An agent can read any section you add. A calendar feed people subscribe to, a weekly question schedule, a read-only warehouse connection, an approval workflow: each is a few keys the agent's own code reads at startup.

  • Bot-owned sections are read from the raw TOML; the engine never has to know about them.
  • Values can be bindings too, so the section is still customer-neutral.
agent.toml
[calendar]
public_url = "binding:service_url"
token_env = "FIELDOPS_ICS_TOKEN"
name = "Field Ops"
howto = "calendar_howto.txt"

[warehouse]
database = "binding:warehouse_database"
read_only = true
row_cap = 500

The whole thing

A whole agent,
in one file

Everything the engine needs to run an agent that tracks field requests, reads a sites sheet, and checks for overdue work each morning. The prompt, templates and the agent's own tools sit beside it in the same folder.

agents/fieldops/agent.toml — complete
# Field Ops — agent definition.
# One folder: this file, instruction.txt, templates, and the agent's own
# Python (tools.py, rowtypes.py, tasks.py). Run with AGENT_NAME=fieldops.

[agent]
name = "fieldops"
display_name = "Field Ops"
instruction_file = "instruction.txt"
model = "gemini-2.5-flash"
fallback_model = "gemini-2.5-flash-lite"
tools = [
  "get_current_date",
  "get_requests", "upsert_request", "close_request",
  "get_sites",
  "report_feedback",
]

[slack]
icon = "icon.png"
bot_token_env = "FIELDOPS_SLACK_BOT_TOKEN"
placeholder_texts = ["_Checking the log…_", "_One sec…_"]

[sessions]
backend = "firestore"
firestore_collection = "fieldops-sessions"

[reminder_state]
backend = "firestore"
collection = "fieldops-reminder-actions"

[data_cache]
ttl_seconds = 60

[git]
token_env = "GITHUB_TOKEN"
branch = "main"

[[data]]
name = "requests"
source = "git"
binding = "data_store"
path = "requests.csv"
row_type = "RequestRow"
writable = true

[[data]]
name = "sites"
source = "spreadsheet"
binding = "sites_sheet"
tab = "Sites"
row_type = "SiteRow"

[schedule]
timezone = "America/New_York"
day_of_week = "mon"
hour = 8
minute = 0
reminder_template = "reminder_template.txt"
nag_template = "nag_template.txt"

[daily]
tasks = ["check_overdue_requests"]

[limits]
daily_turns = 300

[bindings]
required = ["data_store", "sites_sheet"]

[bindings.defaults]
alerts_channel = "general"

Want the keys explained one by one? Read the TOML reference.

Tell us
the job.

One conversation is enough to draft the first agent. Your organization, your data, your Slack.