# Jobs, runs and attempts

A **job** says what to do and when. Each time it's due, that is an **occurrence**, recorded as one **run**. Each try at delivering a run is an **attempt**. A run that fails twice and then succeeds is one run with three attempts.

## Workspaces

Everything belongs to a **workspace**: jobs, API keys, secrets, alert channels and rules. Every user gets a personal workspace when they first sign in. API keys are bound to one workspace, so a key can never see another workspace's jobs.

## Jobs

A job has:

| Part | Example | Docs |
|---|---|---|
| Schedule + timezone | `every weekday at 9am`, `Europe/London` | [Schedules](../schedules/index.md) |
| Sink | Webhook to `https://api.example.com/reports` | [Sinks](../sinks/index.md) |
| Retry policy | 3 attempts, exponential backoff from 10 s | [Retries](../retries-and-failures/retries.md) |
| Timeout | `30s` per attempt | |
| Misfire and overlap policies | `fire_once`, `allow` | [Misfires and overlap](../schedules/misfires-and-overlap.md) |
| Auto-disable threshold | 20 consecutive failed runs | [Auto-disable](../retries-and-failures/auto-disable.md) |
| Tags | `["reports","billing"]` | Used by alert rules and filters |

### Job states

| State | Meaning | How it gets there | How it leaves |
|---|---|---|---|
| `active` | Scheduled and firing | Created, resumed or re-enabled | Pause, auto-disable, schedule exhausted |
| `paused` | Not firing; you paused it | `POST /jobs/{id}/pause` | `POST /jobs/{id}/resume` |
| `disabled` | Not firing; cronpad stopped it after too many consecutive failures | [Auto-disable](../retries-and-failures/auto-disable.md) | `POST /jobs/{id}/enable` (resets the failure counter) |
| `completed` | Schedule has no more occurrences (one-off job fired, `count` or `until` reached) | Automatic | Edit the schedule |

Deleting a job removes it from scheduling immediately. Its run history stays until it ages out under the [retention policy](../platform/data-retention.md).

Every edit increments the job's `version`. Occurrences already queued with an older version are dropped, so an edit takes effect from the next fire time, never with a mix of old and new settings.

## Runs

A run is one occurrence: "this job, scheduled for this moment". Runs are created for scheduled fires, for **Run now** (`trigger = manual`) and for manual retries of a failed run (`trigger = retry_manual`).

### Run states

```
           claim                 success
scheduled ──────▶ running ─────────────────────▶ succeeded
                    │  retryable failure, attempts left
                    ├──────────────────────────▶ deferred ── backoff ──▶ running …
                    │  fatal failure, or no attempts left
                    └──────────────────────────▶ failed
scheduled ── missed during downtime, policy says skip ──▶ skipped
scheduled ── job paused, edited or deleted first ─────▶ cancelled
```

### Run timing fields

| Field | Meaning |
|---|---|
| `scheduled_at` | The moment the run was supposed to start. Never changes, even across retries. |
| `claimed_at` | When the dispatcher took it from the queue. |
| `started_at` | When the first attempt began. |
| `finished_at` | When the final attempt ended. |
| `start_lag_ms` | `started_at − scheduled_at`. The number cronpad's reliability is judged on. |
| `e2e_ms` | `finished_at − scheduled_at`, including retries. |
| `sla_breached` | `true` if `start_lag_ms` exceeded the SLA threshold. See [Delivery guarantees](delivery-guarantees.md#on-time). |

## Attempts

An attempt records one delivery try: start and end time, duration, outcome (`success`, `retryable`, `fatal`, `timeout`), HTTP status or exit code, error message, a request summary (`POST https://api.example.com/reports (412 bytes)`) and a truncated response body.

By default, response bodies are kept only for attempts that didn't succeed, and only for 7 days. See [Data retention](../platform/data-retention.md).

### Why runs and attempts are separate

Reliability questions are about runs ("did the 09:00 run succeed, and was it on time?"). Debugging questions are about attempts ("what did the second try get back?"). Keeping them apart keeps run queries fast for dashboards and alerts, and lets the bulky attempt data expire sooner.