# API reference

> Routes and JSON of the OpenWAF Tasks management API under dash.openwaf.dev, the CLI commands and the MCP tools.

## API

All routes are under `https://dash.openwaf.dev/v1/orgs/{org}` and take an OpenWAF API token (`owf_…`) or the dashboard session. Reads need `readonly`; writes need an `admin` of the whole organization, take `X-Change-Reason` (recorded in the audit log) and accept `?dry_run=true` to validate without saving.

| Route | What it does |
|---|---|
| `GET /tasks` | Overview: `enabled`, `requirements`, `quota`, `usage`, `kill` and every task. Readable before access is approved. |
| `POST /tasks` | Create a task; `201 {"task", "webhook_secret"}` (the secret only when a webhook is set, once) |
| `GET /tasks/{task}` | Read a task (`{task}` is its id or name) |
| `PATCH /tasks/{task}` | Change any writable field; `"rotate_webhook_secret": true` for a new secret |
| `DELETE /tasks/{task}` | Delete it (`204`) |
| `POST /tasks/{task}/run` | Run it now; `202` with the run |
| `GET /tasks/{task}/runs?limit=&before=` | Runs, newest first: `{"items", "next"}` |
| `GET /tasks/browser-profiles?key_id=` | Profiles of a browser key: `{"items": [{"id", "name"}]}` |
| `GET /task-runs/{run}` | One run with its `approvals` and `artifacts` |
| `GET /task-runs/{run}/logs?after=<seq>` | Log lines after `seq`: `{"items": [{"seq", "at", "level", "type", "message"}]}` |
| `GET /task-runs/{run}/artifacts/{id}` | A screenshot (`image/jpeg` or `image/png`) |
| `GET /task-runs/{run}/recording` | `302` to the recording file, when the run recorded one |
| `POST /task-runs/{run}/cancel` | Cancel a run; answers the run |
| `GET /task-approvals?status=&task=` | Approvals: `{"items"}`; `status` is `pending`, `approved`, `rejected`, `expired`, `executed` or `failed` |
| `GET /task-approvals/{id}` | One approval |
| `POST /task-approvals/{id}/approve` | `{"payload_sha256", "reason"}`; answers the approval |
| `POST /task-approvals/{id}/reject` | `{"reason"}`; answers the approval |

### Overview

```json
{"enabled": true,
 "requirements": {"browsers": true, "egress": true, "email_verified": true, "two_factor": true},
 "quota": {"runs_per_month": 200, "max_tasks": 5, "max_concurrent": 1},
 "usage": {"period_start": "2026-10-01T00:00:00Z", "period_end": "2026-11-01T00:00:00Z",
           "runs": 14, "active": 0, "pending_approvals": 1},
 "kill": false,
 "tasks": [ … ]}
```

`kill` is `true` while the service is paused for every organization: no runs start.

### Task

```json
{"id": "…", "name": "guestbook", "description": "", "kind": "script",
 "schedule": {"cron": "0 9 * * *", "timezone": "Europe/Madrid"},
 "paused": false,
 "browser": {"key_id": "…", "key_name": "ci-agent", "profile": "", "recording": false},
 "allow": ["example.com"],
 "script": "export default async function ({ page, task }) { … }",
 "input": {}, "state": {},
 "max_run_minutes": 10,
 "approval": {"policy": "required", "expire_minutes": 120, "relaxed_domains": []},
 "action_limits": [{"domain": "example.com", "min_interval_minutes": 30, "max_per_day": 4}],
 "webhook": {"url": "https://hooks.example.com/openwaf", "secret_set": true},
 "pool": "browsers", "egress_agent": null,
 "next_run_at": "2026-10-30T08:00:00Z",
 "last_run": {"id": "…", "status": "succeeded", "finished_at": "2026-10-29T08:00:41Z"},
 "created_at": "…", "updated_at": "…"}
```

