Skip to content

Notifications

Send notifications across multiple channels: email, Slack, Discord, SMS, push notifications, and webhooks. Each channel is a separate connector type with a send operation as target.

Operations

Connector Operation Direction Description
email send target Send an email
slack send target Post a Slack message
discord send target Post a Discord message
sms send target Send an SMS
push send target Send a push notification
webhook (outbound) send target Send an HTTP webhook
webhook (inbound) receive source Receive an external webhook

Slack

Two modes: webhook (simple, no auth) or token (full Slack API with chat.postMessage). If both are set, webhook takes precedence.

# Webhook mode — simple, no OAuth needed
connector "slack_webhook" {
  type        = "slack"
  webhook_url = env("SLACK_WEBHOOK_URL")
  channel     = "#notifications"
  username    = "Mycel Bot"
  icon_emoji  = ":robot_face:"
}

# Token mode — full API access, requires Bot OAuth token
connector "slack_api" {
  type    = "slack"
  token   = env("SLACK_BOT_TOKEN")
  channel = "#notifications"
}

# Token mode with custom API URL (proxy, Slack Enterprise Grid, etc.)
connector "slack_custom" {
  type    = "slack"
  token   = env("SLACK_BOT_TOKEN")
  api_url = env("SLACK_API_URL")   # default: https://slack.com/api
  channel = "#notifications"
}

Connector Options

Option Type Required Default Description
webhook_url string yes* Slack incoming webhook URL
token string yes* Bot/User OAuth token (for API mode)
api_url string optional https://slack.com/api Slack API base URL (for proxies or custom endpoints)
channel string optional Default channel to post to
username string optional Display username (webhook mode only)
icon_emoji string optional Display emoji icon (webhook mode only)
icon_url string optional Display icon URL (webhook mode only)
timeout duration optional 30s HTTP request timeout

* Either webhook_url or token is required. Not both.

Transform Fields

Field Type Description
channel string Channel to post to (overrides default)
text string Message text (required)
thread_ts string Thread timestamp (reply to thread)
username string Override display username
icon_emoji string Override display emoji
icon_url string Override display icon URL
unfurl_links bool Unfurl links in message
unfurl_media bool Unfurl media in message
mrkdwn bool Enable markdown parsing

Example

flow "alert_slack" {
  from {
    connector = "rabbit"
    operation = "alerts"
  }
  transform {
    channel = "'#alerts'"
    text    = "'Alert: ' + input.body.message"
  }
  to {
    connector = "slack_api"
    operation = "send"
  }
}

Message batching (since v2.5.0)

Slack's chat.postMessage and Incoming Webhooks are rate-limited to roughly 1 message per second per channel. Above that, Slack does not return an error — it silently hides messages with a banner:

"Due to a high volume of activity, we are not displaying some messages sent by this application."

To stay under that limit, the Slack connector coalesces high-rate writes into a single summary message per window, enabled by default. Low-rate traffic is unaffected: when a window contains only one message it is sent as-is.

Defaults (no batch { } block needed):

  • window = "3s" — first message in a bucket arms a 3-second timer
  • max_size = 50 — flush early if the bucket reaches this many messages
  • group_by = "channel" — one bucket per Slack channel
  • built-in summary: 📨 *N events:*\n• …\n• …

Tuning:

connector "slack_alerts" {
  type        = "slack"
  webhook_url = env("SLACK_WEBHOOK_URL")
  channel     = "#alerts"

  batch {
    window   = "5s"
    max_size = 100
    group_by = "channel"   # or "global"
    # Optional CEL summary. Vars: messages, count, channel, window.
    summary  = <<-CEL
      "🔔 *" + string(count) + " alerts in the last " + window + "*\n" +
      messages.map(m, "• " + m.text).join("\n")
    CEL
  }
}

Opting out (restores the pre-v2.5.0 immediate-send behavior):

connector "slack" {
  type        = "slack"
  webhook_url = env("SLACK_WEBHOOK_URL")

  batch {
    enabled = false
  }
}

