Skip to content
Docs/Set up in one command

Get started

Set up in one command

One command reads your repo and sets up monitors for the real endpoints, heartbeats for the real cron jobs, a status page and alerts. No account needed to start.

npx upbutler init

Run it in the project root (Node 18+ or Bun). It scans the repo, shows the plan, asks where alerts should go, and then:

  1. creates a workspace (no sign-up; the email you give gets a link to claim it),
  2. writes upbutler.yaml and applies it: HTTP monitors, heartbeats, a status page,
  3. inserts the heartbeat ping into each cron job where the place is unambiguous, after showing the diff,
  4. adds a short block to AGENTS.md (and CLAUDE.md if you have one) so the next coding-agent session knows the project is monitored.

In Claude Code with the UpButler plugin, type /upbutler:setup instead.

See the plan first

$ npx upbutler audit
acme-shop: Next.js, Vercel, Stripe, Supabase, OpenAI
Scanned 15 files: 10 routes, 1 scheduled job, 8 env var names (names only, never values).
Production URL: https://acme-shop.com (.env.example (NEXT_PUBLIC_SITE_URL))

What would break without you noticing (6):
  • The site goes down or its certificate expires  [✓ monitor homepage]
  • /api/health stops answering  [✓ monitor api-health]
  • Cron: daily-digest silently stops running  [✓ heartbeat cron-daily-digest]
  • Stripe webhooks stop arriving  [✓ heartbeat stripe-webhooks]
  • Supabase fails  [○ optional supabase-alive]
  • OpenAI fails  [○ optional openai-llm]

"upbutler init" would set up: 4 HTTP monitors, 2 heartbeats, 1 status page, 2 optional monitors (need keys).

init also writes repo: owner/name into the file, read from the git remote (override with --repo). It links incidents to commits and diffs. With --fix-with-claude it additionally turns on Fix with Claude: it asks for a fine-grained GitHub token, creates the responder and writes .github/workflows/upbutler-fix.yml (--fix-delivery local or routine for the other two ways).

upbutler audit (the same as init --dry-run) needs no account and makes no network request. It prints what would break without anyone noticing and what init would set up.

What it recognises

In the repoBecomes
Routes: Next.js (app and pages router), Remix / React Router, SvelteKit, Nuxt, Astro, Express, Hono, Fastify, NestJS, FastAPI, Flask, Django, Rails, Go (net/http, chi, gin, echo)HTTP monitors for the homepage, health endpoints and a few key public pages
Schedules: vercel.json crons, GitHub Actions schedule, node-cron, Celery beat, sidekiq-cron, Cloudflare cron triggers, Render cron jobsHeartbeats, with the period taken from the cron expression
A Stripe webhook routeA heartbeat that alerts when no webhook arrived for 3 days
Env var names such as SUPABASE_*, OPENAI_*, ANTHROPIC_*, OPENROUTER_*Optional monitors, commented out in the file because they need a key
DATABASE_URL, REDIS_URL, RESEND_*, POSTMARK_*Advice in the report: these cannot be checked from outside, so the health route should cover them
Deploy config (vercel.json, netlify.toml, fly.toml, wrangler.toml, render.yaml, railway.json, Dockerfile)The production URL, and a deploy hook to paste into the host

