Cron is still the most reliable way to run something on a schedule on a Linux or macOS box, and the Nylas CLI turns “email me when this finishes” into one line. This recipe covers the shell-specific parts of sending mail from a script: how to schedule it with cron, how to read the exit code, where the output goes, how to keep a flaky job from sending five copies, and how to quote a body that contains apostrophes and dollar signs without the shell mangling it.
For the underlying send mechanics (how the API works, attachments, scheduled-send via the API), see send email without SMTP. This page assumes you already have the CLI authenticated.
How do I send one email from a bash script?
Section titled “How do I send one email from a bash script?”A single nylas email send call is all a script needs. Authenticate once with nylas auth config --api-key <key> (or set the key in the environment), then call the command non-interactively. The --yes flag skips the confirmation prompt so the script never hangs waiting for a human, and --json gives you a machine-readable result you can log or parse.
Set the API key in the environment rather than hardcoding it in the script. A leaked key in a checked-in file is the most common way these jobs go wrong. Pass --yes and --json on every automated send so the command runs in under 2 seconds without blocking on input.
#!/usr/bin/env bashset -euo pipefail
nylas email send \ --subject "Nightly backup finished" \ --body "The 02:00 backup completed without errors." \ --yes \ --jsonThat is the only send command this recipe shows. Everything below wraps it.
How do I schedule it with cron?
Section titled “How do I schedule it with cron?”A cron entry has 5 fields followed by the command: minute, hour, day-of-month, month, and day-of-week. Edit your table with crontab -e. The 5-field layout reads left to right, so 30 6 * * 1-5 means 6:30 AM, Monday through Friday. An asterisk means “every value” for that field.
Cron runs with a near-empty environment, so it will not find the nylas binary on its bare PATH, which is usually just 2 directories. Use the absolute path to the binary (which nylas tells you where it is) and set any variables your script needs at the top of the crontab.
# m h dom mon dow commandNYLAS_API_KEY=nyk_...PATH=/usr/local/bin:/usr/bin:/bin
# 6:30 AM on weekdays30 6 * * 1-5 /usr/local/bin/nylas email send --to "[email protected]" --subject "Daily standup reminder" --body "Standup at 9:30. Add blockers to the board." --yes >> /var/log/nylas-cron.log 2>&1Wrapping the send in a script file is cleaner than a long inline command, and it keeps the crontab readable.
How do I handle the exit code?
Section titled “How do I handle the exit code?”The CLI returns exit code 0 on success and a non-zero code on failure, so your script can branch on the result. With set -e a failed send aborts the script immediately, which is the right default for a job that should not continue after a send fails. When you want to react instead of abort, capture the status into a variable and test it.
Check $? right after the command, before any other line runs, because the next command overwrites it. A rate-limit response surfaces as a non-zero exit, so a back-off of around 60 seconds before one retry handles the common 429 case without a retry storm.
#!/usr/bin/env bashset -uo pipefail # note: no -e here, we handle errors ourselves
if nylas email send --to "[email protected]" --subject "Job done" --body "All green." --yes --json; then echo "sent ok"else status=$? echo "send failed with exit code $status" >&2 exit "$status"fiWhere does the output go and how do I log it?
Section titled “Where does the output go and how do I log it?”Cron captures whatever a job writes to stdout and stderr and, by default, mails it to the local user, which is rarely what you want. Redirect both streams to a log file with >> file 2>&1 so each run appends rather than overwrites. The 2>&1 part sends stderr to the same place as stdout, so errors and normal output land in one file.
Keep the log from growing without bound. A daily job that logs 1 line per run is harmless, but a per-minute job adds about 1,440 lines in 1 day, so rotate it with logrotate or trim it on a schedule. Timestamp each line so you can tell runs apart later.
set -euo pipefail
log() { echo "$(date '+%Y-%m-%dT%H:%M:%S') $*" >> /var/log/nylas-cron.log; }
log "starting send"# Capture the status with `|| status=$?` so a failed send still reaches the log line under `set -e`.nylas email send --to "[email protected]" --subject "Heartbeat" --body "Service is up." --yes --json >> /var/log/nylas-cron.log 2>&1 || status=$?log "finished with exit ${status:-0}"How do I stop a job from sending duplicates?
Section titled “How do I stop a job from sending duplicates?”Use a lock file to stop duplicate sends. Take the lock at the start of the script, release it on exit, and skip the run if the lock is already held. flock does this in 1 line and removes the race that a plain “does the file exist” check has.
A lock guards against overlap, but it does not guard against a job that already sent then crashed before recording success. For that, write a marker file keyed to the day (or run ID) and check it before sending, so a re-run within the same window of 24 hours becomes a no-op.
#!/usr/bin/env bashset -euo pipefail
exec 9>/tmp/nylas-send.lockflock -n 9 || { echo "already running, skipping"; exit 0; }
marker="/tmp/nylas-sent-$(date +%F)"[[ -f "$marker" ]] && { echo "already sent today"; exit 0; }
nylas email send --to "[email protected]" --subject "Daily report" --body "Numbers attached below." --yes --jsontouch "$marker"How do I quote a body with special characters?
Section titled “How do I quote a body with special characters?”Shell quoting decides what the --body value actually becomes, and getting it wrong is the top cause of garbled messages. Inside double quotes the shell still expands $variable and backticks, so a body containing a literal dollar amount or backtick gets rewritten before the CLI ever sees it. Single quotes are literal and expand nothing, which is safest for fixed text.
When the body mixes variables you do want and a $ you do not, build the string deliberately. The example below keeps $amount as a real value while protecting the literal text around it, so a body of about 200 bytes renders exactly as written. For multi-line bodies, a quoted here-document is the most readable option.
amount="49.99"
# Single quotes: nothing expands, safest for static text --subject 'Invoice ready' \ --body 'Your invoice is attached. Reply with questions.' --yes
# Mixed: $amount expands, the rest is protectedbody="Payment of \$$amount received. Thank you."
# Multi-line via here-documentread -r -d '' body <<'EOF' || trueHi there,
Your nightly job ran clean. No action needed.
- OpsEOFThings to know
Section titled “Things to know”- Cron’s
%is special. An unescaped%in a crontab line becomes a newline, which breaks dates and bodies. Escape it as\%or move the command into a script file, which sidesteps the issue. - Exit code 1 covers most failures. Auth errors, validation errors, and rate limits all surface as non-zero. Read the
--jsonoutput to tell them apart before retrying. - Test the schedule fast. Set a cron entry to
* * * * *(every minute), confirm 1 message arrives, then change it to the real schedule. Do not wait until 6 AM to find a typo.
Next steps
Section titled “Next steps”- Send from PowerShell on Windows - the same task in a Windows scheduled job
- Send email in CI/CD - running these sends inside a pipeline
- Nylas CLI - installation and command reference