Automating cold-email ops checks with the Norbelys CLI in CI
A working GitHub Actions example: install the norbelys CLI, authenticate with $NORBELYS_API_KEY, and run a scripted campaign-config check on every push or schedule.
By Gabriel Lara, Developer Relations, Norbelys
Founder-reviewed ·How we research and correct articles
Some checks against your Norbelys workspace are worth running the same way every time, on a schedule, without a person remembering to do it. “Does every active sender still have valid credentials?” “Is there a program configured with no sender attached to it?” Those are exactly the kind of question a CI job answers better than a human does — a human forgets to check on a Friday; a scheduled job doesn’t.
This post walks through wiring the norbelys CLI
into a GitHub Actions pipeline, authenticated with $NORBELYS_API_KEY, running
a real scripted check. For the broader question of when a script beats
clicking through the dashboard at all,
that’s a separate post — this one assumes you’ve already decided the check
is worth automating and just want it working.
Why $NORBELYS_API_KEY specifically
The CLI resolves its bearer credential from an environment variable before
it looks at anything saved on disk — that’s covered in more depth in the
auth deep-dive, but the
practical consequence for CI is simple: you never run norbelys login
inside a pipeline. You set one secret, export it as NORBELYS_API_KEY, and
every command in that job picks it up automatically. There’s no config file
to persist between runs, no interactive step, nothing that depends on a
browser existing on the runner.
Setting up the secret
Generate an org-scoped API key from the dashboard the same way you would for
any script, and store it as a repository (or environment) secret in GitHub
— Settings → Secrets and variables → Actions. Name it
NORBELYS_API_KEY so it lines up with what the CLI already looks for.
Treat it like any other production credential: scope it to the org it belongs to, don’t echo it in logs, and rotate it if a runner’s secret store is ever exposed. None of that is CLI-specific advice — it’s the same hygiene you’d apply to a database URL or a payment-provider key — but it’s worth restating because a CI secret is easy to set once and then forget exists.
What makes a good check to automate
Not every question is worth a pipeline. A good candidate for this pattern has three properties: it has a clear pass/fail answer (not “does this look right,” which needs a human), it’s worth catching before something depends on it being true (a broken sender before a send window opens beats finding out after), and it would otherwise rely on someone remembering to check manually. “Every sender is healthy,” “every active program has a sender attached,” and “a domain’s verification hasn’t silently lapsed” all fit that shape. “Does this campaign’s copy sound right” doesn’t — that’s a dashboard task, not a CI one, precisely because it needs a person reading it.
A realistic workflow
Here’s a complete GitHub Actions workflow that runs a scripted check on
every push to main and again on a nightly schedule, so a broken
configuration gets caught either at merge time or overnight, whichever
happens first:
name: norbelys-ops-check
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * *" # 06:00 UTC daily
jobs:
verify-senders:
runs-on: ubuntu-latest
env:
NORBELYS_API_KEY: ${{ secrets.NORBELYS_API_KEY }}
steps:
- name: Install the norbelys CLI
run: curl -fsSL https://cli.norbelys.com/install | sh
- name: List senders as JSON
run: |
~/.local/bin/norbelys senders list --json > senders.json
- name: Fail if any active sender is unhealthy
run: |
set -e
UNHEALTHY=$(jq '[.[] | select(.status != "healthy")] | length' senders.json)
if [ "$UNHEALTHY" -gt 0 ]; then
echo "::error::$UNHEALTHY sender(s) not healthy"
jq '[.[] | select(.status != "healthy")]' senders.json
exit 1
fi
echo "All senders healthy."
The mechanics generalize past sender health. The same three steps —
install, run a command with --json, assert on the output with jq — work
for any generated command: verifying every active program has at least one
sender attached before a send window opens, checking that a suppression
list import landed the row count you expected, or confirming a domain’s
verification status before a campaign that depends on it goes live.
Try it locally before you trust it in CI
Debugging a failing GitHub Actions run through log output alone is slow.
Run the same commands locally with the same environment variable set, so
you’re iterating on the jq filter directly instead of pushing commits to
see what breaks:
Once the filter produces the exit code you expect against real data, paste the same command into the workflow file. The CI run should now be reproducing a check you’ve already watched work on your own machine, not one you’re debugging for the first time in a runner’s logs.
Scheduling frequency and rate limits
Resist the urge to run an ops check every five minutes “just to be safe.” A daily or hourly cadence catches configuration drift long before it costs you anything real, and it keeps your automated traffic well clear of anything that could look like abuse to the API’s own rate limiting. If you’re running the same check across several client orgs — an agency pattern — stagger the schedule or explicitly target one org’s key per job rather than looping through all of them inside a single tight script.
Beyond a single check: gating a deploy or a launch
The pattern in this post is a standalone scheduled job, but the same three steps drop cleanly into a pre-deploy or pre-launch gate too — a workflow step that runs before a campaign activation job, exits non-zero on a problem, and blocks the rest of the pipeline from proceeding. That’s the real payoff of scripting an ops check instead of a person eyeballing a dashboard before hitting “launch”: the check runs identically every time, and it runs even on the Friday afternoon nobody wants to be doing it manually.
It’s not GitHub-Actions-specific
Nothing above depends on GitHub Actions particularly — the CLI is a small
bundle that runs anywhere Bun or Node runs, so the same three steps
(install, authenticate via env var, assert on --json output) work
unchanged in GitLab CI, CircleCI, Buildkite, or a plain cron entry on a box
you manage yourself. The only thing that changes between platforms is the
syntax for declaring a secret and a schedule; the actual norbelys ...
invocation in the run step is identical everywhere, which is a reasonable
sanity check that you haven’t accidentally hard-coded a GitHub-specific
assumption into the check itself.
Next steps
- Install the CLI and authenticate if this is your first time setting it up.
- API key vs. browser OAuth —
why CI always uses the API-key path, and where
$NORBELYS_API_KEYfits into credential resolution. - CLI vs. dashboard: when to script for the broader case on when automating a check is worth the setup cost.
- The OpenAPI spec explained for the full resource and field reference behind whatever check you build next.
- The developer portal for the full API surface, if you want to see everything else that’s scriptable beyond senders.