Triggers
Triggers allow agents to run automatically in response to events — cron schedules, file changes, incoming webhooks, or messaging platforms. They are configured in spec.triggers and activated with initrunner run <role> --daemon.
Trigger Types
| Type | Description |
|---|---|
cron | Fire on a cron schedule |
file_watch | Fire when files change in watched directories |
webhook | Fire on incoming HTTP requests (localhost only) |
heartbeat | Fire on a fixed interval, processing a markdown checklist file |
telegram | Respond to Telegram messages via long-polling (outbound only) |
discord | Respond to Discord DMs and @mentions via WebSocket (outbound only) |
slack | Respond to Slack @mentions and DMs via Socket Mode (outbound only) |
Quick Example
spec:
triggers:
- type: cron
schedule: "0 9 * * 1"
prompt: "Generate weekly status report."
- type: file_watch
paths: ["./watched"]
extensions: [".md", ".txt"]
prompt_template: "File changed: {path}. Summarize the changes."
- type: webhook
path: /webhook
port: 8080
secret: ${WEBHOOK_SECRET}
- type: heartbeat
file: ./tasks.md
interval_seconds: 3600
active_hours: [9, 17]initrunner run role.yaml --daemonCron Trigger
Fires the agent on a cron schedule.
triggers:
- type: cron
schedule: "0 9 * * 1"
prompt: "Generate weekly status report."
timezone: UTC| Field | Type | Default | Description |
|---|---|---|---|
schedule | str | (required) | Cron expression (5-field: min hour day month weekday) |
prompt | str | (required) | Prompt sent to the agent when the trigger fires |
timezone | str | "UTC" | Timezone for schedule evaluation |
Schedule Examples
| Expression | Meaning |
|---|---|
"0 9 * * 1" | Every Monday at 9:00 AM |
"*/5 * * * *" | Every 5 minutes |
"0 0 1 * *" | First day of every month at midnight |
"30 14 * * 1-5" | Weekdays at 2:30 PM |
File Watch Trigger
Fires when files change in watched directories using watchfiles.
triggers:
- type: file_watch
paths: ["./watched", "./data"]
extensions: [".md", ".txt"]
prompt_template: "File changed: {path}. Summarize."
debounce_seconds: 1.0
process_existing: false| Field | Type | Default | Description |
|---|---|---|---|
paths | list[str] | (required) | Directories to watch |
extensions | list[str] | [] | File extensions to filter (empty = all) |
prompt_template | str | "File changed: {path}" | Template with {path} placeholder |
debounce_seconds | float | 1.0 | Debounce interval |
process_existing | bool | false | Fire once for each matching file already present on startup |
Webhook Trigger
Fires when an HTTP request is received on a local endpoint. Useful for GitHub webhooks, CI/CD systems, or HTTP callbacks.
triggers:
- type: webhook
path: /webhook
port: 8080
method: POST
secret: ${WEBHOOK_SECRET}| Field | Type | Default | Description |
|---|---|---|---|
path | str | "/webhook" | URL path to listen on |
port | int | 8080 | Port to listen on |
method | str | "POST" | HTTP method to accept |
secret | str | null | null | HMAC secret for X-Hub-Signature-256 verification |
HMAC Verification
When secret is set, requests must include a valid X-Hub-Signature-256 header (GitHub-compatible HMAC-SHA256). Invalid or missing signatures return 403 Forbidden.
Example: GitHub Webhook
triggers:
- type: webhook
path: /github
port: 9000
secret: ${GITHUB_WEBHOOK_SECRET}curl -X POST http://127.0.0.1:9000/github \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=..." \
-d '{"action": "opened", "pull_request": {"title": "Fix bug"}}'Heartbeat Trigger
Fires on a fixed interval, reading a markdown checklist file and prompting the agent with any unchecked items. Useful for batching multiple periodic tasks into a single trigger instead of separate cron entries.
triggers:
- type: heartbeat
file: ./tasks.md # required
interval_seconds: 3600 # default: 3600 (1 hour)
autonomous: true # default: false
active_hours: [9, 17] # default: null (always active)
timezone: America/New_York # default: UTCOptions
| Field | Type | Default | Description |
|---|---|---|---|
file | str | (required) | Path to the markdown checklist file |
interval_seconds | int | 3600 | Seconds between heartbeat checks. Must be > 0 |
prompt_prefix | str | "You are processing a periodic task checklist..." | Text prepended to the checklist content in the prompt |
active_hours | list[int] | null | null | Two-element list [start, end] defining active hours (0-23). null means always active |
timezone | str | "UTC" | Timezone for active_hours evaluation. Must be a valid IANA timezone (e.g. America/New_York) |
Active Hours
When active_hours is set, the trigger only fires during the specified window:
- Normal window (e.g.
[9, 17]): fires whenstart <= hour < end - Midnight-spanning (e.g.
[22, 6]): fires whenhour >= startorhour < end - Always active: omit
active_hoursor set tonull
Behavior
- The first heartbeat fires after one full interval from daemon startup (not immediately).
- On each heartbeat, the file is read (capped at 64KB with
[truncated]marker). - Unchecked items (
- [ ]) are counted. If there are zero open items, no event is fired. - The prompt is composed as:
prompt_prefix + "\n\n" + file_content. - The trigger event includes
metadata: {"file": "...", "item_count": "...", "interval_seconds": "..."}.
Example Checklist
# Daily Tasks
- [ ] Check deployment health
- [x] Review overnight alerts
- [ ] Update documentation
- [ ] Run integration testsNo new dependencies — uses stdlib zoneinfo (Python 3.9+).
Telegram Trigger
Responds to Telegram messages using long-polling via python-telegram-bot. Outbound HTTPS only — no ports opened, no inbound connections required.
Setup
- Create a bot with @BotFather and copy the token.
- Set the token:
export TELEGRAM_BOT_TOKEN=your-token(or add it to~/.initrunner/.env). - Install the optional dependency:
pip install initrunner[telegram].
triggers:
- type: telegram
token_env: TELEGRAM_BOT_TOKEN # default
allowed_users: ["alice", "bob"] # empty = allow all
prompt_template: "{message}" # defaultOptions
| Field | Type | Default | Description |
|---|---|---|---|
token_env | str | "TELEGRAM_BOT_TOKEN" | Environment variable holding the bot token. |
allowed_users | list[str] | [] | Telegram usernames allowed to interact. Empty list allows all users. |
prompt_template | str | "{message}" | Template for the prompt. {message} is replaced with the user's message text. |
Behavior
- Uses long-polling (outbound HTTPS) — no ports opened, no webhooks to configure.
- Only text messages are processed (commands like
/startare ignored). - When
allowed_usersis set, messages from other users are silently dropped. - The agent's response is sent back to the originating chat, automatically chunked to Telegram's 4096-character message limit.
- Chunks are split at newline boundaries when possible for cleaner output.
- The trigger event includes
metadata: {"user": "...", "chat_id": "..."}.
Security
- Store the bot token securely — use environment variables or a secrets manager, never commit it to version control.
- Use
allowed_usersto restrict access to known usernames. An empty list means anyone can interact with the bot. - Set
daemon_daily_token_budgetin guardrails to prevent runaway costs.
For the full quickstart walkthrough, see Telegram Bot.
Discord Trigger
Responds to Discord DMs and @mentions via WebSocket client using discord.py. Outbound only — no ports opened.
Setup
- Create a bot in the Discord Developer Portal.
- Enable the Message Content Intent under Bot settings.
- Invite the bot to your server with the
botscope andSend Messages+Read Message Historypermissions. - Set the token:
export DISCORD_BOT_TOKEN=your-token(or add it to~/.initrunner/.env). - Install the optional dependency:
pip install initrunner[discord].
triggers:
- type: discord
token_env: DISCORD_BOT_TOKEN # default
channel_ids: ["123456789"] # empty = all channels
allowed_roles: ["Admin", "Bot-User"] # empty = all roles
prompt_template: "{message}" # defaultOptions
| Field | Type | Default | Description |
|---|---|---|---|
token_env | str | "DISCORD_BOT_TOKEN" | Environment variable holding the bot token. |
channel_ids | list[str] | [] | Channel IDs to respond in. Empty list allows all channels. |
allowed_roles | list[str] | [] | Role names required to interact. Empty list allows all users. |
prompt_template | str | "{message}" | Template for the prompt. {message} is replaced with the user's message text. |
Behavior
- Uses WebSocket client connection — outbound only, no ports opened.
- Responds to DMs and @mentions only (not every message in every channel).
- When
allowed_rolesis set, DMs are denied (DMs have no role context, so allowing them would bypass the role filter). - Bot @mention is stripped from the message content using the mention ID pattern for robustness.
- The agent's response is sent back to the originating channel, automatically chunked to Discord's 2000-character message limit.
- The trigger event includes
metadata: {"user": "...", "channel_id": "..."}.
Security
- Store the bot token securely — never commit it to version control.
- Use
channel_idsto restrict the bot to specific channels. - Use
allowed_rolesto restrict access to specific server roles. Note that DMs are automatically denied when roles are configured. - Set
daemon_daily_token_budgetin guardrails to prevent runaway costs.
For the full quickstart walkthrough, see Discord Bot.
Slack Trigger
Since v2026.4.15. Responds to Slack @mentions and DMs over Socket Mode, so no inbound ports are required. Plain channel messages without a mention are ignored.
Setup
-
Create a Slack app at api.slack.com/apps and enable Socket Mode.
-
Generate an App-Level token with
connections:writescope and a Bot token (xoxb-...) withapp_mentions:read,chat:write,im:history,im:read, andim:write. -
Subscribe to the
app_mentionandmessage.imevents under Event Subscriptions. -
Install the optional dependency:
pip install initrunner[slack]. -
Export the tokens (or store them in the vault):
export SLACK_APP_TOKEN=xapp-... export SLACK_BOT_TOKEN=xoxb-...
triggers:
- type: slack
app_token_env: SLACK_APP_TOKEN # default
bot_token_env: SLACK_BOT_TOKEN # default
channel_ids: ["C0123456789"] # empty = all channels
allowed_user_ids: ["U0123456789"] # empty = all users
respond_in_thread: true # default
prompt_template: "{message}" # defaultOptions
| Field | Type | Default | Description |
|---|---|---|---|
app_token_env | str | "SLACK_APP_TOKEN" | Environment variable holding the App-Level token used for Socket Mode. |
bot_token_env | str | "SLACK_BOT_TOKEN" | Environment variable holding the Bot token used for replies. |
channel_ids | list[str] | [] | Channel IDs to respond in. Empty list allows all channels. |
allowed_user_ids | list[str] | [] | Slack user IDs allowed to interact. Empty list allows all users. |
respond_in_thread | bool | true | When responding in a channel, post the reply in a thread off the triggering message. |
prompt_template | str | "{message}" | Template for the prompt. {message} is replaced with the user's text (the bot mention is stripped). |
Behavior
- Uses Socket Mode (outbound WebSocket). No public URL or inbound port required.
- Subscribes to
app_mentionevents in channels andmessageevents in DMs. Other channel chatter is ignored. - The bot's own messages, edits, and deletes are dropped.
- When the source message is already in a thread, the reply goes back to the same thread. In a channel,
respond_in_thread: trueopens a new thread off the mention; setting it tofalseposts in the channel directly. - Each Slack thread maps to its own
conversation_key(channel:thread_ts), so multi-turn context stays per-thread. - The trigger event includes
metadata: {"channel_target": "...", "user_id": "...", "channel_id": "...", "thread_ts": "..."}andprincipal_id: "slack:<user_id>"for audit attribution.
Security
- Store tokens securely. Use environment variables, the vault, or a secrets manager. Never commit them.
- Use
allowed_user_idsto restrict access. Empty allows everyone in the channels the bot is invited to. - Use
channel_idsto keep the bot out of channels you don't want it touching. - Set
daemon_daily_token_budgetin guardrails to prevent runaway costs.
Daemon Mode
The initrunner run --daemon flag starts all configured triggers and waits for events:
initrunner run role.yaml --daemon
initrunner run role.yaml --autopilot # all triggers use autonomous loop
initrunner run role.yaml --daemon --audit-db ./custom-audit.db
initrunner run role.yaml --daemon --no-auditSee CLI Reference — Run Options for the full flag list.
Bot mode shortcut: For Telegram and Discord, you can also use
initrunner run role.yaml --bot telegramor--bot discordto start an ephemeral bot without writing trigger config in YAML. See the Telegram and Discord setup guides for full details.
Lifecycle
- The role is loaded and the agent is built.
- All triggers are started in daemon threads via
TriggerDispatcher. - When a trigger fires, the prompt is sent to the agent.
- All trigger types (cron, file watch, webhook, Telegram, Discord, Slack, heartbeat) use the autonomous loop when
autonomous: trueis set on the trigger config. The--autopilotflag forces all triggers into autonomous mode regardless of per-trigger config. - For messaging triggers (Telegram, Discord, Slack), the final output of the autonomous run is sent back to the originating channel or thread. For other triggers, the result is displayed and dispatched to sinks.
- Triggers without
autonomous: true(and not in--autopilotmode) use direct single-shot execution. - The daemon continues until interrupted.
Retry and Circuit Breaker
Since v2026.4.11, daemon runs can automatically retry on transient provider errors (rate limits, 5xx, connection failures) with exponential backoff. A circuit breaker tracks provider health across trigger fires and stops dispatching when the provider is unhealthy.
Both are configured under spec.guardrails. See Guardrails for setup and configuration.
Hot-Reload
By default, the daemon watches the role YAML and referenced skill files for changes. When a change is detected, the role and agent are reloaded without restarting the daemon.
spec:
daemon:
hot_reload: true # default: true
reload_debounce_seconds: 1.0 # default: 1.0| Field | Type | Default | Description |
|---|---|---|---|
hot_reload | bool | true | Enable file-watching for role YAML and skill files |
reload_debounce_seconds | float | 1.0 | Debounce interval (0-30 seconds) for batching rapid writes |
What reloads: role YAML, skill files, model config, tools, triggers, autonomy config.
What does NOT reload (requires daemon restart): memory store, audit logger, .env files, sink dispatcher configuration.
Fail-open policy: if the reloaded YAML is invalid, the daemon keeps the last known-good config and logs a warning.
Thread safety: in-flight trigger runs use a snapshot of the old agent/role. New runs after a reload use the updated config. Trigger dispatchers are restarted only if the trigger config actually changed.
Hot-reload requires a role_path — it is automatically enabled when running initrunner run role.yaml --daemon. Ephemeral roles (e.g. from initrunner run with no YAML) do not support hot-reload.
Trigger Events
Every trigger fires a TriggerEvent containing:
| Field | Type | Description |
|---|---|---|
trigger_type | str | "cron", "file_watch", "webhook", "heartbeat", "telegram", "discord", or "slack" |
prompt | str | The prompt to send to the agent |
timestamp | str | ISO 8601 timestamp of when the event was created |
metadata | dict[str, str] | Type-specific metadata (schedule, path, user, etc.) |
reply_fn | Callable | None | Optional callback to send the agent's response back to the originating channel |
Signal Handling
The daemon handles SIGINT (Ctrl+C) and SIGTERM for clean shutdown:
- Sets a stop event
- Stops all triggers
- Joins trigger threads (5-second timeout)
- Exits cleanly