notifications
Named channels that notify steps send into. rflow ships its own senders, so you don't need an external webhook relay.
rflow_version: 1
name: my-project
config:
port: 3947
db_connection: ${DATABASE_URL}
notifications:
channels:
dev:
console: {}
ops:
telegram:
bot_token: ${TG_BOT_TOKEN}
chat_id: ${TG_CHAT_ID}
eng:
slack:
webhook_url: ${SLACK_WEBHOOK}
alerts:
discord:
webhook_url: ${DISCORD_WEBHOOK}
pager:
pagerduty:
routing_key: ${PD_ROUTING_KEY}
oncall:
opsgenie:
api_key: ${OPSGENIE_API_KEY}
eu: true
sms:
twilio:
account_sid: ${TWILIO_ACCOUNT_SID}
auth_token: ${TWILIO_AUTH_TOKEN}
from: "+15550001111"
to: "+15552223333"
audit:
email:
from: "rflow ops <ops@example.com>"
to: [treasury@example.com]
smtp:
host: smtp.example.com
username: ${SMTP_USER}
password: ${SMTP_PASS}
workflows:
heartbeat:
trigger:
cron: { expression: "0 9 * * *" }
steps:
- id: ping
notify:
channel: dev
message: "rflow is up"Each channel configures exactly one sender (validation error otherwise).
console
The zero-setup sender: no credentials, nothing to reach. The rendered message
is written to rflow's log output (the rflow start stream) and journaled like
any other sender. Use it locally, then swap in a real channel. No fields:
console: {} and a bare console: both parse.
notifications:
channels:
dev:
console: {} telegram
| Field | Description |
|---|---|
bot_token | ✅ Bot token from @BotFather |
chat_id | ✅ Target chat/group id |
slack
| Field | Description |
|---|---|
webhook_url | ✅ An incoming webhook url |
discord
| Field | Description |
|---|---|
webhook_url | ✅ A channel webhook url |
pagerduty
Sends through the Events API v2
(events.pagerduty.com/v2/enqueue, event_action: trigger).
| Field | Description |
|---|---|
routing_key | ✅ The integration (routing) key of an Events API v2 integration |
Severity follows what fired the notification: notify: steps send error,
circuit breaker trips and
liveness/stall breaches send
critical, and approval requests send info.
opsgenie
Creates alerts via the Alert API
(Authorization: GenieKey <api_key>).
| Field | Description |
|---|---|
api_key | ✅ An API integration key |
eu | Use the EU host api.eu.opsgenie.com (default false = api.opsgenie.com) |
Priority follows the same taxonomy as pagerduty severity: circuit trips and
liveness/stall breaches are
P1, notify: steps P3, approval requests P5.
twilio
SMS through the Messages API (basic auth account_sid:auth_token).
| Field | Description |
|---|---|
account_sid | ✅ Account SID |
auth_token | ✅ Auth token |
from | ✅ The sending Twilio number (E.164, e.g. +15550001111) |
to | ✅ The destination number (E.164) |
One email: sender per channel: a shared envelope plus exactly one
transport, either plain SMTP or a first-party provider API (each provider is
its own small adapter; there is no generic "mail API" shape). Every credential
position is redacted like the other channel secrets.
| Field | Description |
|---|---|
from | ✅ The From header / envelope sender, e.g. rflow ops <ops@example.com> |
to | ✅ Recipient addresses (at least one) |
subject | Subject template — default rflow: <workflow> |
| transport | ✅ Exactly one of smtp | ses | sendgrid | mailgun | postmark | resend | brevo | gmail_api | microsoft_graph |
The message body is plain text (the rendered notify: message).
smtp
Works against any transactional provider's SMTP endpoint and internal MTAs.
| Field | Description |
|---|---|
host | ✅ SMTP server hostname |
port | 587/2587 = STARTTLS required (default 587). 465/2465 = implicit TLS. Any other port (25/2525/…): STARTTLS still required whenever credentials are configured; only the credential-less relay mode falls back to opportunistic TLS |
username / password | Optional PLAIN/LOGIN credentials. Omit both for an IP-allowlist relay (Google Workspace smtp-relay.gmail.com, internal MTAs) |
auth | plain (default when credentials are set) | xoauth2 — pass a caller-supplied access token; rflow never runs an OAuth consent flow |
token_command | Command whose stdout is the XOAUTH2 access token (the email-oauth2-proxy pattern) |
Never point it at port 25: direct port-25 is blocked outright on GCP,
throttled on AWS/Azure and blocklisted from residential IPs. Provider quirks:
Postmark has no implicit-TLS 465 (STARTTLS on 25/2525/587 only), and
several providers use a literal SMTP username (SendGrid: apikey, Resend:
resend, Postmark: the server token as both username and password).
Provider quickstarts
Each provider config is complete. Swap in your secret and go:
rflow_version: 1
name: my-project
config:
port: 3947
db_connection: ${DATABASE_URL}
notifications:
channels:
audit-email:
email:
from: "rflow ops <ops@example.com>"
to: [treasury@example.com]
# ── pick exactly ONE transport ──
sendgrid: { api_key: ${SENDGRID_KEY} }
# resend: { api_key: ${RESEND_KEY} }
# brevo: { api_key: ${BREVO_KEY} }
# postmark: { server_token: ${POSTMARK_TOKEN}, message_stream: outbound }
# mailgun: { api_key: ${MAILGUN_KEY}, domain: mg.example.com, region: us } # us | eu #
# ses: { region: us-east-1, access_key_id: ${AWS_KEY}, secret_access_key: ${AWS_SECRET} }
# gmail_api: { client_id: ${G_CLIENT_ID}, client_secret: ${G_SECRET}, refresh_token: ${G_REFRESH} }
# microsoft_graph: { tenant_id: ${MS_TENANT}, client_id: ${MS_CLIENT}, client_secret: ${MS_SECRET}, from_mailbox: ops@example.com }
workflows:
audit-report:
trigger:
cron: { expression: "0 9 * * *" }
steps:
- id: report
notify:
channel: audit-email
message: "daily treasury report"| Provider | API auth | Notes |
|---|---|---|
sendgrid | Bearer | SMTP alternative: username literally apikey |
resend | Bearer | rflow always sends the User-Agent Resend requires |
brevo | api-key header | new accounts need manual transactional activation before sends deliver (SMTP rejects with 535 until then); the SMTP key ≠ the API key |
postmark | X-Postmark-Server-Token | message_stream defaults to Postmark's outbound; no SMTP on 465 |
mailgun | Basic api:<key> | region: eu switches to api.eu.mailgun.net. A domain lives in ONE region |
ses | SigV4 (SESv2 SendEmail) | the ses: transport takes IAM keys; SES SMTP credentials are a different, region-specific pair. Use them with the smtp: transport instead. Sandbox accounts only reach verified recipients |
gmail_api | OAuth refresh token / service account | see below |
microsoft_graph | client credentials | see below |
Gmail and Google Workspace
Mailbox accounts suit low-volume ops notices; at volume use a transactional provider above.
- Consumer Gmail (app password +
smtp:) — raw account passwords have been dead since 2022. Enable 2-Step Verification, mint an app password (Google Account → Security → 2-Step Verification → App passwords), thensmtp: { host: smtp.gmail.com, username: you@gmail.com, password: ${APP_PASSWORD} }. App passwords are auto-revoked when the account password changes; caps at roughly 500 mails/day. - Workspace relay (no credentials at all) —
smtp-relay.gmail.comsupports IP-allowlist relaying (Admin console → Apps → Google Workspace → Gmail → Routing → SMTP relay service): omitusername/passwordand list your egress IP there. 10k/user/day. gmail_apiwith a refresh token — a GCP project + OAuth consent screen mint theclient_id/client_secret/refresh_tokentrio; rflow exchanges the refresh token at send time (it never runs the consent flow itself).gmail_apiwith a service account (Workspace) — the one fully non-interactive Google path: create a service account, enable domain-wide delegation for thegmail.sendscope in the Admin console, thengmail_api: { service_account_key: ${G_SA_JSON}, impersonate: ops@example.com }.
Microsoft 365 and Outlook.com
- Prefer
microsoft_graph. Basic SMTP AUTH on Microsoft 365 is disabled by default from the end of Dec 2026 (550 5.7.30) and is removed entirely in H2 2027. Register an app in Entra, grant the applicationMail.Sendpermission with admin consent, create a client secret, and setfrom_mailboxto the sending mailbox. - Scope the app with an
ApplicationAccessPolicy. ApplicationMail.Sendis tenant-wide send-as-anyone by default. Constrain the app registration to the one ops mailbox (New-ApplicationAccessPolicy -PolicyScopeGroupId ops@example.com ...). - Consumer Outlook.com is unsupported: password SMTP and app passwords died in Sept 2024 and Graph has no unattended path for consumer accounts. Route through any transactional relay instead.
Doctor checks
rflow doctor validates every configured email channel without sending a
message: SMTP gets a real connect + EHLO + STARTTLS negotiation + an AUTH
dry-run when credentials are present; the API transports validate the
credential against a no-send endpoint (SendGrid scopes, Mailgun domain,
Postmark server, Resend domains, Brevo account, SES GetAccount); Google and
Microsoft transports acquire a token and stop. Failure hints name the
provider-specific fix: 550 5.7.30 → switch to microsoft_graph, Brevo
535 → transactional activation pending, Gmail 535 → use an app password.
Using channels
rflow_version: 1
name: transfer-alerts
config:
port: 3948
db_connection: ${DATABASE_URL}
networks:
- name: ethereum
chain_id: 1
rpc: ${ETH_RPC}
contracts:
USDC:
abi: ./abis/erc20.json
addresses:
ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
notifications:
channels:
ops:
telegram:
bot_token: ${TG_BOT_TOKEN}
chat_id: ${TG_CHAT_ID}
workflows:
large-transfer:
trigger:
event:
contract: USDC
name: Transfer
network: ethereum
where: "${{ trigger.args.value > wei('1000000', 6) }}"
steps:
- id: alert
notify:
channel: ops
message: "large transfer: ${{ format_units(trigger.args.value, 6) }} USDC"Messages are full templates: any expression works, and
values under secrets: are redacted if they ever end up in one.