Send server alerts from any box with the Nylas CLI

작성자

카테고리:

← 피드로
DEV Community · Qasim · 2026-06-25 개발(SW)

Most server alerting starts the same way: you curl a Slack webhook from a health-check script, or you point your monitoring SaaS at an SMTP relay, and you call it a day. That works right up until the thing you’re alerting about is the thing that breaks the alert. The box that can’t reach the internet can’t curl Slack. The relay credentials expire and every cron alert silently vanishes for a month before anyone notices. And the moment you’re on a bare VM, an air-gapped build node, or a customer’s on-prem box, “just install Postfix and configure SPF” turns a five-minute task into an afternoon.

What you actually want for a whole category of alerts — cron failures, backup reports, threshold breaches, deploy summaries, nightly audit digests — is dead simple: a real email address you own, that any machine with a single binary can send from, with no mail server to babysit. That’s what a Nylas Agent Account plus the Nylas CLI gives you, and that’s what this post builds.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. I’ll show both angles for every operation — the nylas command and the raw curl HTTP call — because in practice your scripts shell out to the CLI but your services hit the API, and you’ll end up wanting both.

One honest framing before we start: email is not a pager. If you need sub-minute, escalate-up-the-rotation, wake-someone-at-3am paging, that’s PagerDuty’s job and I’m not going to pretend otherwise. Email alerting owns the enormous middle band below paging — the stuff that’s important enough to land in an inbox and searchable history, but doesn’t justify a phone call. Cron jobs, batch pipelines, capacity warnings, deploy notifications, weekly reports. That band is where most alerting actually lives, and it’s chronically underserved by “I bolted on a Slack webhook.”

Why a Nylas Agent Account beats sendmail and a Slack hook

Here’s the part that makes this tractable: an Agent Account is just a grant. It has a grant_id, and that ID works with every grant-scoped endpoint Nylas already exposes — Messages, Drafts, Threads, Folders, Attachments. There’s nothing new to learn on the data plane. You provision a real [email protected] mailbox once, and from then on every machine just needs the CLI and an API key to send from it.

What that buys you over the usual options:

  • No SMTP server, no relay, no app passwords scattered across boxes. You’re not running Postfix or trusting a shared ESP relay. A box sends by calling an HTTPS API. If it can reach api.us.nylas.com, it can alert.
  • A real, replyable sender you own. Alerts come from your domain with your DKIM signature — not [email protected]. And because it’s a real mailbox, the on-call engineer can hit Reply and that reply lands somewhere your code can read. More on that later.
  • Identical from a laptop, a cron box, and a CI runner. The same nylas email send works everywhere the binary runs. No per-environment mail config drift.
  • Searchable history for free. Every alert lands in the account’s Sent folder automatically. That’s an audit trail of “what did we alert on, and when” without building one.

The honest tradeoff, stated plainly: don’t route your high-urgency paging through this, and don’t use it as a metrics backend. It’s an alerting and reporting channel, not Prometheus. Use it for what email is good at.

Before you begin

You need three things:

  1. The CLI. On macOS or Linux it’s a Homebrew tap:
   brew install nylas/nylas-cli/nylas

Enter fullscreen mode Exit fullscreen mode

  1. A Nylas API key. If you don’t have one, nylas init creates an account and mints a key in a single guided command. You can also pass an existing key non-interactively with nylas init --api-key <your-key>.

  2. A domain for the sender. Every Agent Account lives on a domain. For prototyping, Nylas hands out trial *.nylas.email subdomains, so you can create [email protected] immediately. For production, register a dedicated subdomain like alerts.yourcompany.com and publish the MX and TXT records Nylas gives you. New domains warm over roughly four weeks, so don’t register one the morning you flip alerting on.

Every API example below uses the US host https://api.us.nylas.com and a bearer token: Authorization: Bearer <NYLAS_API_KEY>. For the EU region, point at https://api.eu.nylas.com (the CLI honors the NYLAS_API_BASE_URL environment variable for the same purpose).

Provision the alerting identity

This is the only step specific to Agent Accounts, and it’s one line:

nylas agent account create [email protected] --name "Acme Alerts"

