# Send email from bash or cron

Source: https://developer.nylas.com/docs/cookbook/cli/send-email-bash-cron/

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](/docs/cookbook/use-cases/build/send-email-without-smtp/). This page assumes you already have the CLI authenticated.

## 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.

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

nylas email send \
  --to "ops@example.com" \
  --subject "Nightly backup finished" \
  --body "The 02:00 backup completed without errors." \
  --yes \
  --json
```

That is the only send command this recipe shows. Everything below wraps it.

## 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.

```cron
# m h dom mon dow  command
NYLAS_API_KEY=nyk_...
PATH=/usr/local/bin:/usr/bin:/bin

# 6:30 AM on weekdays
30 6 * * 1-5  /usr/local/bin/nylas email send --to "team@example.com" --subject "Daily standup reminder" --body "Standup at 9:30. Add blockers to the board." --yes >> /var/log/nylas-cron.log 2>&1
```

Wrapping 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?

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.

```bash
#!/usr/bin/env bash
set -uo pipefail   # note: no -e here, we handle errors ourselves

if nylas email send --to "ops@example.com" --subject "Job done" --body "All green." --yes --json; then
  echo "sent ok"
else
  status=$?
  echo "send failed with exit code $status" >&2
  exit "$status"
fi
```

## 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.

```bash
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 "ops@example.com" --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?

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.

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

exec 9>/tmp/nylas-send.lock
flock -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 "ops@example.com" --subject "Daily report" --body "Numbers attached below." --yes --json
touch "$marker"
```

## 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.

```bash
amount="49.99"

# Single quotes: nothing expands, safest for static text
nylas email send --to "billing@example.com" \
  --subject 'Invoice ready' \
  --body 'Your invoice is attached. Reply with questions.' --yes

# Mixed: $amount expands, the rest is protected
body="Payment of \$$amount received. Thank you."
nylas email send --to "billing@example.com" --subject "Receipt" --body "$body" --yes

# Multi-line via here-document
read -r -d '' body <<'EOF' || true
Hi there,

Your nightly job ran clean. No action needed.

- Ops
EOF
nylas email send --to "ops@example.com" --subject "Status" --body "$body" --yes
```

## 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 `--json` output 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

- [Send from PowerShell on Windows](/docs/cookbook/cli/send-email-powershell/) - the same task in a Windows scheduled job
- [Send email in CI/CD](/docs/cookbook/cli/email-in-cicd/) - running these sends inside a pipeline
- [Nylas CLI](https://cli.nylas.com/) - installation and command reference