Skip to content

Schedules and channels

Most runs start from an HTTP request. Triggers are the other two ways a run begins: a schedule fires a prompt on a clock, and a channel fires on an inbound event. Both run only while the agent is served (maiden serve), and both share the same single runtime lock as the HTTP server — so a triggered run and a request never overlap.

A schedule fires a fixed prompt on a fixed interval. Create a file under schedules/:

my-agent/schedules/heartbeat.toml
prompt = "summarize anything new since the last tick"
every_seconds = 300
Key Type Description
prompt string The message sent on each fire.
every_seconds integer Seconds between fires.

Each schedule runs on its own thread, named after the file. The prompt runs through the normal agent loop, so it can call tools and its history accumulates on that thread.

A channel starts a run from an inbound event. There are seven kinds: file-drop, telegram, slack, discord, github, linear and twilio.

Every credential is named, never written into the folder: a channel takes the name of an environment variable and resolves it at load time.

maiden watches a directory, and each JSON file dropped into it becomes a message.

my-agent/channels/inbox.toml
kind = "file-drop"
dir = "/var/spool/my-agent-inbox"
Key Type Description
kind string Must be file-drop.
dir string Directory to watch for *.json files.

Drop a file like {"message": "process order 1234"} into that directory, and maiden runs the agent with that message. Each file is processed exactly once: after the run, the file is moved into a .processed/ subdirectory, so a restart never re-runs an already-handled message.

Terminal window
echo '{"message":"hello from a dropped file"}' > /var/spool/my-agent-inbox/msg.json

Long-polls the Bot API, so it needs no public URL and no port.

kind = "telegram"
token_env = "TELEGRAM_BOT_TOKEN"
allow_from = ["123456789"]
Key Type Description
token_env string Env var holding the bot token.
allow_from string[] Chat ids allowed to drive the agent. Empty or absent allows all.

slack, discord, github, linear and twilio receive events pushed over HTTP. Each serves its own port — separate from maiden serve’s — and each verifies the provider’s signature before doing anything with a request, so an unsigned or forged event is rejected with a 401.

Give each channel a distinct port and put them behind a reverse proxy or tunnel that terminates TLS.

channels/slack.toml
kind = "slack"
port = 8091
token_env = "SLACK_BOT_TOKEN"
signing_secret_env = "SLACK_SIGNING_SECRET"
Kind Keys beyond kind and port
slack token_env, signing_secret_env
discord public_key_env (Ed25519 public key, hex)
github webhook_secret_env, installation_token_env
linear access_token_env, webhook_secret_env
twilio webhook_url, account_sid_env, auth_token_env, allow_from

Notes worth knowing before you wire one up:

  • Twilio’s allow_from is mandatory and deny-by-default. Unlike Telegram’s, an empty list denies every sender. webhook_url must be the exact public URL Twilio posts to, because it is part of what the signature covers.
  • GitHub takes a pre-resolved installation token. maiden does not mint one from an App JWT, so whoever operates the agent rotates installation_token_env before it expires.
  • Discord replies inline, as the interaction’s immediate response. That needs no bot token, but Discord’s acknowledgement window is about three seconds — a slow model turn can miss it.

A complete multi-channel agent lives in examples/opsbot.

Triggers start automatically with the server:

Terminal window
maiden serve my-agent
# [trigger] schedule 'heartbeat' every 300s
# [trigger] channel 'inbox' watching /var/spool/my-agent-inbox

They log each run’s outcome to stderr. Because everything shares one lock, a busy schedule or channel will queue behind in-flight work rather than run in parallel — see Execution model.