# Did It Work? — User Guide

This is the complete product guide in plain Markdown, suitable for humans and LLMs.
Human-formatted version: https://diw.run/docs.html
Product overview: https://diw.run/llms.md

**Base URL:** `https://diw.run`

## Core concepts

- **Organization (org)** — your account. All jobs, keys, and users belong to an org.
- **Job (monitor)** — one thing you expect to run: a cron job, a pipeline, an agent task.
- **Run** — one execution of a job, with status, timing, and a timeline of events.
- **Ingest key** — a write-only machine token your jobs use to send pings. Sent as an
  `Authorization: Bearer <key>` header. Never usable to read data or log in.
- **Label** — a name defined by your org. Labels connect ingest keys to jobs: a key can
  only ping jobs it shares at least one label with.
- **Contact point** — where alerts go: an email address or a webhook.
- **Alert rule** — a condition on a job's events that opens an incident and notifies a
  contact point.
- **Incident** — a firing alert. It stays open until resolved (automatically or manually).

Signing in is passwordless: enter your email, click the magic link we send you.

---

## Job types

### Simple — one ping, "it ran"

Use when you only need to know **that** the job ran, not how long it took or why it
failed. One HTTP call at the end of the job is the entire integration. This is right
for most cron jobs: backups, certificate renewals, cleanup scripts, `cron`-driven
syncs.

Endpoint:

```
POST /ping/{job-id}
Authorization: Bearer <ingest-key>
```

Optional JSON body: `{"status": "...", "message": "...", "labels": {"key": "value"}}`.
All fields are optional — an empty POST is a valid heartbeat.

**Bash**

```bash
# at the end of your job, or chained in the crontab entry itself:
curl -fsS -X POST "https://diw.run/ping/YOUR_JOB_ID" \
  -H "Authorization: Bearer $DIW_INGEST_KEY"
```

```bash
# crontab example — ping only if the job succeeded
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -X POST "https://diw.run/ping/YOUR_JOB_ID" -H "Authorization: Bearer $DIW_INGEST_KEY"
```

**PowerShell**

```powershell
# at the end of your scheduled task
Invoke-RestMethod -Method Post `
  -Uri "https://diw.run/ping/YOUR_JOB_ID" `
  -Headers @{ Authorization = "Bearer $env:DIW_INGEST_KEY" }
```

**Python**

```python
import os, requests

requests.post(
    "https://diw.run/ping/YOUR_JOB_ID",
    headers={"Authorization": f"Bearer {os.environ['DIW_INGEST_KEY']}"},
    timeout=10,
).raise_for_status()
```

**Go**

```go
req, _ := http.NewRequest("POST", "https://diw.run/ping/YOUR_JOB_ID", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("DIW_INGEST_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
resp.Body.Close()
```

### Advanced — full run lifecycle

Use when you care about **how** the job ran: duration, success vs failure, exit codes,
and progress messages along the way. Advanced jobs report an explicit start and end,
so you also catch the case where a job starts and then hangs forever — something a
single end-of-job ping can never tell you.

Endpoints (in call order):

```
POST /ping/{job-id}/start                 → {"status":"accepted","run_id":"..."}
POST /ping/{job-id}/{run-id}/message      body: {"message":"...", "labels":{...}}
POST /ping/{job-id}/{run-id}/end          body: {"status":"success|failure", "message":"...", "exit_code":N, "labels":{...}}
```

`/start` returns a `run_id`; pass it to every later call. Messages are optional and
appear on the run timeline — use them for progress markers and for message-based
alerting (see Alerts below).

**Bash** (uses `jq` to read the run id)

```bash
BASE="https://diw.run/ping/YOUR_JOB_ID"
AUTH="Authorization: Bearer $DIW_INGEST_KEY"

RUN_ID=$(curl -fsS -X POST "$BASE/start" -H "$AUTH" | jq -r .run_id)

curl -fsS -X POST "$BASE/$RUN_ID/message" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"message":"extract finished, 1200 rows","labels":{"stage":"extract"}}'

./run-the-job
EXIT=$?
STATUS=$([ "$EXIT" -eq 0 ] && echo success || echo failure)

curl -fsS -X POST "$BASE/$RUN_ID/end" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"status\":\"$STATUS\",\"exit_code\":$EXIT}"
```

