Monitoring as Code
Monitoring as Code lets you define your LoadFocus monitoring setup as version-controlled files and apply it from the command line or CI, the same way you manage infrastructure with Terraform or Pulumi. You describe the monitors, groups, alerts, maintenance windows, dashboards and status pages you want; the @loadfocus/monitoring CLI computes the difference against what is live and reconciles it (create, update, delete).
It is declarative and idempotent: running deploy twice changes nothing the second time. Your files are the source of truth, so changes go through pull requests and your monitoring history lives in git.
Everything runs inside your account and your active team, with your plan limits enforced by the LoadFocus backend exactly as in the dashboard. The CLI only does what you could do yourself in the UI.
How it works
Section titled “How it works”You keep a folder of small YAML (or JavaScript) files, one resource per file, plus a loadfocus.config.yaml that points at them. The CLI sends those definitions to LoadFocus, which maps them to live resources, diffs them, and returns a plan. You review the plan, then apply it.
- Author: describe resources as files (YAML or JS constructs).
- Plan:
deploy --dry-runshows exactly what will be created, updated, adopted or deleted. - Apply:
deployreconciles your account to match the files. - Reconcile identity: every resource carries a stable
logicalIdyou choose. That is how the CLI tracks a resource across renames, so changing a check's display name never recreates it.
Install
Section titled “Install”The CLI is a Node package (Node 18+). Run it on demand with npx:
npx @loadfocus/monitoring --help…or install it globally to get the loadfocus-monitoring command:
npm install -g @loadfocus/monitoringloadfocus-monitoring --helpAuthenticate
Section titled “Authenticate”The CLI authenticates with a LoadFocus API key and a team id. Find both on the Developers page, which shows your API key and your team id together, along with the MCP setup.
Sign in once and the credentials are saved to ~/.loadfocus/config.json:
loadfocus-monitoring loginloadfocus-monitoring whoami # confirm who you are and which team you're targetingFor CI, prefer environment variables (they override the saved config and never touch disk):
export LOADFOCUS_API_KEY="apikey_xxxxxxxx"export LOADFOCUS_TEAM_ID="team_xxxxxxxx"# optional: export LOADFOCUS_API_URL="https://apimonitor.loadfocus.com"Create a project
Section titled “Create a project”Scaffold a config file and a sample monitor in your repository:
loadfocus-monitoring initThis writes loadfocus.config.yaml:
project: my-project # a namespace for this set of resourcescheckMatch: - "monitors/**/*.{check,group,alertRule,maintenanceWindow,dashboard,statusPage,alertChannel,variable}.{yaml,yml,js}"defaults: schedule: "300" # applied to checks that omit a schedule locations: [us-east-1]projectscopes everything the CLI manages. Resources in a project are reconciled together; anything in the project that is no longer in your files is deleted ondeploy. Use separate projects to manage independent sets of monitors.checkMatchis the glob(s) for your authoring files.defaultsfill inschedule,locationsandalertChannelsfor checks that omit them.
The workflow
Section titled “The workflow”loadfocus-monitoring validate # compile locally + server-side dry-run; great as a PR gateloadfocus-monitoring deploy --dry-run # show the plan (created / updated / adopted / deleted)loadfocus-monitoring deploy # apply itloadfocus-monitoring list # inventory of what's deployed in the projectloadfocus-monitoring list --status # …with each check's latest up/down/degraded statusloadfocus-monitoring get <logicalId> # show one deployed resourceloadfocus-monitoring trigger --id <logicalId> # run a check now (no --id runs every deployed check)loadfocus-monitoring destroy # delete everything managed in the projectdeploy is safe by default: it shows the plan and, when run interactively, asks before deleting anything. In CI (non-interactive), it refuses to delete without --yes and exits with a clear code instead of hanging on a prompt. Add --json to read/result commands for machine-readable output.
Adopt existing monitors
Section titled “Adopt existing monitors”Already have monitors in the dashboard? Pull them into files instead of recreating them:
loadfocus-monitoring import --project my-project --out monitorsThis writes one file per resource and a loadfocus.config.yaml. Review, commit, then run deploy --dry-run: matching resources are adopted in place (brought under management) rather than duplicated.
Resources
Section titled “Resources”Every resource is one file with a kind, a logicalId (your stable identifier), and the fields for that kind. References between resources use logicalIds (or names for alert channels), the server resolves them, and deploy order is handled for you.
Checks
Section titled “Checks”One check kind covers every check type via type: api, browser, multistep, tcp, heartbeat (in JavaScript the Monitor construct does the same).
kind: checktype: apilogicalId: homename: Home APIschedule: "300" # seconds between runslocations: [us-east-1, eu-west-1]request: url: "https://example.com/health" method: GETassertions: - { type: statusCode, comparison: equals, value: 200 } - { type: responseTime, comparison: less-than, value: 1000 }- api - HTTP request with assertions on status code, response time and body. Comparisons are
equals,not-equals,greater-than,less-than,equals-or-greater-than,equals-or-less-than,containsandnot-contains. - browser: a Playwright user-flow script with screenshots and per-step timings (paid).
- multistep - an ordered sequence of API requests passing data between steps (paid).
- tcp - a port/reachability check from multiple regions (paid).
- heartbeat: a dead-man's switch: an external job pings a URL on a schedule, and LoadFocus alerts if a ping is missed.
Groups
Section titled “Groups”Share locations, alert channels, frequency and activation across many checks. A check joins a group with group: <logicalId>.
kind: grouplogicalId: webname: Web serviceslocations: [us-east-1, eu-west-1]Alert rules
Section titled “Alert rules”Alert when a check's metric crosses a threshold.
kind: alertRulelogicalId: home-latencyname: Home API latencycheck: home # reference a check by logicalIdmetric: responseTime # responseTime | statusCode | durationcondition: aboveconditionValue: 1500 # millisecondsAlert channels
Section titled “Alert channels”Manage notification channels as code and reference them by name from a check, group or alert rule. Supported types: email, slack, microsoftteams, webhook, discord, pagerduty, opsgenie. Email, Slack, Teams and webhook channels are on every plan; Discord, PagerDuty and Opsgenie are paid-only. Secret fields (webhookUrl, routingKey, apiKey) take a {{secrets.NAME}} reference, the value is stored with env set-secret and resolved when an alert is sent, never committed to your files.
kind: alertChannellogicalId: oncall # the name checks / groups / alert rules referencetype: pagerdutyroutingKey: "{{secrets.PAGERDUTY_KEY}}"Maintenance windows
Section titled “Maintenance windows”Suppress alerts during planned work. Times are UTC. startsAt / endsAt accept an ISO-8601 string (e.g. "2026-07-01T00:00:00Z") or unix milliseconds.
kind: maintenanceWindowlogicalId: weekly-deployname: Weekly deploy windowenabled: truestartsAt: "2026-07-01T00:00:00Z" # ISO-8601 or unix msendsAt: "2026-07-01T02:00:00Z"repeat: weekly # none | daily | weekly | monthlyweekdays: [2] # 0=Sun … 6=Sattargets: allChecks: false checkIds: [home] # by logicalIdDashboards
Section titled “Dashboards”A shared view of selected checks, optionally public via a slug.
kind: dashboardlogicalId: status-overviewname: Status overviewvisibility: private # private | publicchecks: [home] # by logicalIdwindow: 24h # 24h | 7d | 30dStatus pages
Section titled “Status pages”A public status page at <slug>.loadfoc.us, optionally on your own custom domain.
kind: statusPagelogicalId: public-statustitle: Acme Statusslug: acme # -> acme.loadfoc.us (globally unique)enabled: truecustomDomain: status.acme.com # optional, paid; point a CNAME at cname.loadfoc.usgroups: - { id: core, name: Core Services, order: 0 }components: - id: api name: API groupId: core monitors: [home] # checks shown on this component, by logicalIdbranding: brandColor: "#5353a4" colorTheme: darkA custom domain goes live once you create the CNAME and the certificate is issued, deploy declares it; verification happens out of band.
Variables
Section titled “Variables”Non-secret values (base URLs, IDs) that API checks reference at run time as ${{NAME}} in the URL, headers, query parameters and body, the same placeholder as secrets. The logicalId is the variable key. (For secrets, use env set-secret, never put them in files.)
kind: variablelogicalId: BASE_URLvalue: "https://api.example.com"Authoring in JavaScript or TypeScript
Section titled “Authoring in JavaScript or TypeScript”If you prefer code over YAML, build the same definitions programmatically and export them, the constructs produce identical resources:
const { Monitor, Group, AlertRule, Maintenance, Dashboard, StatusPage, AlertChannel, Variable } = require('@loadfocus/monitoring');
module.exports = [ new Monitor({ type: 'api', logicalId: 'home', name: 'Home API', schedule: '300', locations: ['us-east-1'], request: { url: 'https://example.com/health', method: 'GET' }, assertions: [{ type: 'statusCode', comparison: 'equals', value: 200 }], }), new Group({ logicalId: 'web', name: 'Web services', locations: ['us-east-1'] }),];Export the constructs from the file (an array, as above) and name it so checkMatch picks it up, for example monitors/home.check.js. The CLI loads it like any other resource.
Secrets and variables
Section titled “Secrets and variables”Reference values from your checks without committing them. Secrets (tokens, passwords) are managed imperatively only and referenced as ${{NAME}} in check fields (URL, headers, body) and as {{secrets.NAME}} in alert-channel secret fields. Variables (non-secret) can be declared as files (kind: variable, above) or set imperatively, and are referenced the same way in API check fields, as ${{NAME}}; in those fields a placeholder without the dollar sign, such as {{NAME}}, is sent as plain text. A multistep check passes values between steps instead: its setup script reads the variable as ${{NAME}} and stores it in ctx.vars, and the steps use it as {{name}} (see Setup and teardown scripts).
loadfocus-monitoring env set-secret API_TOKEN "s3cr3t"loadfocus-monitoring env set-var BASE_URL "https://example.com"loadfocus-monitoring env ls # list secret + variable keys (values never shown)Run it in CI
Section titled “Run it in CI”A typical pipeline validates on every pull request and deploys on merge to the main branch.
name: monitoringon: pull_request: push: branches: [main]jobs: monitoring: runs-on: ubuntu-latest env: LOADFOCUS_API_KEY: ${{ secrets.LOADFOCUS_API_KEY }} LOADFOCUS_TEAM_ID: ${{ secrets.LOADFOCUS_TEAM_ID }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npx @loadfocus/monitoring validate - if: github.ref == 'refs/heads/main' run: npx @loadfocus/monitoring deploy --yesThings worth knowing
Section titled “Things worth knowing”logicalIdis the identity. Keep it stable. You can rename a check'snameortitlefreely; changing itslogicalIdis treated as deleting one resource and creating another.- Deletes are scoped to the project.
deployonly removes resources in the currentprojectthat are no longer in your files, never anything in another project or created outside Monitoring as Code (until you adopt it). - Status-page slugs are global.
slugbecomes a subdomain, so it must be unique across all LoadFocus customers. - Paid features need a paid plan. A free team that declares a status-page custom domain gets a clear error on
deploy. Removing the "Powered by" badge is not applied on a free plan: the field stays off. - Plan limits apply. Creating resources via the CLI is subject to the same plan quotas as the dashboard. A resource that would go over a quota is reported as rejected and the rest of the deploy continues.