Enter fullscreen mode Exit fullscreen mode

The --name sets the display name, so the on-call sees Acme Alerts <[email protected]> instead of a bare address. The command prints the new grant’s id, status, and connector details — save that id, it’s the handle for every send below.

Under the hood the CLI is a thin wrapper over POST /v3/connect/custom with provider: "nylas". The same call your provisioning code makes directly:

curl --request POST 
  --url "https://api.us.nylas.com/v3/connect/custom" 
  --header "Authorization: Bearer <NYLAS_API_KEY>" 
  --header "Content-Type: application/json" 
  --data '{
    "provider": "nylas",
    "name": "Acme Alerts",
    "settings": {
      "email": "[email protected]"
    }
  }'

Enter fullscreen mode Exit fullscreen mode

The "provider": "nylas" is what marks this as an Agent Account rather than an OAuth grant — that’s why there’s no refresh token in the body. The response carries data.id; that’s your grant_id. There’s deliberately no --workspace flag on create — the API auto-creates a default workspace and policy for the account, and if you later want stricter limits you attach a custom policy with nylas workspace update <workspace-id> --policy-id <policy-id>.

Provision this once, store the grant ID somewhere your fleet can read it (config management, a secret store, an env var), and you never touch this step again.

The one-liner: a CPU alert

Here’s the whole point of the post in a single command. A threshold check fires, and you send:

nylas email send [email protected] 
  --to [email protected] 
  --subject "[CRITICAL] web-01 CPU at 96%" 
  --body "Load average 14.2 on web-01 (8 cores). CPU has been above 90% for 5 minutes." 
  --yes

Enter fullscreen mode Exit fullscreen mode

The first positional argument is the grant — the Agent Account’s email (or its grant_id). The --yes skips the interactive confirmation prompt, which is non-negotiable for anything running unattended in cron or CI; leave it off and the command hangs forever waiting for a keypress nobody’s there to give.

The same send over HTTP is POST /v3/grants/{grant_id}/messages/send:

curl --request POST 
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send" 
  --header "Authorization: Bearer <NYLAS_API_KEY>" 
  --header "Content-Type: application/json" 
  --data '{
    "to": [{ "email": "[email protected]" }],
    "subject": "[CRITICAL] web-01 CPU at 96%",
    "body": "Load average 14.2 on web-01 (8 cores). CPU has been above 90% for 5 minutes."
  }'

Enter fullscreen mode Exit fullscreen mode

That’s it. No SMTP handshake, no relay, no mail config. Put the host and the number in the subject — that’s what shows up in the notification and the inbox list, and an on-call engineer should be able to triage severity without opening the mail.

A real threshold script

One alert is a demo. What you actually deploy is a small health-check script that samples CPU, memory, and disk, and only emails when something crosses a line. The --body accepts HTML, so you can ship a readable metrics table instead of a wall of text. Here’s a Linux example you can drop in and adapt:

#!/usr/bin/env bash
set -euo pipefail

GRANT="[email protected]"
ONCALL="[email protected]"
HOST="$(hostname -f)"

# Sample metrics
CORES="$(nproc)"
LOAD="$(awk '{print $1}' /proc/loadavg)"
LOAD_PCT="$(awk -v l="$LOAD" -v c="$CORES" 'BEGIN{printf "%.0f", (l/c)*100}')"
MEM_PCT="$(free | awk '/Mem:/{printf "%.0f", $3/$2*100}')"
DISK_PCT="$(df --output=pcent / | tail -1 | tr -dc '0-9')"

# Thresholds
breaches=()
(( LOAD_PCT >= 90 )) && breaches+=("CPU ${LOAD_PCT}% (load ${LOAD} / ${CORES} cores)")
(( MEM_PCT  >= 90 )) && breaches+=("Memory ${MEM_PCT}%")
(( DISK_PCT >= 85 )) && breaches+=("Disk ${DISK_PCT}% on /")

