Triggers & Integrations
Triggers connect agents to external services. When an event arrives (a Slack message, an incoming SMS), the agent spins up a session, processes the event, and optionally delivers a response back to the source.
Supported Integrations
| Provider | Trigger Events | Agent Tools | Config (swarmlord.jsonc) |
|---|---|---|---|
| Slack | message, app_mention | slack_post_message, slack_upload_file, slack_read_thread, slack_list_channels, slack_get_user, slack_add_reaction | Yes |
| Twilio | incoming_message | twilio_send_sms, twilio_send_mms | Yes |
| Sendblue | incoming_message, group_message | sendblue_reply, sendblue_send_typing_indicator, sendblue_send_reaction, sendblue_mark_read, sendblue_evaluate_service | Yes |
| GitHub | push, pull_request.*, issue_comment.* | (agent tools planned) | Yes |
| Linear | Issue.*, Comment.* | (agent tools planned) | Yes |
| Webhook | (any custom event) | (none — output only) | Yes |
Integration Plugins
Each integration is an integration plugin that bundles everything the agent needs: trigger parsing, webhook lifecycle hooks, agent tools, a skill, and permission scopes. When you set trigger.provider: "slack", the Slack plugin is automatically enabled — its tools become available to the agent during its run.
Tool-Driven Delivery
For integrations with tools (Slack, Twilio), the agent delivers results during its run using tool calls — not via post-completion outputs. This gives the agent full control over what to post, when, and how to format it.
User: @support-bot Check the billing issue for customer #1234
Agent:
→ investigates the issue...
→ calls slack_post_message: "Looking into it, one moment..."
→ queries the database, reads billing code...
→ calls slack_post_message: "Found the issue — duplicate charge from March."
→ calls slack_upload_file: uploads a CSV with transaction details
→ DoneThe agent uses slack_post_message and slack_upload_file as tools, just like it uses bash or read. The Slack skill (auto-loaded with the plugin) teaches the agent how to format messages, thread replies, and upload files.
For simple integrations like Twilio SMS, the auto-reply output system is still used — the agent's final text is sent back as an SMS automatically.
expectedTools — Completion Contract
Define which tools the agent is expected to call in swarmlord.jsonc:
jsonc
{
"name": "support-bot",
"trigger": { "provider": "slack", "channels": ["support"] },
"expectedTools": ["slack_post_message"],
}If the agent finishes without calling every tool in the list, the system flags it — similar to how open todos indicate incomplete work. This catches cases where the agent "forgets" to deliver its results.
expectedTools is a simple AND list: every tool must be called at least once. It's a safety net, not a workflow engine. Complex delivery logic belongs in the agent's instructions and skill, not in the config.
| Config field | Default | Description |
|---|---|---|
expectedTools | (none) | Tool IDs the agent must call at least once |
onMissedExpectedTool | "warn" | What happens if the agent misses: "warn" (log only), "retry" (re-prompts the agent once, listing the missed tools) |
Integration Tools (Slack)
When a Slack trigger is configured, these tools are available to the agent:
| Tool | Description |
|---|---|
slack_post_message | Post a message to a channel or thread (mrkdwn supported) |
slack_upload_file | Upload a file — from workspace path or public URL |
slack_read_thread | Read messages from a thread |
slack_list_channels | List channels the bot is a member of |
slack_get_user | Get user info (name, email) by user ID |
slack_add_reaction | Add an emoji reaction to a message |
slack_reply | Reply in a thread (convenience wrapper for thread replies) |
slack_upload_files | Upload multiple files to a channel or thread |
Tools are namespaced (slack_post_message, github_create_issue) and can be disabled in the tools config:
jsonc
{
"tools": {
"slack_upload_file": false,
},
}Scopes
Integration tools are grouped into scopes for convenience:
| Scope | Slack tools |
|---|---|
read | slack_read_thread, slack_list_channels, slack_get_user |
write | slack_reply, slack_post_message, slack_add_reaction |
files | slack_upload_file |
Use scopes in the integrations config to enable specific groups:
jsonc
{
"integrations": {
"slack": ["read", "write"],
},
}Testing Integrations
Use swarmlord test-integration to simulate webhook events and stream the agent's full run:
bash
# List available test fixtures
swarmlord test-integration <trigger-id> --list-fixtures
# Run with a fixture
swarmlord test-integration <trigger-id> --fixture app_mention
# Run with a custom payload
swarmlord test-integration <trigger-id> --payload '{"type":"event_callback","event":{...}}'The agent runs in a real session with full tool access — you see tool calls, text output, and timing streamed to your terminal. See CLI Reference — test-integration for full details.
Slack
Setup via Dashboard
- Create a Slack app with Event Subscriptions and Bot Token Scopes (
chat:write,channels:read,groups:read,files:read,files:write). - In the swarmlord dashboard, go to Integrations → Slack and enter your Bot Token and Signing Secret.
- Click Setup — this creates a webhook URL.
- Copy the webhook URL back into your Slack app's Event Subscriptions → Request URL.
- Assign the agent to a channel — select which deployed agent handles messages in that channel.
Setup via Config
For CLI-based setup, add trigger and output configuration to swarmlord.jsonc:
jsonc
{
"name": "support-bot",
"model": "anthropic/claude-haiku-4.5",
"trigger": {
"provider": "slack",
"events": ["message", "app_mention"],
},
"promptTemplate": "A Slack message was received:\n\nFrom: {{event.user}}\nChannel: {{event.channel}}\nText: {{event.text}}\n\nRespond helpfully.",
"outputs": [
{
"provider": "slack",
"action": "thread_reply",
"config": {
"token": "{{secrets.SLACK_BOT_TOKEN}}",
"channel": "{{event.channel}}",
"thread_ts": "{{event.ts}}",
},
"template": "{{result}}",
},
],
"permission": { "*": "allow" },
}How Slack Triggers Work
- Slack posts the event to the webhook URL. The server validates
X-Slack-Signatureand drops retries. - For
messageevents, only thread replies and messages with file attachments are processed (plain top-level messages are handled byapp_mention). - A thread-based session is created — same Slack thread reuses the same session for multi-turn conversations.
- The
promptTemplateis rendered with event variables, the agent runs, and the output is delivered back.
TIP
Use the dashboard to connect Slack — it stores the real Signing Secret. CLI-only deploys generate a random secret that won't match Slack's signatures.
Slack Event Variables
The full Slack event payload is flattened into event.* variables. Common fields:
| Variable | Description | Example |
|---|---|---|
{{event.user}} | Slack user ID of the sender | U01ABC23DEF |
{{event.channel}} | Channel ID | C01XYZ456 |
{{event.text}} | Message text (mentions replaced) | @bot check the dashboard |
{{event.ts}} | Message timestamp (threading key) | 1710000000.000100 |
{{event.thread_ts}} | Thread timestamp (if replying in a thread) | 1710000000.000050 |
{{event.type}} | Event type | message or app_mention |
{{event.team}} | Workspace team ID | T01ABC23DEF |
For file uploads, additional fields are available:
| Variable | Description |
|---|---|
{{event.files.0.name}} | Filename of first attachment |
{{event.files.0.mimetype}} | MIME type |
{{event.files.0.size}} | File size in bytes |
Nested fields use dot notation — the provider flattens the entire Slack event payload, so any field Slack sends is accessible as {{event.path.to.field}}.
Slack Output Actions
| Action | Description |
|---|---|
message | Post a new message to a channel |
thread_reply | Reply in the same thread as the triggering message |
file_upload | Upload a file to the channel |
Twilio
Setup via Dashboard
- Create a Twilio account and get a phone number.
- In the dashboard, go to Integrations → Twilio and enter your Account SID and Auth Token.
- Select a phone number and assign an agent to it.
The dashboard automatically configures Twilio's SMS webhook URL to point at the agent's trigger endpoint.
Setup via Config
jsonc
{
"name": "joke-writer",
"model": "google/gemini-3-flash-preview",
"trigger": {
"provider": "twilio",
"events": ["incoming_message"],
},
"promptTemplate": "Someone texted you:\n\nFrom: {{event.from}}\nMessage: {{event.body}}\n\nWrite them a joke in response. Just the joke — keep it short enough for a single SMS.",
"outputs": [
{
"provider": "twilio",
"action": "sms",
"config": {
"account_sid": "{{secrets.TWILIO_ACCOUNT_SID}}",
"auth_token": "{{secrets.TWILIO_AUTH_TOKEN}}",
"from": "{{secrets.TWILIO_PHONE_NUMBER}}",
"to": "{{event.from}}",
},
"template": "{{result}}",
},
],
"tools": {
"bash": false,
"read": false,
"write": false,
},
"permission": { "*": "deny" },
}Twilio Event Variables
| Variable | Description |
|---|---|
{{event.from}} | Sender's phone number |
{{event.to}} | Your Twilio number |
{{event.body}} | SMS text content |
Twilio Output Actions
| Action | Description |
|---|---|
sms | Send a text message |
mms | Send a message with media attachment |
Sendblue
Sendblue is an iMessage API. Agents can send and receive iMessage, SMS, MMS, and RCS with automatic fallback (iMessage → RCS → SMS). Unlike Twilio, Sendblue agents get native iMessage features: typing indicators, read receipts, tapback reactions, and expressive effects.
Setup
- Install the Sendblue CLI and authenticate:
bash
bun install -g @sendblue/cli
sendblue login
# Enter your email and the 8-digit OTP sent to it- Retrieve your credentials:
bash
sendblue show-keys # prints API key and secret
sendblue lines # prints your assigned phone number- Store them as agent secrets:
bash
swarmlord secret put SENDBLUE_API_KEY
swarmlord secret put SENDBLUE_API_SECRET
swarmlord secret put SENDBLUE_PHONE_NUMBER- Deploy your agent — this outputs a webhook URL:
bash
swarmlord deploy
# → Webhook URL: https://api.swarmlord.ai/webhook/<id>- Point Sendblue's receive webhook at that URL:
bash
sendblue webhooks add --type receive https://api.swarmlord.ai/webhook/<id>Setup via Config
jsonc
{
"name": "imessage-bot",
"model": "google/gemini-3-flash-preview",
"trigger": {
"provider": "sendblue",
"events": ["incoming_message"],
},
"promptTemplate": "An iMessage was received:\n\nMessage: {{event.body}}",
"tools": {
"sendblue_reply": true,
"sendblue_send_typing_indicator": true,
"sendblue_send_reaction": true,
"sendblue_mark_read": true,
},
"permission": { "*": "allow" },
}Sendblue agents use tool-driven delivery — the agent calls sendblue_reply during its run rather than relying on post-completion outputs. This enables the agent to send read receipts and typing indicators before replying, making the conversation feel native.
Integration Tools (Sendblue)
| Tool | Description | Inputs |
|---|---|---|
sendblue_reply | Reply to the contact who texted you (iMessage/SMS/MMS) | content, optional mediaUrl, sendStyle |
sendblue_send_typing_indicator | Show the "..." typing bubble (iMessage only) | (none) |
sendblue_send_reaction | Send a tapback reaction (love, like, laugh, etc.) | reaction |
sendblue_mark_read | Send a read receipt (iMessage only) | (none) |
sendblue_evaluate_service | Check if the contact supports iMessage | (none) |
All tools automatically target the contact who triggered the session — the agent never handles phone numbers directly. The from_number is resolved from the SENDBLUE_PHONE_NUMBER secret.
Sendblue Event Variables
| Variable | Description | Example |
|---|---|---|
{{event.from}} | Sender's phone number | +15551234567 |
{{event.to}} | Your Sendblue number | +15559876543 |
{{event.body}} | Message text content | Hey, tell me a joke |
{{event.media_url}} | URL of attached media (expires after 30 days) | https://storage.sendblue.co/... |
{{event.service}} | Delivery service used | iMessage or SMS |
{{event.group_id}} | Group conversation ID (empty for 1:1 messages) | group_abc123 |
{{event.date_sent}} | ISO 8601 timestamp | 2024-01-15T10:30:00Z |
Sendblue Output Actions
For simple auto-reply agents that don't need typing indicators or reactions, you can use the output system instead of tools:
| Action | Description |
|---|---|
message | Send an iMessage/SMS/MMS reply |
Required Secrets
| Secret | Description | How to get |
|---|---|---|
SENDBLUE_API_KEY | API key for authentication | sendblue show-keys |
SENDBLUE_API_SECRET | API secret for authentication | sendblue show-keys |
SENDBLUE_PHONE_NUMBER | Your dedicated Sendblue number | sendblue lines |
Rate Limits
- 1 message/second per number (auto-queued up to 1,500 messages; 429 after that)
- AI Agent plan: 1,000 inbound contacts/day/line, 200 follow-ups/day/line, unlimited replies within 24h
- iMessage detection: 30/hour, 100/day per line
Prompt Templates
The promptTemplate field defines the user message sent to the agent when a trigger fires. Use {{variable}} syntax to interpolate event data:
text
A Slack message was received:
From: {{event.user}}
Channel: {{event.channel}}
Text: {{event.text}}
Run a full audit on the site mentioned.If promptTemplate is omitted, a provider-specific default is used.
WARNING
{{secrets.*}} references are not allowed in promptTemplate — secrets can only appear in outputs[].config values.
Outputs
Outputs define where agent results are delivered after a session completes. They're valid when a trigger or schedule is configured.
jsonc
"outputs": [
{
"provider": "slack",
"action": "thread_reply",
"config": {
"token": "{{secrets.SLACK_BOT_TOKEN}}",
"channel": "{{event.channel}}",
"thread_ts": "{{event.ts}}",
},
"template": "{{result}}",
},
],| Field | Description |
|---|---|
provider | Output provider name (slack, twilio, webhook) |
action | Provider-specific action (e.g. thread_reply, sms, post) |
config | Provider credentials and routing. Supports {{secrets.*}} and {{event.*}} interpolation. |
template | Response template. {{result}} is replaced with the agent's final text output. |
Webhook Output
The webhook provider sends results to an arbitrary URL:
jsonc
{
"provider": "webhook",
"action": "post",
"config": {
"url": "https://my-app.com/hooks/agent-done",
"authorization": "{{secrets.WEBHOOK_TOKEN}}",
},
"template": "{{result}}",
}Conditional Outputs
Outputs fire when the agent's final text result is non-empty. To build agents that only deliver output on certain conditions (e.g., alert only when something is wrong), have the agent return an empty string for the silent case:
markdown
<!-- In SOUL.md -->
## Output Rules
- If all checks pass, respond with exactly an empty message (no text at all)
- If any check fails, respond with a formatted alert message — this triggers the outputThe agent's final {{result}} is what gets sent. An empty result means no output fires — the session completes silently. This is the recommended pattern for monitoring and alerting agents.
Secrets in Outputs
Output config values can reference encrypted secrets stored via the CLI or dashboard:
jsonc
"token": "{{secrets.SLACK_BOT_TOKEN}}"
"account_sid": "{{secrets.TWILIO_ACCOUNT_SID}}"Set secrets with swarmlord secret put SLACK_BOT_TOKEN or via the dashboard Secrets page. They are resolved at runtime and passed directly to integrations — the LLM never has access to the raw values.
Schedules
Scheduled agents run on a cron expression. Each tick creates a fresh session, sends the configured prompt, and optionally delivers results via outputs.
How a Scheduled Run Works
- The cron fires (evaluated every 5 minutes against UTC)
- A new session is created with the agent's workspace bundle
- The
schedule.promptis sent as the user message - The agent runs (tools, skills, etc.) just like an on-demand session
- When the agent finishes, if
outputsare configured and the final result is non-empty, outputs fire - The session is recorded in logs (
swarmlord logs <agent-name>)
Config
jsonc
{
"name": "daily-digest",
"schedule": {
"cron": "0 9 * * 1-5",
"prompt": "Summarize yesterday's activity and post to Slack.",
},
"outputs": [
{
"provider": "webhook",
"action": "post",
"config": {
"url": "{{secrets.SLACK_WEBHOOK_URL}}",
},
"template": "{{result}}",
},
],
}Timezone
WARNING
Cron expressions are evaluated in UTC. A PST user writing 0 9 * * * will see it fire at 1 AM Pacific / 2 AM Mountain. Convert your desired local time to UTC before configuring.
Examples:
- 9 AM US Eastern (EST/UTC-5):
0 14 * * * - 9 AM US Pacific (PST/UTC-8):
0 17 * * * - 9 AM Central European (CET/UTC+1):
0 8 * * *
Schedule Template Variables
These variables are available in output config and template fields:
| Variable | Description | Example value |
|---|---|---|
{{schedule.run_id}} | Unique session ID for this run | a1b2c3d4-... |
{{schedule.fired_at}} | ISO 8601 timestamp (UTC) when fired | 2025-03-15T14:00:00.000Z |
{{schedule.agent_name}} | Name of the scheduled agent | daily-digest |
{{secrets.NAME}} | Decrypted secret value | (resolved at runtime) |
{{result}} | Agent's final text output | (in template field only) |
Error Handling
- If the agent errors or times out, no output fires (the session still appears in logs with error status)
- If the agent returns an empty string, no output fires — the session completes silently
- If an output delivery fails (e.g., webhook 500), the error is logged but does not retry automatically
- Concurrent runs: if a cron tick fires while a previous run is still active, both run independently
Monitoring Pattern
The silent-on-empty behavior makes schedules ideal for alerting. Instruct the agent to return nothing when healthy:
markdown
<!-- In SOUL.md -->
## Rules
- If all checks pass, respond with NO text (empty response = silent = healthy)
- If any check fails, respond with a formatted alert including what failed and whyManual Trigger
Test a scheduled agent without waiting for the cron:
bash
swarmlord trigger daily-digest # fire immediately
swarmlord trigger daily-digest -o results # fire and download workspace filesPersistent State
Scheduled agents can accumulate state across runs using /workspace/persist/. Files written there are backed up after each run and restored at the start of the next one. See Persistent Storage for details.
Schedules vs Triggers
| Trigger | Schedule | |
|---|---|---|
| Activation | External event (webhook) | Cron expression (UTC) |
| Input | Event payload → promptTemplate | Fixed prompt string |
| Output | Delivers to source (Slack, SMS, etc.) | Delivers via configured outputs if set |
| Sessions | Thread-based (Slack) or one-per-event | One session per cron tick |
| Use case | Chat bots, event responders | Monitoring, periodic reports, alerting |
| Variables | {{event.*}} | {{schedule.run_id}}, {{schedule.fired_at}}, {{schedule.agent_name}} |
An agent can have either a trigger or a schedule, not both. Agents with neither are on-demand — used via the SDK only.
Template Variable Reference
All {{variable}} substitutions available across the platform, by context:
| Variable | Available in | Description |
|---|---|---|
{{event.*}} | promptTemplate, output config | Flattened trigger event payload |
{{secrets.NAME}} | output config | Decrypted secret (never in prompt) |
{{result}} | output template | Agent's final text output |
{{schedule.run_id}} | output config/template | Session ID for this scheduled run |
{{schedule.fired_at}} | output config/template | ISO 8601 UTC timestamp of cron fire |
{{schedule.agent_name}} | output config/template | Agent name |
Where variables work
promptTemplate: only{{event.*}}— no secrets, no schedule varsoutputs[].config:{{event.*}},{{secrets.*}},{{schedule.*}}outputs[].template:{{result}},{{schedule.*}}