# Monitors as code

Keep your monitors in openping.yml, next to the code they watch. They are reviewed like code, and the repo stays the source of truth.

## An example

This is the file one of our own products uses: a web check, an agent test, a heartbeat and an MCP monitor.

openping.yml

```yaml
service: taskos
defaults:
  regions: [mumbai, frankfurt, virginia]
  every: 1m
  alert: taskos-oncall

monitors:
  - name: Web app
    type: http
    url: https://tasks.supertuned.ai/api/health
    expect:
      status: 200
      latency: { under: 1500ms }

  - name: Chat answers
    type: agent
    endpoint: https://tasks.supertuned.ai/api/chat
    auth: { bearer: secret.TASKOS_TEST_TOKEN }
    every: 1h
    cases:
      - ask: "What's due today?"
        expect:
          - answered: { within: 20s }
          - not_contains: "error"
          - judge: "Lists the tasks due today, or says there are none."

  - name: Meeting notes read
    type: heartbeat
    schedule: "50 10 * * 1-5"
    timezone: Asia/Kolkata
    grace: 15m

  - name: OpenPing MCP
    type: mcp
    url: https://mcp.openping.ai
    auth: { oauth: client_credentials, secret: secret.OPENPING_MCP_CLIENT }
    expect:
      tools: { includes: [list_monitors, create_monitors] }
      on_change: alert
    call:
      tool: list_monitors
      args: { limit: 1 }
```

## The flow

```bash
openping init      # reads the project, writes openping.yml
openping test      # runs every monitor now, from this machine
openping deploy    # shows the plan, asks, then applies it
```

`deploy` prints what it will do before it does it: `+ add`, `~ change` and `- remove`. Only monitors that came from this file, in this service, are ever changed or removed. Monitors you made in the app are left alone.

## The file

- `service`: the name of the product or part these monitors belong to.
- `defaults`: `regions`, `every`, `alert` (an alert rule’s name) and `timeout`, used by any monitor that doesn’t set its own.
- `monitors`: up to 500. Each has a `name` and a `type`; the rest depends on the type.

| Key | Used by | Meaning |
| --- | --- | --- |
| `url`, `endpoint`, `host`, `domain` | All but heartbeat | What to check |
| `every`, `timeout`, `grace` | All | A duration: `30s`, `1m`, `1h`, `1500ms` |
| `regions`, `severity`, `tags`, `alert` | All | Where from, how loud, labels, and which alert rule |
| `method`, `headers`, `body` | http | The request to send |
| `auth` | http, mcp, agent | `{ bearer: secret.NAME }`, `basic`, `header`, or `{ oauth: client_credentials, secret: secret.NAME }` |
| `expect` | http, mcp, gateway | `status`, `latency`, `contains`, `not_contains`, `regex`, `json`, `header`, `cert_days`, `tools`, `on_change`, `first_token` |
| `cases`, `samples`, `target`, `protocol`, `model` | agent | The questions and how answers are judged |
| `schedule`, `timezone` | heartbeat | A cron expression and its time zone |
| `call` | mcp | The safe test call: `tool`, `args` |
| `models`, `api_key` | gateway | Which models get the tiny streamed reply |
| `records`, `mail` | dns | Record types to watch; also check SPF and DMARC |
| `port` | tcp, cert | The port to connect to |

A key the file doesn’t know is an error, not a silent skip. A mistake is reported with its place: `monitors[2].every: use a duration like 30s, 1m or 1h`.

## Secrets never go in the file

Write `secret.NAME` where a token goes. On our side the value comes from your organisation’s secrets. When you run `openping test` locally, `secret.NAME` is read from the environment variable `$NAME`. `deploy` tells you which secrets are missing before it applies anything.

## In CI

.github/workflows/openping.yml

```yaml
name: openping
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx openping test
      - run: npx openping deploy --yes
        env:
          OPENPING_API_KEY: ${{ secrets.OPENPING_API_KEY }}
```

See [the CLI](https://openping.ai/docs/cli) for every command.