Never monitored: routes that only accept POST, PUT, PATCH or DELETE, routes behind a login (by path, middleware matcher or the handler's own auth check), and dynamic routes. The plan lists them under "Left alone" with the reason. Proposals stop at 10 monitors, the Free plan; raise it with --max-monitors.

Heartbeat pings in your code

app/api/cron/daily-digest/route.ts:9
+   // upbutler:heartbeat cron-daily-digest — tells UpButler this job ran (see upbutler.yaml)
+   await fetch("https://upbutler.com/hb/hb_…").catch(() => {});

A ping is inserted only at the end of a handler's success path: before the final return of a route handler, at the end of a node-cron callback or Celery task, or as a last step of a single-job workflow. When a handler returns from inside try blocks or branches, or the schedule calls a function defined elsewhere, init leaves the code alone and prints the snippet to paste. Re-running never inserts twice. Skip this step with --no-code-changes.

The ping URL contains the heartbeat token. It can only report that the job ran; if your repository is public, move it to an environment variable.

upbutler.yaml

upbutler.yaml
# yaml-language-server: $schema=https://upbutler.com/schemas/upbutler.yaml.json
version: 1
project: acme-shop
baseUrl: https://acme-shop.com

monitors:
  - id: homepage
    name: Homepage
    url: https://acme-shop.com/
    sslExpiryDays: 14
  - id: api-health
    name: API health (/api/health)
    url: https://acme-shop.com/api/health

heartbeats:
  - id: cron-daily-digest
    name: "Cron: daily-digest"
    schedule: 0 5 * * *
    periodSec: 86400
    graceSec: 3600
    source: app/api/cron/daily-digest/route.ts

statusPages:
  - id: status
    name: Acme Shop Status
    groups:
      - name: Website
        components:
          - id: homepage
            name: Website
            monitors: [homepage]

alerts:
  channels: [Email]          # by name; channels are created in the dashboard or with channels.create

deploy:
  guard: true
  monitors: [homepage, api-health]

responders: [fix-with-claude] # agent responders paged first

Commit it. Every entry has a stable id: renaming a monitor in the file renames it in UpButler and keeps its history. Only the fields you write are managed; anything you leave out keeps whatever was set in the dashboard. Monitor entries accept the same fields as the API. Editors that support the YAML language server get completion from /schemas/upbutler.yaml.json.

Error-rate monitors

monitors:
  - id: shop-errors
    kind: error_rate
    errorRate:
      provider: vercel            # vercel | netlify | cloudflare | generic
      include: ["/api/"]          # only these path prefixes (default: all paths)
      downAtPct: 5                # 5xx rate that means down
      degradedAtPct: 1
      minRequests: 20             # fewer requests in the window can't take it down
      secret: ${env:VERCEL_DRAIN_SECRET}   # optional: the drain's signature secret

An error-rate monitor (kind: error_rate) is fed by your host's log drain. The drain URL holds a secret token, so it is never written to the file: upbutler apply prints it once, when it creates the monitor (resources.monitors.<id>.drainUrl in the JSON output). Paste it at your hosting provider. Lost it? POST /monitors/:id/error-rate/rotate-token mints a new one.

A spend monitor is an entry with a spend object (dailyCapUsd, monthlyBudgetUsd, anomaly, sources, autoStopRuns). The source key runs works in any workspace; balance:… and provider:… keys are ids of one workspace.

Secrets

monitors:
  - id: openai-llm
    kind: llm
    url: https://api.openai.com/v1
    llm:
      model: gpt-5-mini
      apiKey: ${env:OPENAI_API_KEY}

${env:NAME} is filled in by upbutler apply from the environment of the shell or CI job, never from files. The value is stored encrypted and never returned. Changing a secret in the file is detected on the next apply.

Keep it in sync

upbutler plan                # what would change (nothing is written)
upbutler plan --exit-code    # CI: exit 2 when the file and the workspace differ
upbutler apply               # create and update; never deletes
upbutler apply --prune       # also delete what left the file
upbutler export -o upbutler.yaml   # existing workspace → file
$ upbutler plan
  ~ heartbeat  cron-daily-digest  changed: periodSec
  + monitor    page-docs
  ? monitor    page-login  no longer in the file (kept; --prune deletes it)
Plan: 1 to create, 1 to update, 0 to delete, 9 unchanged, 1 not in the file.
  • Idempotent. Applying the same file twice changes nothing.
  • Nothing is deleted unless you pass --prune, and prune only touches resources this project's file created or adopted. Monitors made by hand or by another repo are never candidates.
  • Several repos, one workspace. project scopes everything a file manages.
  • Already set up by hand? upbutler export writes the file from your workspace; applying it changes nothing and adopts the resources.
  • After adding an endpoint or job, run upbutler init again: it adds the new entries to the existing file and leaves the rest of it alone.

For agents

npx upbutler init --yes --email [email protected] --json

--yes asks nothing; --json prints one object with the changes, heartbeat ping URLs, status page URL, claim status, files changed and next steps. Without --email the claim link is in the output: show it to your human. Over MCP or REST the same reconcile is the setup_apply tool (POST /api/v1/setup/apply), which takes the file as JSON (config) or text (yaml), with dryRun and prune; setup_export is the reverse.

curl -X POST https://upbutler.com/api/v1/setup/apply \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"dryRun": true, "config": {"version": 1, "project": "acme", "monitors": [{"id": "homepage", "url": "https://acme.com/"}]}}'

Options

FlagMeaning
--urlProduction URL, when it cannot be inferred from the repo
--emailWhere alerts and the claim link go
--alertsA Slack, Discord or webhook URL to alert right away
--yes, --jsonNon-interactive, machine-readable
--dry-runReport only; no account, no request
--no-code-changesDo not edit job code; print the pings instead
--max-monitorsMost monitors to propose (default 10)
--dir, -fProject root and file name