# Jobs API

Jobs are the core resource: a schedule, a sink, and policies for retries, misfires, overlap and auto-disable. All endpoints are workspace-scoped through your API key.

## The job object

```json
{
  "id": "job_01J9ZK3Q…",
  "name": "nightly-report",
  "description": "Builds the daily revenue report",
  "tags": ["reports"],
  "schedule": {
    "expr": "every day at 9am",
    "kind": "natural",
    "canonical": "FREQ=DAILY;BYHOUR=9;BYMINUTE=0;BYSECOND=0",
    "description": "At 09:00 every day",
    "timezone": "Asia/Kolkata",
    "starts_at": null,
    "ends_at": null,
    "count": null
  },
  "sink": {"kind": "webhook", "config": {"url": "https://api.example.com/reports", "method": "POST"}},
  "retry": {"max_attempts": 3, "backoff": "exponential", "initial_interval": "10s", "max_interval": "10m", "multiplier": 2, "jitter": 0.2},
  "timeout": "30s",
  "sla_threshold": null,
  "misfire_policy": "fire_once",
  "overlap_policy": "allow",
  "auto_disable_threshold": null,
  "state": "active",
  "disabled_reason": null,
  "consecutive_failures": 0,
  "next_run_at": "2026-11-11T03:30:00Z",
  "next_runs": ["2026-11-11T03:30:00Z", "2026-11-12T03:30:00Z", "2026-11-13T03:30:00Z"],
  "last_run_at": "2026-11-10T03:30:00Z",
  "last_run_status": "succeeded",
  "version": 3,
  "created_at": "2026-10-16T08:00:00Z",
  "updated_at": "2026-10-20T11:12:00Z"
}
```

`null` for `sla_threshold` and `auto_disable_threshold` means "use the server default". `next_runs` is included by `GET /jobs/{id}`.

## Create a job

`POST /api/v1/jobs`

```json
{
  "name": "nightly-report",
  "schedule": {"expr": "every day at 9am", "timezone": "Asia/Kolkata"},
  "sink": {"kind": "webhook", "config": {"url": "https://api.example.com/reports"}},
  "retry": {"max_attempts": 5},
  "timeout": "30s",
  "tags": ["reports"]
}
```

Only `name`, `schedule.expr` and `sink` are required. Omitted fields take server defaults. Partial `retry` objects are merged with the defaults.

Response `201` is the job object plus, once only, `"signing_secret": "whsec_…"` for webhook jobs.

Send an `Idempotency-Key` header to replay the same create response for 24 hours within a workspace. Reusing the key with a different payload returns `409 idempotency_conflict`.

Errors: `400 validation_failed` (bad schedule, unknown secret, URL not allowed, sink disabled on this server), `403 limit_exceeded` (job count or minimum interval).

## List jobs

`GET /api/v1/jobs?state=active&tag=reports&q=report&limit=50`

| Query | Description |
|---|---|
| `state` | `active`, `paused`, `disabled`, `completed` |
| `tag` | Jobs with this tag |
| `q` | Case-insensitive match on name and description |

## Get a job

`GET /api/v1/jobs/{id}`, including `next_runs` and 24-hour `stats_24h` (`runs`, `succeeded`, `failed`, `sla_breached`, `p95_start_lag_ms`).

## Update a job

`PATCH /api/v1/jobs/{id}` with any subset of the create fields. The job's `version` increases, queued occurrences of the old version are dropped, and the next fire time is recalculated from now.

## Delete a job

`DELETE /api/v1/jobs/{id}` → `204`. The job stops immediately; its run history remains until it ages out.

## Change state

| Endpoint | From | To |
|---|---|---|
| `POST /api/v1/jobs/{id}/pause` | `active` | `paused` |
| `POST /api/v1/jobs/{id}/resume` | `paused` | `active` (next future occurrence) |
| `POST /api/v1/jobs/{id}/enable` | `disabled` | `active`, failure counter reset |

Calling from any other state returns `409 conflict`.

## Run now

`POST /api/v1/jobs/{id}/trigger` → `202` with `{"run_id": "run_…"}`. Creates a run with `trigger = manual` that starts immediately. The schedule is unaffected. Works on paused jobs too.

## Test the sink

`POST /api/v1/jobs/{id}/test` → `200`

```json
{"outcome": "success", "status_code": 200, "duration_ms": 87, "response_body": "{\"ok\":true}", "request_summary": "POST https://api.example.com/reports (231 bytes)"}
```

Executes the sink once, synchronously, without recording a run or affecting counters. You can also test an unsaved config with `POST /api/v1/jobs/test` and a full job body.

## Rotate the signing secret

`POST /api/v1/jobs/{id}/rotate-signing-secret` → `200` with the new `signing_secret`. Both secrets sign requests for 24 hours.