Repository workflow
Your repository is the source of truth. waitcher.json declares one project, its environments and its monitors; test files hold the scripts; waitcher deploy makes one environment match the file.
waitcher config validate # offline, every environment
waitcher deploy plan --env production --verify # local test runs, then server checks
waitcher deploy apply --env production --based-on BASED_ON --smoke
Write tests
API and browser monitors are ordinary e2e tests in *.e2e.ts files. The project next to waitcher.json needs the exact framework versions the server runs:
waitcher runtime install # lists the steps, changes nothing
waitcher runtime install --yes # pins package.json, runs npm install, updates .gitignore
It installs Chromium when the file has browser entries (--browser forces it). npm install runs package install scripts, so read the steps before --yes. It is safe to repeat; rerun it after a CLI update to upgrade the project.
A browser test opens paths relative to the environment's baseUrl, so one test serves every environment:
import { test } from "@e2e-dev/web";
import { expect } from "e2e";
test("visitor can start a game", async ({ app, browser, screen }) => {
await app.open("/games/klondike/");
await expect(browser.locator("main h1")).toContainText("Solitaire");
await expect(screen.getByRole("button", "New game")).toBeVisible();
await app.screenshot("game");
});
screen finds elements the way a visitor does (getByRole, getByLabel, getByText, getByPlaceholder, getByTestId); browser navigates and offers CSS locators. API tests import test and expect from e2e and read the base URL from process.env.TARGET_URL.
Tests may import project files, JSON, Node builtins, e2e and @e2e-dev/web. Other packages fail the build (BUILD_FAILED); use fetch or project code instead. A built script must fit 50,000 characters.
Describe monitors in waitcher.json
{
"schemaVersion": 1,
"project": "Shop",
"environments": {
"production": { "baseUrl": "https://shop.example" },
"staging": { "baseUrl": "https://staging.shop.example" }
},
"defaults": { "interval": 1800, "timeout": 15 },
"monitors": [
{ "key": "home", "type": "http", "path": "/", "settings": { "contains": "Shop" } },
{ "key": "tls", "type": "certificate" },
{ "key": "checkout", "type": "browser", "entry": "monitoring/checkout.e2e.ts", "timeout": 45 },
{ "key": "orders-api", "type": "api", "entry": "monitoring/orders.e2e.ts",
"settings": { "envNames": ["CHECK_ORDERS_TOKEN"] } },
{ "key": "nightly-export", "type": "heartbeat", "environments": ["production"],
"heartbeat": { "graceSeconds": 300, "completionSeconds": 900 } }
]
}
keyidentifies the monitor: 1 to 63 lowercase letters, digits or hyphens. Changing a key replaces the monitor and loses its history;namedefaults to the key and can change freely.pathresolves against the environment'sbaseUrl. Script and certificate targets default tobaseUrl; heartbeats use the key as their label.environmentslimits an entry to some environments (default: all).- HTTP
settings:method,expectedStatus,contains,jsonPathwithjsonValue,maxLatency(ms). Certificates:certificateMinDays. - Never put
enabled,script,scriptFile,schemaVersion,contextordemoin an entry. Visual and AI monitors cannot be deployed from the file. waitcher config schemaprints the JSON Schema offline.- Limits: 256 KiB, 100 monitors, 10 environments.
Validate
waitcher config validate [--env NAME]
Offline: resolves every environment and builds every entry. Each issue has a code, a path such as monitors[2].entry and a hint. Exit 2 means something to fix.
Plan
waitcher deploy plan --env production --verify
A plan checks the file against your account on the server and changes nothing. It lists changes per key: create, adopt, update (with the changed fields), unchanged, delete, orphaned or invalid, plus issues, basedOn (the environment's latest deploy) and next (the exact follow-up commands).
--verify first runs every API and browser monitor of that environment locally against its target. Any failure stops there and sends nothing (exit 4, VERIFY_FAILED, evidence under .waitcher/runs). It uses your local environment variables, not stored secrets.
Apply
waitcher deploy apply --env production --based-on BASED_ON [--activate-new] [--smoke]
- The whole environment is applied or nothing is. An apply without changes records nothing, so repeating it is harmless.
--based-ontakesbasedOnfrom the plan you reviewed, ornonefor the first deploy. If another deploy landed since, apply refuses withCONFLICT: plan again.- New monitors start paused.
--activate-newschedules them immediately. Afterwards the server owns pause and resume, so a pause during an incident survives the next deploy. --smokeruns each created or updated monitor once on the server and waits up to--timeoutseconds (default 180). A failure leaves the deploy applied, exits 4 andnextpoints atrun diagnose. Smoke runs are ordinary manual runs: on a personal account only one fits each 10 minutes, so the others reportnot_startedwithRATE_LIMITED. Script smoke runs also count toward the daily script runs.- Monitors that are deployed but no longer declared are reported as
orphanedand keep running.--prunedeletes them and their history. - Monitors deployed from a file cannot be edited in the web app or with
monitor apply(MANAGED_BY_CONFIG). Change the file. Pause, resume and run once still work anywhere. - Each deploy records its version from the Git commit (
-dirtywith uncommitted changes);--source-versionoverrides it.
Roll back
waitcher deploy list --project-id PROJECT_ID
waitcher deploy rollback --id DEPLOY_ID --dry-run
waitcher deploy rollback --id DEPLOY_ID --based-on BASED_ON
The latest 10 deploys of each environment are restorable:true. A rollback reapplies that deploy's file and built scripts through the normal deploy path and is recorded as a new deploy. Secrets are not rolled back; current values stay. Revert the change in your repository too, or the next apply brings it back.
Adopt existing monitors
If you created monitors before using waitcher.json, a plan warns ADOPTION_CANDIDATE when an existing monitor looks like an entry. If it is the same check, link it:
waitcher deploy plan --env production --adopt home=MONITOR_ID
waitcher deploy apply --env production --adopt home=MONITOR_ID --based-on BASED_ON
The monitor keeps its ID, history and pause state, and takes its definition from the file. --adopt is repeatable. Without it, apply creates a second monitor.
Environment secrets
Scripts list the CHECK_* names they need in settings.envNames and read them from process.env. Values are stored per project environment, are write-only and never belong in waitcher.json:
printf %s "$VALUE" | waitcher secret set --env staging --name CHECK_ORDERS_TOKEN --value-stdin
waitcher secret set --env production --name CHECK_ORDERS_TOKEN --value-file ./private/orders-token
waitcher secret list --env staging
waitcher secret delete --env staging --name CHECK_ORDERS_TOKEN --yes
Secrets address the project by its name in waitcher.json, so you can set them before the first deploy. A plan reports SECRET_MISSING with the names that have no value in that environment. Values are up to 8 KiB, 50 per environment. For --verify, export the same CHECK_* variables in your shell (never in files). The Monitors page shows the same list, missing names first, when one project environment is selected, and can set a value there too.
How values are protected. A stored value is encrypted with a key kept outside the database, bound to your account, the environment and the name, and never shown again, not even to you. Setting the same name again replaces it (the revision goes up); runs already queued keep the value they started with. A hosted run receives only the names its monitor lists.
What evidence shows. Reports, traces and logs, hosted and local, show a listed value of at least 6 characters as <secret:CHECK_NAME>. Shorter values, values your script transforms (base64, part of a token) and pixels are not recognised: screenshots and video show whatever the page shows. To keep a password out of your test code and out of screenshots, fill it with the framework's handle instead of process.env; the run takes no screenshot after that fill:
import { secrets } from "e2e";
await screen.getByLabel("Password").fill(secrets.get("CHECK_LOGIN_PASSWORD"));