Trade-offs to know about:

  • Latency adds up to window seconds (3s by default). For every-message-immediate use cases, opt out.
  • Messages with blocks or attachments, or thread replies (thread_ts), bypass batching automatically — collapsing them would lose structure or thread context.
  • Batching is in-process: a hard crash drops the buffered messages. Graceful shutdown (Close()) drains every bucket before exiting.
  • A 429 Too Many Requests response from Slack is retried once after the Retry-After delay (seconds or HTTP-date), capped at 30 seconds and honoring the request context. This complements batching: batching keeps you under Slack's soft "high volume" suppression; the 429 handler recovers the rare burst that still trips the hard rate limit.

Discord

Two modes: webhook (simple) or bot token (full Discord API). If both are set, webhook takes precedence.

# Webhook mode
connector "discord_webhook" {
  type        = "discord"
  webhook_url = env("DISCORD_WEBHOOK_URL")
  username    = "Mycel Bot"
  avatar_url  = "https://example.com/bot.png"
}

# Bot token mode — requires channel_id
connector "discord_bot" {
  type       = "discord"
  bot_token  = env("DISCORD_BOT_TOKEN")
  channel_id = env("DISCORD_CHANNEL_ID")
}

Connector Options

Option Type Required Default Description
webhook_url string yes* Discord webhook URL
bot_token string yes* Discord bot token (for API mode)
api_url string optional https://discord.com/api/v10 Discord API base URL
channel_id string optional Default channel ID (required for bot mode)
username string optional Display username (webhook mode only)
avatar_url string optional Display avatar URL (webhook mode only)
timeout duration optional 30s HTTP request timeout

* Either webhook_url or bot_token is required. Not both.

Transform Fields

Field Type Description
content string Message text (required)
username string Override display username
avatar_url string Override avatar URL
tts bool Text-to-speech
thread_name string Create a thread (forum channels)

Example

flow "notify_discord" {
  from {
    connector = "api"
    operation = "POST /notify/discord"
  }
  transform {
    content = "'New order: ' + input.body.order_id"
  }
  to {
    connector = "discord_webhook"
    operation = "send"
  }
}

Email

Three drivers: smtp, sendgrid, and ses (AWS Simple Email Service).

# SMTP
connector "email_smtp" {
  type     = "email"
  driver   = "smtp"
  host     = env("SMTP_HOST")
  port     = 587
  username = env("SMTP_USER")
  password = env("SMTP_PASS")
  from     = "notifications@example.com"
  tls      = "starttls"
}

# SendGrid
connector "email_sendgrid" {
  type    = "email"
  driver  = "sendgrid"
  api_key = env("SENDGRID_API_KEY")
  from    = "notifications@example.com"
}

# AWS SES
connector "email_ses" {
  type              = "email"
  driver            = "ses"
  region            = "us-east-1"
  access_key_id     = env("AWS_ACCESS_KEY_ID")
  secret_access_key = env("AWS_SECRET_ACCESS_KEY")
  from              = "notifications@example.com"
}

Connector Options (all drivers)

Option Type Required Default Description
driver string yes smtp, sendgrid, or ses
from string optional Default sender email address
from_name string optional Default sender display name
reply_to string optional Default reply-to address

SMTP Options

Option Type Required Default Description
host string yes SMTP server hostname
port int optional 587 SMTP port (25, 465, 587)
username string optional SMTP auth username
password string optional SMTP auth password
tls string optional starttls TLS mode: none, starttls, tls
timeout duration optional 30s Request timeout
pool_size int optional 5 Connection pool size

SendGrid Options

Option Type Required Default Description
api_key string yes SendGrid API key
endpoint string optional https://api.sendgrid.com API endpoint
timeout duration optional 30s Request timeout

AWS SES Options

Option Type Required Default Description
region string optional us-east-1 AWS region
access_key_id string optional AWS access key (falls back to default credential chain)
secret_access_key string optional AWS secret key
configuration_set string optional SES configuration set name
timeout duration optional 30s Request timeout

Transform Fields

Field Type Description
to list Recipients: [{email, name}] (required)
cc list CC recipients: [{email, name}]
bcc list BCC recipients: [{email, name}]
subject string Email subject (required)
textBody string Plain text body
htmlBody string HTML body
from string Override sender address
reply_to string Override reply-to
template_id string Provider template ID (SendGrid/SES)
template_data map Template variables
track_opens bool Enable open tracking
track_clicks bool Enable click tracking
tags list Email tags