| Field | Rules |
|---|---|
| `name` | Lowercase letters, digits and hyphens, up to 63, unique in the organization |
| `kind` | `script` or `agent`; cannot change |
| `schedule` | `{"cron", "timezone"}` (5 fields, IANA zone, at most every 15 minutes) or `{"at"}` for one run |
| `browser` | `key_id` of a browser key of the organization, `profile` name (empty for a clean browser), `recording` |
| `allow` | 1 to 20 domains: `example.com` or `*.example.com` |
| `script` | `script` only: ES module up to 256 KiB. See the [script quickstart](https://developers.openwaf.dev/tasks/quickstart-script/#write-the-script). |
| `agent` | `agent` only: `{"goal", "egress_target", "model", "max_steps"}`. See the [agent quickstart](https://developers.openwaf.dev/tasks/quickstart-agent/#create-the-task). |
| `input` | JSON up to 64 KiB, given to the script or the agent |
| `max_run_minutes` | 1 to 30 |
| `approval` | `expire_minutes` 10 to 1440. `policy` and `relaxed_domains` are read-only; see [policies](https://developers.openwaf.dev/tasks/approvals/#policies). |
| `action_limits` | Stricter [per-domain limits](https://developers.openwaf.dev/tasks/limits/#per-domain) |
| `webhook` | `{"url"}` to set, `null` to remove. See [webhook](https://developers.openwaf.dev/tasks/approvals/#webhook). |

`state`, `pool`, `egress_agent`, `next_run_at` and `last_run` are read-only.

### Run

```json
{"id": "…", "task_id": "…", "task_name": "guestbook", "number": 12, "trigger": "schedule",
 "status": "awaiting_approval",
 "created_at": "…", "started_at": "…", "finished_at": null, "error": "",
 "output": {"posted": "pending approval"},
 "steps": 0, "browser_seconds": 38, "model_calls": 0, "prompt_tokens": 0, "completion_tokens": 0,
 "blocked_requests": 0, "live_url": null, "recording_available": false, "egress_agent": null,
 "approvals": [ … ],
 "artifacts": [{"id": "…", "name": "before", "content_type": "image/jpeg", "size": 81234, "created_at": "…"}]}
```

`status` is one of `queued`, `running`, `awaiting_approval`, `executing`, `succeeded`, `failed`, `rejected`, `expired`, `timed_out` and `canceled`. `trigger` is `schedule` or `manual`. `live_url` is only set while the run has a browser.

### Approval

```json
{"id": "…", "task_id": "…", "task_name": "guestbook", "run_id": "…", "status": "pending",
 "summary": "Sign the guestbook with \"Hello from Example Inc.\"",
 "target": "https://example.com/guestbook", "domain": "example.com",
 "payload": {"steps": [{"op": "goto", "url": "https://example.com/guestbook"}, … ]},
 "payload_sha256": "9c1e…", "screenshot_id": "…",
 "requested_at": "…", "expires_at": "…", "decided_at": null, "decided_by": null, "reason": null,
 "not_before": null, "executed_at": null, "result": null}
```

After the act phase, `result` is `{"ok", "final_url", "error", "screenshot_id"}`. Steps are described in [approvals](https://developers.openwaf.dev/tasks/approvals/#steps).

### Errors

| Status | `code` | When |
|---|---|---|
| 403 | `feature_disabled` | OpenWAF Tasks is not enabled for the organization |
| 403 | `forbidden` | The role is too low, or the member or token is limited to a client or a zone |
| 404 | `not_found` | No such task, run, artifact or approval |
| 409 | `payload_changed` | The `payload_sha256` you sent is not the approval's |
| 409 | `not_pending` | The approval was already decided or expired |
| 422 | `invalid` | A field breaks the rules above; `field` names it |

## CLI

`openwaf-cloud ctl tasks …` calls the API above. See [get started with the CLI](https://developers.openwaf.dev/cli/get-started/).

The organization comes from `--org` or `OPENWAF_ORG`. Changes show what they will do and ask before applying (`--yes` skips the question, `--dry-run` only validates).

| Command | What it does |
|---|---|
| `tasks list` | Overview and tasks |
| `tasks get <task>` | One task |
| `tasks create <task.json\|-> --reason "…"` | Create a task from a JSON body |
| `tasks update <task> <patch.json\|-> --reason "…"` | Change a task |
| `tasks delete <task> --reason "…"` | Delete a task |
| `tasks run <task> --reason "…"` | Run it now |
| `tasks runs <task> [--limit n]` | Runs of a task |
| `tasks run-get <run>` | One run with its approvals and artifacts |
| `tasks cancel <run> --reason "…"` | Cancel a run |
| `tasks approvals [--status pending] [--task <task>]` | Approvals |
| `tasks approve <approval> --sha256 <hash> --reason "…"` | Approve the exact payload |
| `tasks reject <approval> --reason "…"` | Reject it |

## MCP tools

The [OpenWAF MCP server](https://developers.openwaf.dev/mcp/) has 12 tools for OpenWAF Tasks. Writes need a `reason` and take two steps: `confirm=false` only checks, `confirm=true` applies.

| Tool | Role |
|---|---|
| `tasks_list` | `readonly` |
| `tasks_get` | `readonly` |
| `task_runs_list` | `readonly` |
| `task_runs_get` | `readonly` |
| `task_approvals_list` | `readonly` |
| `tasks_create` | `admin` |
| `tasks_update` | `admin` |
| `tasks_delete` | `admin` |
| `tasks_run` | `admin` |
| `task_runs_cancel` | `admin` |
| `task_approve` | `admin`. The check step shows the exact payload, its `payload_sha256` and the screenshot id; the apply step needs that `payload_sha256`. |
| `task_reject` | `admin` |

An assistant can prepare an approval for you, but read the steps yourself before you confirm it.