# Nothing wrong? Stay quiet. The best alert is the one you don't send.
(( ${#breaches[@]} == 0 )) && exit 0

# Build an HTML body with a metrics table
rows=""
for b in "${breaches[@]}"; do rows+="<tr><td>⚠️ ${b}</td></tr>"; done
BODY="<h2>Threshold breach on ${HOST}</h2>
<table border='1' cellpadding='6'>
<tr><th>Metric</th></tr>${rows}</table>
<p>CPU ${LOAD_PCT}% · Memory ${MEM_PCT}% · Disk ${DISK_PCT}%</p>"

SEV="WARN"; (( LOAD_PCT >= 95 || MEM_PCT >= 95 || DISK_PCT >= 95 )) && SEV="CRITICAL"

nylas email send "$GRANT" 
  --to "$ONCALL" 
  --subject "[${SEV}] ${HOST}: $(IFS=,; echo "${breaches[*]}")" 
  --body "$BODY" 
  --metadata "key1=${SEV}" 
  --metadata "key2=${HOST}" 
  --yes

Enter fullscreen mode Exit fullscreen mode

A few things worth calling out. The script exits silently when everything is healthy — alert fatigue is a real outage risk, and a pipeline that emails “all good” every five minutes trains people to ignore it. The severity is computed from how far past the line you are, and it goes in the subject so triage is glanceable. And the body is HTML, so the table renders properly in any mail client.

Those --metadata flags are doing quiet but useful work. Nylas indexes five reserved metadata keys — key1 through key5 — and you can filter your sent alerts on them later. So tagging key1=CRITICAL and key2=web-01 means you can pull every critical alert from a host straight from the CLI:

nylas email list [email protected] 
  --folder SENT --metadata key1:CRITICAL --limit 50

Enter fullscreen mode Exit fullscreen mode

That’s your “what fired last night” query, no log aggregator required. (The metadata filter on email list only supports those key1key5 names, so name your alert tags accordingly rather than reaching for severity= and wondering why the filter ignores it.)

Wire it into cron

The script is the hard part; scheduling it is trivial. Run it every five minutes:

*/5 * * * * /usr/local/bin/alert-check.sh >> /var/log/alert-check.log 2>&1

Enter fullscreen mode Exit fullscreen mode

The CLI reads NYLAS_API_KEY from the environment, so the cleanest setup is to put the key in the crontab environment (or a sourced file with locked-down permissions) and keep it out of the script entirely:

NYLAS_API_KEY=nyl_xxxxxxxxxxxxxxxxxxxx
*/5 * * * * /usr/local/bin/alert-check.sh >> /var/log/alert-check.log 2>&1

Enter fullscreen mode Exit fullscreen mode

Now every box in the fleet that has the binary and that key is an alerting node. No per-host mail configuration, no relay credentials to rotate in fifty places.

Send alerts from your CI/CD pipeline

The same machinery fits CI. A failed nightly build, a deploy that finishes, a smoke test that regresses — all of it is a nylas email send away, and the env-var auth means it slots into any runner without an interactive login.

Here’s a GitHub Actions step that emails the team when a deploy job fails. Store the API key as a repository secret and expose it as NYLAS_API_KEY:

- name: Alert on deploy failure
  if: failure()
  env:
    NYLAS_API_KEY: ${{ secrets.NYLAS_API_KEY }}
  run: |
    brew install nylas/nylas-cli/nylas
    nylas email send [email protected] 
      --to [email protected] 
      --cc [email protected] 
      --subject "[DEPLOY FAILED] ${{ github.repository }} @ ${{ github.sha }}" 
      --body "Deploy failed on <b>${{ github.ref_name }}</b>.<br>Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" 
      --metadata "key1=deploy-failed" 
      --metadata "key2=${{ github.ref_name }}" 
      --yes

Enter fullscreen mode Exit fullscreen mode

The if: failure() gate means it only fires on a red build. The --cc pulls in the on-call alongside the release team — --cc and --bcc both take comma-separated addresses, same as --to. And because the body is HTML, you can drop in a clickable link straight to the failed run, which is the first thing anyone clicks.

If your pipeline auth needs the equivalent raw call (say you’re in a runner without Homebrew and would rather curl), it’s the same send endpoint — set the secret as an env var and reference it:

curl --request POST 
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send" 
  --header "Authorization: Bearer ${NYLAS_API_KEY}" 
  --header "Content-Type: application/json" 
  --data '{
    "to": [{ "email": "[email protected]" }],
    "subject": "[DEPLOY FAILED] build #4821",
    "body": "Deploy failed. See the pipeline for logs."
  }'

Enter fullscreen mode Exit fullscreen mode

Batch the noise: scheduled digests

Not every alert deserves to interrupt someone the instant it fires. Capacity trends, nightly backup results, weekly cost reports — those want to land at a predictable time, not whenever the cron happened to run. The CLI supports scheduled sends with --schedule, which takes a duration (2h), a clock time (tomorrow 9am), or an absolute timestamp:

nylas email send [email protected] 
  --to [email protected] 
  --subject "Nightly capacity digest — $(date +%F)" 
  --body "$DIGEST_HTML" 
  --schedule "tomorrow 8am" 
  --yes

Enter fullscreen mode Exit fullscreen mode

Over the API that’s the send_at field (a Unix timestamp) on the same send endpoint. Generate the digest whenever your batch job runs, but have it arrive when someone’s actually at their desk to read it.

Close the loop: make alerts repliable

Here’s the thing a Slack webhook can’t do: receive an answer. Because the alerting address is a real mailbox, the on-call engineer can reply to an alert — “ack, looking into it” or “false alarm, threshold’s too tight” — and that reply lands in the Agent Account’s inbox and fires the standard message.created webhook. Subscribe to it:

nylas webhook create 
  --url https://alerts-handler.yourcompany.com/webhooks/nylas 
  --triggers message.created

Enter fullscreen mode Exit fullscreen mode

The equivalent API call:

curl --request POST 
  --url "https://api.us.nylas.com/v3/webhooks" 
  --header "Authorization: Bearer <NYLAS_API_KEY>" 
  --header "Content-Type: application/json" 
  --data '{
    "trigger_types": ["message.created"],
    "webhook_url": "https://alerts-handler.yourcompany.com/webhooks/nylas"
  }'

Enter fullscreen mode Exit fullscreen mode

Now a reply to an alert can update a status board, silence a duplicate, or get logged against the incident. From the terminal you can watch the inbox while you build the handler — nylas email list shows what’s landed, and nylas email reply <message-id> --body "Acknowledged — rolling back now." answers in-thread, preserving the conversation. You’ve turned a one-way firehose into a two-way channel without standing up a single new service.

Guardrails

A few things I’d treat as non-negotiable before this runs across a fleet:

  • Stay quiet when healthy. The script above exits 0 with no send when nothing breaches. Do the same everywhere. An alerting channel that cries wolf gets filtered to a folder nobody reads, which is functionally the same as having no alerting at all.
  • Dedupe at the source. A box pinned at 96% CPU should not email every five minutes for an hour. Keep a small state file (last-alerted timestamp per metric) and suppress repeats inside a cooldown window. Email providers and humans both treat a hundred identical messages as spam.
  • Mind the send limits. On the free plan, an Agent Account is capped at 200 sends per account per day, with 3 GB of storage per organization. That’s plenty for sane alerting, but a tight-loop bug can blow through it — which is exactly why the cooldown above matters. Size your plan to your real alert volume before launch.
  • Keep the API key out of the repo and the script. Read it from the environment (NYLAS_API_KEY) sourced from your secret store or CI secrets. Never commit it, never echo it into a log.
  • Know the retention window. On the free plan the mailbox holds messages for 30 days (spam for 7); paid plans retain longer. It’s a searchable recent trail, not a compliance archive — if you need alerts kept for years, ship them to your log store too.
  • Pick the right region. If your org is on the EU data region, point everything at https://api.eu.nylas.com (or set NYLAS_API_BASE_URL). Sending from the wrong region is a confusing way to fail.

What’s next

The whole story here is that an Agent Account is just a grant, and the CLI is just a fast way to drive it. Once alerts@ exists, every machine you own is one binary away from sending real, owned, searchable, repliable email — from a cron line, a CI step, or a one-off shell command — with no mail server in sight.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다