Example

flow "send_welcome_email" {
  from {
    connector = "api"
    operation = "POST /notify/email"
  }
  transform {
    to       = "[{'email': input.body.email, 'name': input.body.name}]"
    subject  = "'Welcome, ' + input.body.name + '!'"
    htmlBody = "'<h1>Welcome!</h1><p>Your account is ready.</p>'"
  }
  to {
    connector = "email_smtp"
    operation = "send"
  }
}

HTML Email Templates

Set the template attribute on the connector to specify a default HTML template file. The template uses Go text/template syntax and receives the flow payload as its data context.

This is different from template_id, which references a provider-side template (e.g., a SendGrid dynamic template). With template, the rendering happens locally before sending.

The template can be set at two levels: 1. Connector config (recommended) — infrastructure detail, keeps flows focused on business data 2. Flow payload — override per-request for dynamic template selection

Level Field Description
Connector template Default HTML template file path
Flow payload template Per-request override (CEL expression)

Template variables use {{.FieldName}} syntax, where each field corresponds to a key in the transform payload.

Example

connector "order_email" {
  type     = "email"
  driver   = "smtp"
  host     = "${SMTP_HOST}"
  port     = 587
  template = "./templates/order_confirmation.html"
}

flow "send_order_confirmation" {
  from {
    connector = "api"
    operation = "POST /orders/:id/confirm"
  }

  step "order" {
    connector = "db"
    query     = "SELECT * FROM orders WHERE id = :id"
    params    = { id = "input.params.id" }
  }

  transform {
    to          = "[{'email': step.order.email, 'name': step.order.customer_name}]"
    subject     = "'Order #' + step.order.number + ' confirmed'"
    Name        = "step.order.customer_name"
    OrderNumber = "step.order.number"
    Total       = "string(step.order.total)"
  }

  to {
    connector = "order_email"
    operation = "send"
  }
}

templates/order_confirmation.html:

<h1>Thank you, {{.Name}}!</h1>
<p>Your order <strong>#{{.OrderNumber}}</strong> has been confirmed.</p>
<p>Total: ${{.Total}}</p>

SMS

Two drivers: twilio and sns (AWS Simple Notification Service).

# Twilio
connector "sms_twilio" {
  type        = "sms"
  driver      = "twilio"
  account_sid = env("TWILIO_ACCOUNT_SID")
  auth_token  = env("TWILIO_AUTH_TOKEN")
  from        = env("TWILIO_FROM_NUMBER")
}

# AWS SNS
connector "sms_sns" {
  type              = "sms"
  driver            = "sns"
  region            = "us-east-1"
  access_key_id     = env("AWS_ACCESS_KEY_ID")
  secret_access_key = env("AWS_SECRET_ACCESS_KEY")
  sms_type          = "Transactional"
}

Twilio Options

Option Type Required Default Description
account_sid string yes Twilio Account SID
auth_token string yes Twilio Auth Token
from string yes Sender phone number or messaging service SID
api_url string optional https://api.twilio.com Twilio API base URL
timeout duration optional 30s Request timeout

AWS SNS Options

Option Type Required Default Description
region string optional us-east-1 AWS region
access_key_id string optional AWS access key
secret_access_key string optional AWS secret key
sender_id string optional SMS sender ID
sms_type string optional Promotional or Transactional
timeout duration optional 30s Request timeout

Transform Fields

Field Type Description
to string Recipient phone number (required)
body string SMS message text (required)
from string Override sender number

Example

flow "send_verification_sms" {
  from {
    connector = "api"
    operation = "POST /notify/sms"
  }
  transform {
    to   = "input.body.phone"
    body = "'Your code is: ' + input.body.code"
  }
  to {
    connector = "sms_twilio"
    operation = "send"
  }
}

Push Notifications

Two drivers: fcm (Firebase Cloud Messaging) and apns (Apple Push Notification Service).

# Firebase Cloud Messaging
connector "push_fcm" {
  type       = "push"
  driver     = "fcm"
  server_key = env("FCM_SERVER_KEY")
}

