Skip to content

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

ProviderTrigger EventsAgent ToolsConfig (swarmlord.jsonc)
Slackmessage, app_mentionslack_post_message, slack_upload_file, slack_read_thread, slack_list_channels, slack_get_user, slack_add_reactionYes
Twilioincoming_messagetwilio_send_sms, twilio_send_mmsYes
Sendblueincoming_message, group_messagesendblue_reply, sendblue_send_typing_indicator, sendblue_send_reaction, sendblue_mark_read, sendblue_evaluate_serviceYes
GitHubpush, pull_request.*, issue_comment.*(agent tools planned)Yes
LinearIssue.*, 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
  → Done

The 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 fieldDefaultDescription
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:

ToolDescription
slack_post_messagePost a message to a channel or thread (mrkdwn supported)
slack_upload_fileUpload a file — from workspace path or public URL
slack_read_threadRead messages from a thread
slack_list_channelsList channels the bot is a member of
slack_get_userGet user info (name, email) by user ID
slack_add_reactionAdd an emoji reaction to a message
slack_replyReply in a thread (convenience wrapper for thread replies)
slack_upload_filesUpload 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:

ScopeSlack tools
readslack_read_thread, slack_list_channels, slack_get_user
writeslack_reply, slack_post_message, slack_add_reaction
filesslack_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

  1. Create a Slack app with Event Subscriptions and Bot Token Scopes (chat:write, channels:read, groups:read, files:read, files:write).
  2. In the swarmlord dashboard, go to Integrations → Slack and enter your Bot Token and Signing Secret.
  3. Click Setup — this creates a webhook URL.
  4. Copy the webhook URL back into your Slack app's Event Subscriptions → Request URL.
  5. 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

  1. Slack posts the event to the webhook URL. The server validates X-Slack-Signature and drops retries.
  2. For message events, only thread replies and messages with file attachments are processed (plain top-level messages are handled by app_mention).
  3. A thread-based session is created — same Slack thread reuses the same session for multi-turn conversations.
  4. The promptTemplate is 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:

VariableDescriptionExample
{{event.user}}Slack user ID of the senderU01ABC23DEF
{{event.channel}}Channel IDC01XYZ456
{{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 typemessage or app_mention
{{event.team}}Workspace team IDT01ABC23DEF

For file uploads, additional fields are available:

VariableDescription
{{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

ActionDescription
messagePost a new message to a channel
thread_replyReply in the same thread as the triggering message
file_uploadUpload a file to the channel

Twilio

Setup via Dashboard

  1. Create a Twilio account and get a phone number.
  2. In the dashboard, go to Integrations → Twilio and enter your Account SID and Auth Token.
  3. 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

VariableDescription
{{event.from}}Sender's phone number
{{event.to}}Your Twilio number
{{event.body}}SMS text content

Twilio Output Actions

ActionDescription
smsSend a text message
mmsSend 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

  1. 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
  1. Retrieve your credentials:
bash
sendblue show-keys    # prints API key and secret
sendblue lines        # prints your assigned phone number
  1. Store them as agent secrets:
bash
swarmlord secret put SENDBLUE_API_KEY
swarmlord secret put SENDBLUE_API_SECRET
swarmlord secret put SENDBLUE_PHONE_NUMBER
  1. Deploy your agent — this outputs a webhook URL:
bash
swarmlord deploy
# → Webhook URL: https://api.swarmlord.ai/webhook/<id>
  1. 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)

ToolDescriptionInputs
sendblue_replyReply to the contact who texted you (iMessage/SMS/MMS)content, optional mediaUrl, sendStyle
sendblue_send_typing_indicatorShow the "..." typing bubble (iMessage only)(none)
sendblue_send_reactionSend a tapback reaction (love, like, laugh, etc.)reaction
sendblue_mark_readSend a read receipt (iMessage only)(none)
sendblue_evaluate_serviceCheck 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

VariableDescriptionExample
{{event.from}}Sender's phone number+15551234567
{{event.to}}Your Sendblue number+15559876543
{{event.body}}Message text contentHey, tell me a joke
{{event.media_url}}URL of attached media (expires after 30 days)https://storage.sendblue.co/...
{{event.service}}Delivery service usediMessage or SMS
{{event.group_id}}Group conversation ID (empty for 1:1 messages)group_abc123
{{event.date_sent}}ISO 8601 timestamp2024-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:

ActionDescription
messageSend an iMessage/SMS/MMS reply

Required Secrets

SecretDescriptionHow to get
SENDBLUE_API_KEYAPI key for authenticationsendblue show-keys
SENDBLUE_API_SECRETAPI secret for authenticationsendblue show-keys
SENDBLUE_PHONE_NUMBERYour dedicated Sendblue numbersendblue 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}}",
    },
],
FieldDescription
providerOutput provider name (slack, twilio, webhook)
actionProvider-specific action (e.g. thread_reply, sms, post)
configProvider credentials and routing. Supports {{secrets.*}} and {{event.*}} interpolation.
templateResponse 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 output

The 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

  1. The cron fires (evaluated every 5 minutes against UTC)
  2. A new session is created with the agent's workspace bundle
  3. The schedule.prompt is sent as the user message
  4. The agent runs (tools, skills, etc.) just like an on-demand session
  5. When the agent finishes, if outputs are configured and the final result is non-empty, outputs fire
  6. 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:

VariableDescriptionExample value
{{schedule.run_id}}Unique session ID for this runa1b2c3d4-...
{{schedule.fired_at}}ISO 8601 timestamp (UTC) when fired2025-03-15T14:00:00.000Z
{{schedule.agent_name}}Name of the scheduled agentdaily-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 why

Manual 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 files

Persistent 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

TriggerSchedule
ActivationExternal event (webhook)Cron expression (UTC)
InputEvent payload → promptTemplateFixed prompt string
OutputDelivers to source (Slack, SMS, etc.)Delivers via configured outputs if set
SessionsThread-based (Slack) or one-per-eventOne session per cron tick
Use caseChat bots, event respondersMonitoring, 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:

VariableAvailable inDescription
{{event.*}}promptTemplate, output configFlattened trigger event payload
{{secrets.NAME}}output configDecrypted secret (never in prompt)
{{result}}output templateAgent's final text output
{{schedule.run_id}}output config/templateSession ID for this scheduled run
{{schedule.fired_at}}output config/templateISO 8601 UTC timestamp of cron fire
{{schedule.agent_name}}output config/templateAgent name

Where variables work

  • promptTemplate: only {{event.*}} — no secrets, no schedule vars
  • outputs[].config: {{event.*}}, {{secrets.*}}, {{schedule.*}}
  • outputs[].template: {{result}}, {{schedule.*}}