---
module: fleet.tasks
title: Fleet Task Workflow
tab: Claude
order: 40
audience: dev
---

Act-without-guessing cheat-sheet. Owner service: **hiveops-fleet** ONLY.

## Ownership
- Fleet tasks (create/queue/lifecycle), `fleet_tasks`, `fleet_task_history`, `fleet_artifacts`, `hiveops.fleet.task.events` → **hiveops-fleet** (`com.hiveops.fleet`, port 8097, DB `hiveiq_fleet`).
- Target device data is a **read-only mirror** (`device_summary`) synced from `hiveops.device.events`. Device truth is owned by **hiveops-devices** — never write device rows here.
- Base URL: `https://api.bcos.cloud/fleet` (prod) · `https://api.bcos.dev/fleet` (dev). Frontend: `fleet.bcos.cloud`, component `FleetManagement/FleetTasks.svelte`.

## User/admin endpoints (`/api/fleet`, JWT)
| Method + Path | Role | Notes |
|---|---|---|
| `GET /api/fleet/tasks` (+`/tasks/paginated`) | USER..BCOS_ADMIN | filters: `status`, `deviceId`, `dateRange`, `institution`, `taskKind`, `scheduledOnly`, `sort`, `page`, `size` |
| `GET /api/fleet/tasks/{id}` | USER..BCOS_ADMIN | |
| `POST /api/fleet/tasks` | ADMIN/MSP_ADMIN/BCOS_ADMIN | body `CreateFleetTaskRequest` |
| `POST /api/fleet/tasks/{id}/cancel` | ADMIN/MSP_ADMIN/BCOS_ADMIN | 409 if terminal |
| `POST /api/fleet/tasks/{id}/retry` | ADMIN/MSP_ADMIN/BCOS_ADMIN | only FAILED/CANCELLED else 409 |
| `GET /api/fleet/tasks/pending-approval` | FLEET_APPROVER or MSP_ADMIN/BCOS_ADMIN | |
| `POST /api/fleet/tasks/{id}/approve` | FLEET_APPROVER or BCOS_ADMIN | body `{"note"?}`; 409 if not PENDING_APPROVAL |
| `POST /api/fleet/tasks/{id}/reject` | FLEET_APPROVER or BCOS_ADMIN | body `{"note"?}` |
| `GET /api/fleet/tasks/history` (+`/history/recent`) | USER..BCOS_ADMIN | defaults last 90 days |
| `DELETE /api/fleet/tasks/history` | ADMIN/MSP_ADMIN/BCOS_ADMIN | institution-scoped |
| `GET /api/fleet/stats` (+`/tasks/stats`) | USER..BCOS_ADMIN | counts are approximate (see Do NOT) |
| `GET /api/fleet/users/me` | authenticated | returns `fleetTaskApprover` |
| `GET /api/fleet/devices` (+`/devices/paginated`) | USER..BCOS_ADMIN | mirror data |
| artifacts: `GET/POST /artifacts`, `GET/DELETE /artifacts/{id}`, `POST /artifacts/{id}/toggle-enabled`, `POST /artifacts/import-cdn`, `GET /cdn-manifest/{type}` | ADMIN+ (reads USER+) | |

## Internal endpoints (`/api/internal/fleet`, `hasRole('INTERNAL')` via `X-Internal-Secret`; agent-proxy only)
| Method + Path | Notes |
|---|---|
| `GET /tasks/next?atm={atmId}&kind={?}` | stamps heartbeat, re-dispatches stale, marks returned task QUEUED |
| `GET /tasks/{taskId}/download?o={offset36}` | 512 KB chunks; offset base-36 |
| `POST /tasks/{taskId}/status` | body `{status,message,downloadOffset,totalSize}` |
| `POST /tasks/{taskId}/verify?f={hex}` | sha256 check → `{"taskAtmStatus":"RUNNING"|"FAILED"}` |
| `POST /tasks/{taskId}/upload?o={offset36}&f={filename}` | raw-byte body |

## Create request (`POST /api/fleet/tasks`)
```json
{ "atmIds": [12, 34], "taskKind": "CONFIG_UPDATE",
  "artifactId": 7, "configPatch": {"k":"v"}, "scheduledAt": "2026-07-01T22:00:00" }
```
- `atmIds` = `device_summary.id` values (required, non-empty). `scheduledAt` = ISO `LocalDateTime`, no zone.

## Enums (exact)
- **Status:** `PENDING_APPROVAL`, `PENDING`, `QUEUED`, `RUNNING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REJECTED`.
- **Kind:** `AUTO_UPDATE`, `UPDATE_AGENT`, `REBOOT`, `RESTART_AGENT`, `LOG_COLLECT`, `LOG_COLLECT_PATH`, `CONFIG_UPDATE`, `INSTALL_MODULE`, `PACKET_CAPTURE`, `HOTFIX`, `HOTFIX_INSTALL`, `SOFTWARE`, `SOFTWARE_INSTALL`, `UPDATE_SCRIPT`, `RUN_SCRIPT`.
- Approval-gated kinds (start `PENDING_APPROVAL`): `HOTFIX`, `HOTFIX_INSTALL`, `SOFTWARE`, `SOFTWARE_INSTALL`. All others start `PENDING`.

## Tables & topics
- Tables: `fleet_tasks` (active), `fleet_task_history` (terminal archive), `device_summary` (Kafka mirror), `fleet_artifacts`, `patch_windows`/`patch_window_devices`.
- Produced topic: `hiveops.fleet.task.events` — eventType `TASK_CREATED` / `TASK_STATUS` / `TASK_ARCHIVED`, key = atmId. Consumed topic: `hiveops.device.events`.

## Gotchas
- Create **silently skips** devices that are unknown, out of institution scope, or offline (`lastHeartbeat` null or >15 min). All-skipped → **HTTP 207** + empty array, not an error.
- Disabled artifact on create → **400** `Artifact '…' is disabled`.
- `COMPLETED` → archived+deleted from `fleet_tasks` immediately. `FAILED` → stays active (retryable), NOT swept by the 5-min archiver.
- Stale QUEUED/RUNNING (>5 min) auto-reset to PENDING with `downloadOffset=0` on next poll.
- `FLEET_APPROVER` = Boolean JWT claim `fleetTaskApprover`, not a ROLE_. MSP_ADMIN can create but not approve unless it also has this claim (BCOS_ADMIN always can).
- Download/upload offsets are **base-36**.
- Env: `DB_URL`, `JWT_SECRET`, `INTERNAL_SECRET`, `KAFKA_BOOTSTRAP_SERVERS`, `CDN_TOKEN`/`cdn.token`, `SCRIPT_SIGNING_PRIVATE_KEY`.

## Do NOT
- Do NOT write to `device_summary` from fleet or call hiveops-devices/incident over HTTP — mirror is Kafka-fed.
- Do NOT create fleet-task endpoints/tables in any other service — hiveops-fleet owns this domain.
- Do NOT trust `GET /stats` numbers — `pendingTasks` counts only `AUTO_UPDATE`+`PENDING`; `completed`/`failed` are hardcoded `0`. Use the tabs/history instead.
- Do NOT hit `/api/internal/fleet/**` with a user JWT — it needs `X-Internal-Secret` (agent-proxy). And do NOT expect a normal user JWT to approve.
- Do NOT expect a POST /tasks to fail loudly when targets are offline — check the returned array length / 207.
- Do NOT assume a downstream consumer of `hiveops.fleet.task.events` exists in-repo (none confirmed — *unverified*).