# Apple Push Notifications
connector "push_apns" {
  type        = "push"
  driver      = "apns"
  team_id     = env("APNS_TEAM_ID")
  key_id      = env("APNS_KEY_ID")
  private_key = env("APNS_PRIVATE_KEY")
  bundle_id   = "com.example.myapp"
  production  = true
}

FCM Options

Option Type Required Default Description
server_key string yes Legacy FCM server key
project_id string optional Firebase project ID (v1 API)
service_account_json string optional Path to service account credentials
api_url string optional https://fcm.googleapis.com FCM API base URL
timeout duration optional 30s Request timeout

APNs Options

Option Type Required Default Description
team_id string yes Apple Developer Team ID
key_id string yes APNs auth key ID
private_key string yes APNs P8 private key content
bundle_id string optional App bundle identifier
production bool optional false Use production endpoint (vs sandbox)
api_url string optional https://api.push.apple.com APNs API base URL (overrides production/sandbox default)
timeout duration optional 30s Request timeout

Transform Fields

Field Type Description
token string Single device token
tokens list Multiple device tokens
topic string Topic-based messaging (FCM)
title string Notification title
body string Notification body
data map Custom data payload
priority string high or normal
ttl int Time-to-live in seconds
collapse_key string Collapsible notification key

Example

flow "send_push" {
  from {
    connector = "api"
    operation = "POST /notify/push"
  }
  transform {
    token = "input.body.device_token"
    title = "'New message'"
    body  = "input.body.message"
    data  = "{'order_id': input.body.order_id}"
  }
  to {
    connector = "push_fcm"
    operation = "send"
  }
}

Webhook

Two modes: outbound (send webhooks to external systems) and inbound (receive external webhooks). Outbound includes HMAC signature verification and automatic retries.

# Outbound — send webhooks
connector "webhooks_out" {
  type               = "webhook"
  mode               = "outbound"
  url                = env("WEBHOOK_TARGET_URL")
  method             = "POST"
  secret             = env("WEBHOOK_SECRET")
  signature_header   = "X-Webhook-Signature"
  signature_algorithm = "hmac-sha256"
  timeout            = "10s"

  retry {
    max_attempts = 3
    initial_delay = "1s"
    max_delay     = "30s"
    multiplier    = 2.0
  }
}

# Inbound — receive external webhooks
connector "webhooks_in" {
  type = "webhook"
  mode = "inbound"
  path = "/webhooks/events"
}

Outbound Options

Option Type Required Default Description
url string yes Webhook destination URL
method string optional POST HTTP method: POST or PUT
secret string optional Secret for HMAC signing
signature_header string optional X-Webhook-Signature Header name for the signature
signature_algorithm string optional hmac-sha256 Signing algorithm: hmac-sha256, hmac-sha1, none
include_timestamp bool optional true Include timestamp in signature computation
timeout duration optional 30s Request timeout
headers map optional Custom headers to include in every request

Retry Options (nested in retry block)

Option Type Required Default Description
max_attempts int optional 3 Maximum retry attempts
initial_delay duration optional 1s Delay before first retry
max_delay duration optional 30s Maximum delay between retries
multiplier float optional 2.0 Exponential backoff multiplier

Retries on: 408, 429, 500, 502, 503, 504.

Transform Fields

Field Type Description
payload any Request body (required)
url string Override destination URL
method string Override HTTP method
event_type string Value for X-Webhook-Event header
idempotency_key string Value for X-Webhook-ID header (defaults to UUID)

Automatic Headers

Every outbound webhook includes: - Content-Type: application/json - User-Agent: Mycel-Webhook/1.0 - X-Webhook-ID: <UUID or idempotency_key> - X-Webhook-Event: <event_type> (if set) - X-Webhook-Signature: <HMAC> (if secret is configured)

Example

flow "notify_partner" {
  from {
    connector = "rabbit"
    operation = "order.completed"
  }
  transform {
    payload    = "input.body"
    event_type = "'order.completed'"
  }
  to {
    connector = "webhooks_out"
    operation = "send"
  }
}

flow "receive_stripe" {
  from {
    connector = "webhooks_in"
    operation = "receive"
  }
  to {
    connector = "db"
    target    = "webhook_events"
  }
}

See the notifications example for a complete working setup.