InitRunner

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

TypeDescription
cronFire on a cron schedule
file_watchFire when files change in watched directories
webhookFire on incoming HTTP requests (localhost only)
heartbeatFire on a fixed interval, processing a markdown checklist file
telegramRespond to Telegram messages via long-polling (outbound only)
discordRespond to Discord DMs and @mentions via WebSocket (outbound only)
slackRespond 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 --daemon

Cron Trigger

Fires the agent on a cron schedule.

triggers:
  - type: cron
    schedule: "0 9 * * 1"
    prompt: "Generate weekly status report."
    timezone: UTC
FieldTypeDefaultDescription
schedulestr(required)Cron expression (5-field: min hour day month weekday)
promptstr(required)Prompt sent to the agent when the trigger fires
timezonestr"UTC"Timezone for schedule evaluation

Schedule Examples

ExpressionMeaning
"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
FieldTypeDefaultDescription
pathslist[str](required)Directories to watch
extensionslist[str][]File extensions to filter (empty = all)
prompt_templatestr"File changed: {path}"Template with {path} placeholder
debounce_secondsfloat1.0Debounce interval
process_existingboolfalseFire 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}
FieldTypeDefaultDescription
pathstr"/webhook"URL path to listen on
portint8080Port to listen on
methodstr"POST"HTTP method to accept
secretstr | nullnullHMAC 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: UTC

Options

FieldTypeDefaultDescription
filestr(required)Path to the markdown checklist file
interval_secondsint3600Seconds between heartbeat checks. Must be > 0
prompt_prefixstr"You are processing a periodic task checklist..."Text prepended to the checklist content in the prompt
active_hourslist[int] | nullnullTwo-element list [start, end] defining active hours (0-23). null means always active
timezonestr"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 when start <= hour < end
  • Midnight-spanning (e.g. [22, 6]): fires when hour >= start or hour < end
  • Always active: omit active_hours or set to null

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 tests

No 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

  1. Create a bot with @BotFather and copy the token.
  2. Set the token: export TELEGRAM_BOT_TOKEN=your-token (or add it to ~/.initrunner/.env).
  3. 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}"       # default

Options

FieldTypeDefaultDescription
token_envstr"TELEGRAM_BOT_TOKEN"Environment variable holding the bot token.
allowed_userslist[str][]Telegram usernames allowed to interact. Empty list allows all users.
prompt_templatestr"{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 /start are ignored).
  • When allowed_users is 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_users to restrict access to known usernames. An empty list means anyone can interact with the bot.
  • Set daemon_daily_token_budget in 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

  1. Create a bot in the Discord Developer Portal.
  2. Enable the Message Content Intent under Bot settings.
  3. Invite the bot to your server with the bot scope and Send Messages + Read Message History permissions.
  4. Set the token: export DISCORD_BOT_TOKEN=your-token (or add it to ~/.initrunner/.env).
  5. 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}"       # default

Options

FieldTypeDefaultDescription
token_envstr"DISCORD_BOT_TOKEN"Environment variable holding the bot token.
channel_idslist[str][]Channel IDs to respond in. Empty list allows all channels.
allowed_roleslist[str][]Role names required to interact. Empty list allows all users.
prompt_templatestr"{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_roles is 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_ids to restrict the bot to specific channels.
  • Use allowed_roles to restrict access to specific server roles. Note that DMs are automatically denied when roles are configured.
  • Set daemon_daily_token_budget in 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

  1. Create a Slack app at api.slack.com/apps and enable Socket Mode.

  2. Generate an App-Level token with connections:write scope and a Bot token (xoxb-...) with app_mentions:read, chat:write, im:history, im:read, and im:write.

  3. Subscribe to the app_mention and message.im events under Event Subscriptions.

  4. Install the optional dependency: pip install initrunner[slack].

  5. 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}"       # default

Options

FieldTypeDefaultDescription
app_token_envstr"SLACK_APP_TOKEN"Environment variable holding the App-Level token used for Socket Mode.
bot_token_envstr"SLACK_BOT_TOKEN"Environment variable holding the Bot token used for replies.
channel_idslist[str][]Channel IDs to respond in. Empty list allows all channels.
allowed_user_idslist[str][]Slack user IDs allowed to interact. Empty list allows all users.
respond_in_threadbooltrueWhen responding in a channel, post the reply in a thread off the triggering message.
prompt_templatestr"{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_mention events in channels and message events 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: true opens a new thread off the mention; setting it to false posts 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": "..."} and principal_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_ids to restrict access. Empty allows everyone in the channels the bot is invited to.
  • Use channel_ids to keep the bot out of channels you don't want it touching.
  • Set daemon_daily_token_budget in 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-audit

See 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 telegram or --bot discord to start an ephemeral bot without writing trigger config in YAML. See the Telegram and Discord setup guides for full details.

Lifecycle

  1. The role is loaded and the agent is built.
  2. All triggers are started in daemon threads via TriggerDispatcher.
  3. When a trigger fires, the prompt is sent to the agent.
  4. All trigger types (cron, file watch, webhook, Telegram, Discord, Slack, heartbeat) use the autonomous loop when autonomous: true is set on the trigger config. The --autopilot flag forces all triggers into autonomous mode regardless of per-trigger config.
  5. 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.
  6. Triggers without autonomous: true (and not in --autopilot mode) use direct single-shot execution.
  7. 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
FieldTypeDefaultDescription
hot_reloadbooltrueEnable file-watching for role YAML and skill files
reload_debounce_secondsfloat1.0Debounce 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:

FieldTypeDescription
trigger_typestr"cron", "file_watch", "webhook", "heartbeat", "telegram", "discord", or "slack"
promptstrThe prompt to send to the agent
timestampstrISO 8601 timestamp of when the event was created
metadatadict[str, str]Type-specific metadata (schedule, path, user, etc.)
reply_fnCallable | NoneOptional 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:

  1. Sets a stop event
  2. Stops all triggers
  3. Joins trigger threads (5-second timeout)
  4. Exits cleanly

On this page