**PowerShell**

```powershell
$base    = "https://diw.run/ping/YOUR_JOB_ID"
$headers = @{ Authorization = "Bearer $env:DIW_INGEST_KEY" }

$run = Invoke-RestMethod -Method Post -Uri "$base/start" -Headers $headers

& .\run-the-job.ps1
$status = if ($LASTEXITCODE -eq 0) { "success" } else { "failure" }

Invoke-RestMethod -Method Post -Uri "$base/$($run.run_id)/end" -Headers $headers `
  -ContentType "application/json" `
  -Body (@{ status = $status; exit_code = $LASTEXITCODE } | ConvertTo-Json)
```

**Python**

```python
import os, subprocess, requests

BASE = "https://diw.run/ping/YOUR_JOB_ID"
HEADERS = {"Authorization": f"Bearer {os.environ['DIW_INGEST_KEY']}"}

run_id = requests.post(f"{BASE}/start", headers=HEADERS, timeout=10).json()["run_id"]

result = subprocess.run(["./run-the-job"])
status = "success" if result.returncode == 0 else "failure"

requests.post(
    f"{BASE}/{run_id}/end",
    headers=HEADERS,
    json={"status": status, "exit_code": result.returncode},
    timeout=10,
)
```

**Go**

```go
base := "https://diw.run/ping/YOUR_JOB_ID"
key := os.Getenv("DIW_INGEST_KEY")

post := func(path string, payload any) map[string]any {
	var buf bytes.Buffer
	if payload != nil {
		json.NewEncoder(&buf).Encode(payload)
	}
	req, _ := http.NewRequest("POST", base+path, &buf)
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	return out
}

runID := post("/start", nil)["run_id"].(string)

err := runTheJob()
status, code := "success", 0
if err != nil {
	status, code = "failure", 1
}
post("/"+runID+"/end", map[string]any{"status": status, "exit_code": code})
```

### Choosing a type

| You want to know | Use |
|---|---|
| "Did my cron job run today?" | Simple |
| "Did it run, how long did it take, did it fail, and why?" | Advanced |

You can pass `labels` on any event. Labels show up in the UI and can be matched by
alert conditions (e.g. alert only when `labels.severity` equals `critical`).

---

## Triggers and alerts

Alerting is configured per job in the **Alerts** panel (separate from job settings).
An alert rule reads like a sentence:

1. **When this happens** — the trigger event.
2. **And only if** — optional conditions on the event.
3. **Wait for** — optionally wait for a follow-up event within a window; alert only
   if it never arrives.
4. **Then do this** — the contact point to notify, the severity, and optional
   repeat notifications.
5. **Resolve when** — what closes the incident.

### Trigger events

| Event | Fires when | Job types |
|---|---|---|
| `job.heartbeat` | a simple ping arrives | simple |
| `job.heartbeat.missed` | a heartbeat did not arrive within the expected window | simple |
| `job.started` | a run starts | advanced |
| `job.message` | a message event arrives | advanced |
| `job.completed` | a run ends (carries `status: success` or `failure`) | advanced |
| `job.timed_out` | a run started but did not end within the job's timeout + grace | advanced |

### Conditions

Conditions narrow when a trigger actually fires (match **all** or **any**):

- completion status — `status eq success` / `status eq failure`
- exit code
- message text — `message contains "..."`
- labels — `labels.<key> eq <value>`

### Common recipes

- **Alert when a job fails**: trigger `job.completed`, condition `status eq failure`.
- **Alert when a cron job goes silent**: trigger `job.heartbeat.missed`. The check
  window is the expected interval **plus allowed runtime plus grace period** — the
  UI shows the full window so you know exactly when the alert will fire.
