The SMTP driver sends a real email per event — multipart/alternative
with both text/plain and text/html parts, so Gmail/Outlook render
the rich layout and terminal MUAs see a usable fallback. It works
with any SMTP relay: Gmail (with an app password), Amazon SES,
Mailgun, Postmark, SendGrid, or your own Postfix on 127.0.0.1.
id, type, host, from, and at least one to are required.
Credentials are optional — leave username and the password fields
unset to talk to a local relay (Postfix listening on
127.0.0.1:25, an internal MTA, etc.). When username is set,
exactly one of password, password_env, password_file must
also be set — the secret rule.
Key
Required
What it does
host
yes
SMTP relay hostname (e.g. smtp.gmail.com).
port
no
TCP port. Defaults to 25; common values: 587 (STARTTLS), 465 (implicit TLS).
tls
no
"starttls" (default for 25/587), "implicit" (default for 465), "none".
tls_skip_verify
no
Skip TLS certificate verification. Do not set this in production.
username
no
SMTP AUTH username. Required when sending creds.
password
one-of (see below)
Inline password. Avoid in committed configs.
password_env
one-of
Name of an env var holding the password.
password_file
one-of
Path to a file containing the password. Relative paths resolve under the data dir.
A relay you already trust on the same host — no auth, no
TLS. The daemon refuses to send credentials over cleartext,
so tls = "none" only works alongside an auth-less relay.
Useful when a secrets manager (Vault agent, sops, Docker
secrets at /run/secrets/...) writes the password to a
known path for you. Relative paths resolve under the data
dir.
Identical to Slack and Telegram — the routing layer does not care
which driver is on the other end:
# On one task:
[tasks.backup-postgres]
notify_on_failure = ["email-ops"]
# …
# Or in a notification rule:
[[notification_route]]
match = { kind = ["run.failed", "run.timeout", "run.crashed"] }
notify = ["email-ops"]
Inline recipient overrides reuse one credential set across many
destinations — notify_on_failure = ["email-ops:[email protected]"]
sends through the email-ops relay but to [email protected]
instead of the parent notifier’s to list:
runwispexecsmoke-test# something that exits non-zero
Emails typically arrive in a few seconds. If yours doesn’t:
Check the bell for a notify.delivery_failed event with the
underlying SMTP error.
Common causes: wrong port for the chosen tls mode (Gmail
needs 587 + STARTTLS; SES on 465 needs tls = "implicit"),
expired app password, recipient address rejected by the relay
(550 No such user).
Spam folder: the first message from a new sender often lands
in spam. Whitelist the from address.
The default template renders one message per event. A failure looks
like:
❌ backup-postgres failed
Exited with code 1 after 0.3s.
Scheduled run · 14 May, 17:11.
Error: connection refused
dial tcp 127.0.0.1:5432: connect:
connection refused
[View full run]
from runwisp · bright-falcon
The captured-output block appears on run.failed and
run.timeout only, capped at three lines / 300 bytes.
The View run link points at
<external_url>/tasks/<task>?runId=<id>, taken from
[daemon] external_url.
When external_url is unset the link is omitted — no broken
anchors.
The fingerprint footer identifies the specific daemon that
emitted the event.
The plain-text alternative is derived from the HTML body, so
terminal MUAs (mutt, mail) see the same content without markup.
template_path overrides the embedded template if you need a
different layout. Copy
smtp.tmpl.html
as your starting point. The helpers statusEmoji, statusVerb,
humanTime, humanDuration, runDuration, triggerPhrase,
eventSentence, eventTrigger, linkLabel, runURL, taskURL,
outputTail, htmlEsc, and fingerprint are available inside it.