- **Alert when a run hangs**: trigger `job.timed_out` (or trigger `job.started`,
  wait for `job.completed` within a window).
- **Alert on log content**: trigger `job.message`, condition
  `message contains "ERROR"` — or annotate messages with labels and match
  `labels.severity eq critical`.
- **Alert if nothing ran this hour/day/week/month**: an activity-window rule fires
  at the end of a calendar window in which the job produced no activity.

### Notifications and incidents

- **Contact points** are managed org-wide (Alerting settings): **email** addresses
  or **webhooks** (your URL, method, and headers). Use the *test* button to send a
  sample delivery before relying on one.
- When a rule fires it opens an **incident**. Repeat notifications can re-notify at
  an interval, with an optional maximum count.
- Incidents resolve automatically on the rule's resolve event (e.g. the next
  successful run) — for `job.completed` failure alerts the incident resolves
  immediately since the run is already over. You can also resolve an incident
  manually from the UI.
- Every job shows an alert badge (none configured / healthy / firing), and the org
  view lists all firing incidents in one place.

Time fields accept human durations: `30s`, `5m`, `4h`, `7d`, `1M`.

---

## Users and roles

Under **Settings → Users** (owners only):

- **Invite** a user by email — they receive a magic link; no passwords to manage.
- **Change a role** or **remove** a user at any time.

| Role | Can do |
|---|---|
| `owner` | Everything: users, org settings, billing, plus all of the below |
| `job_maintainer` | Create/edit/delete jobs, labels, ingest keys, and alert rules |
| `member` | Read-only: view jobs, runs, logs, and alerting |

Give automation-adjacent teammates `job_maintainer`; keep `owner` for the people who
should control membership and billing. Use `member` for stakeholders who only need
visibility.

All configuration changes (users, keys, jobs, alerts, and any support access by our
staff) are recorded in a read-only **audit log** visible to owners under
**Settings → Audit log**, searchable by action and time.

---

## Ingest keys

Under **Settings → Ingestion** (owners and job maintainers):

- **Create a key** — give it a name and select one or more labels. The secret is
  shown **once** at creation; store it in your secret manager. We only keep a hash.
- **Use it** from your jobs as a Bearer header: `Authorization: Bearer <key>`.
  Keys are write-only — they can send pings and nothing else.
- **Revoke a key** — delete it; all pings using it stop being accepted immediately.
- **Rotate a key** — *replace* issues a new secret for the same key configuration,
  so you can swap secrets without rebuilding label assignments.

### Key permissions (labels)

Labels are the permission system for keys. A key can only ping a job when the two
share at least one label — a key with no labels can ping nothing.

Typical setup: create a label per team or per environment (`payments`, `staging`),
attach it to the relevant jobs, and issue each team/host a key carrying only its
label. Compromising one key then only exposes that slice of your jobs, and
revocation is one click.

---

## Organization settings

- **Rename your org** — **Settings → General** (owners). The name appears in the UI
  and in alert notifications.
- **Audit log** — see Users and roles above.

---

## Billing and payments

Billing lives under **Settings → Billing** (owners only). Payments are processed by
**Paddle**, our Merchant of Record — card details never touch our servers, and
Paddle handles invoicing and sales tax/VAT.

- **Plans are additive packages.** Every org starts on Free. Buying a package adds
  its job limits to your total; you can stack packages (e.g. two Gold packages
  doubles those limits) and mix plan tiers.
- **First purchase** opens a Paddle checkout overlay to enter payment details.
  Later purchases charge the subscription with immediate proration.
- **Update payment card** — the *update payment method* action opens a Paddle
  overlay to change the saved card. (Owners only.)
- **Invoices and history** — the transactions list shows all charges with
  downloadable invoices, plus any credit balance.
- **Removing a package** takes effect immediately; the unused remainder of the
  billing period comes back as a Paddle credit automatically applied to future
  invoices (it is not a card refund). You cannot remove a package while your
  current job count needs it — archive or delete jobs first. Removing your last
  package returns the org to the Free plan.
