---
module: fleet.tasks
title: Fleet Task Workflow
tab: Architect
order: 30
audience: dev
---

How Fleet Task Workflow is built. Owner service: **hiveops-fleet** (`com.hiveops.fleet`, Spring Boot 3.4, port 8097, DB `hiveiq_fleet`). Frontend: `FleetManagement/FleetTasks.svelte` → `lib/api.ts` (`fleetAPI`, base `…/api/fleet`). See also [platform.data-architecture] and [platform.kafka]; service-level scope in the technical `fleet/overview` doc.

## Components involved

- **`FleetApiController`** (`/api/fleet`) — authenticated JWT surface for admins/approvers/the UI.
- **`InternalFleetController`** (`/api/internal/fleet`, `hasRole('INTERNAL')`) — the agent work loop; only agent-proxy calls it, authenticated by `X-Internal-Secret`.
- **`FleetTaskService`** — all lifecycle logic (create, poll/dispatch, status transitions, approve/reject, archive, scheduled sweeps).
- **`FleetTaskEventPublisher` / `FleetTaskKafkaConfig`** — emit lifecycle events to Kafka.
- **`DeviceEventConsumer`** — keeps the local `device_summary` mirror in sync (input side).
- Supporting services: `FleetArtifactService` (chunked download/upload, CDN import), `ScriptSigningService`, `AtmCaptureService`, `PatchWindowService`.

## Data flow (create → execute → archive)

1. **Create.** UI POSTs `CreateFleetTaskRequest` (`atmIds[]`, `taskKind`, optional `artifactId`, `configPatch`, `scheduledAt`) to `POST /api/fleet/tasks`. `FleetTaskService.createTask` loops each target device (resolved against `device_summary`), skipping unknown / out-of-scope / offline (>15 min) devices. Initial status = `PENDING_APPROVAL` for `HOTFIX`/`HOTFIX_INSTALL`/`SOFTWARE`/`SOFTWARE_INSTALL`, else `PENDING`. `*_INSTALL` kinds with a device patch window get `scheduledAt = PatchWindowService.nextOccurrence(...)`. `UPDATE_SCRIPT` scripts are RSA-signed and the signature folded into `configPatch`. One row per device → `fleet_tasks`; each save fires `publishCreated` (`TASK_CREATED`).
2. **Approve.** `POST /tasks/{id}/approve` (approver authority) flips `PENDING_APPROVAL → PENDING`, stamps `approvedBy`/`approvedAt`/`approvalNote`. Reject → `REJECTED` then archive.
3. **Dispatch (agent pull).** Agent (via agent-proxy) calls `GET /api/internal/fleet/tasks/next?atm={atmId}&kind={?}`. This stamps `device.lastHeartbeat`, re-dispatches any stale (>5 min) QUEUED/RUNNING task back to PENDING, then returns the oldest eligible PENDING task whose `scheduledAt` has passed and marks it `QUEUED` (`markQueued`). Response is a compact map (`tid`, `taskKind`, artifact `sha256`/`size`/`cdnUrl`, `cdnToken` if CDN-hosted, `configPatch`).
4. **Transfer.** For artifact-bearing kinds the agent pulls 512 KB chunks from `GET /tasks/{taskId}/download?o={offset36}` and hash-checks via `POST /tasks/{taskId}/verify`. Result artifacts (logs / captures) flow back through `POST /tasks/{taskId}/upload`.
5. **Progress / completion.** Agent posts `POST /tasks/{taskId}/status` (`status`, `message`, `downloadOffset`, `totalSize`). `updateTaskStatus` sets `RUNNING` (stamps `startedAt`) then `COMPLETED`/`FAILED` (stamps `completedAt`), and publishes `TASK_STATUS`. On `COMPLETED` the task is archived immediately; `FAILED` stays active for retry.
6. **Archive.** `archiveAndDelete` copies the task into `fleet_task_history`, deletes it from `fleet_tasks`, and publishes `TASK_ARCHIVED`. A `@Scheduled` sweep (every 5 min) catches stray COMPLETED/CANCELLED/REJECTED rows; a daily job prunes history >30 days.

This is the **executor → Kafka → record-store** pattern: the fleet service executes the state machine, publishes each transition to Kafka, and archives the terminal record locally (durable device-facing history lives downstream in devices per service ownership).

## Kafka

Cross-link [platform.kafka].

**Produced** — topic **`hiveops.fleet.task.events`** (`FleetTaskKafkaConfig.FLEET_TASK_EVENTS_TOPIC`, 3 partitions, `acks=all`, idempotent producer). Message key = device `atmId` (fallback: task id). Payload `FleetTaskEvent` with `eventType` ∈ `{TASK_CREATED, TASK_STATUS, TASK_ARCHIVED}` plus task/device/artifact/progress fields. Downstream consumers of this topic are external to hiveops-fleet — *no in-repo consumer confirmed (unverified)*.

**Consumed** — **`hiveops.device.events`** via `DeviceEventConsumer` upserts `device_summary`. Fleet never HTTP-calls devices/incident for target data; it reads its Kafka-synced mirror.

## Database tables (this feature)

| Table | Role |
|-------|------|
| `fleet_tasks` | active tasks (PENDING_APPROVAL/PENDING/QUEUED/RUNNING, and FAILED pending retry). Indexes on `status` and `(device_id, status)` |
| `fleet_task_history` | archived terminal tasks (COMPLETED/FAILED/CANCELLED/REJECTED); denormalized (`atmName`, `institutionKey`, `artifactName/Version`, timestamps, approver fields) |
| `device_summary` | Kafka-synced device mirror; task targets + institution scoping + offline check resolve here |
| `fleet_artifacts` | artifacts attached to tasks (`sha256Hash`, `fileSize`, `cdnUrl`, `enabled`) |
| `patch_windows` / `patch_window_devices` | schedule `*_INSTALL` tasks |

Cross-link [platform.data-architecture].

## API surface (key endpoints)

Authenticated (`/api/fleet`): `GET /tasks`·`/tasks/paginated`, `GET /tasks/{id}`, `POST /tasks`, `POST /tasks/{id}/cancel`·`/retry`, `GET /tasks/pending-approval`, `POST /tasks/{id}/approve`·`/reject`, `GET /tasks/history`·`/tasks/history/recent`, `DELETE /tasks/history`, `GET /stats`·`/tasks/stats`, `GET /users/me`, `GET /devices`·`/devices/paginated`, artifact CRUD + `/artifacts/import-cdn` + `/cdn-manifest/{type}`.

Internal (`/api/internal/fleet`, ROLE_INTERNAL): `GET /tasks/next`, `GET /tasks/{taskId}/download`, `POST /tasks/{taskId}/status`, `POST /tasks/{taskId}/verify`, `POST /tasks/{taskId}/upload`.

## Design notes

- **Two auth planes / one service.** JWT (`JWT_SECRET`) role-gated user surface vs `X-Internal-Secret` (`INTERNAL_SECRET`) agent surface — never mix them.
- **Institution scoping** via `InstitutionContext.resolveScope()` filters reads, create targets, and history clears.
- **Idempotent self-healing** via the 5-min stale re-dispatch; agents can crash mid-transfer and resume (`downloadOffset` in base-36).
- **Multi-device create is non-atomic** — partial success returns 201 with the created subset; all-skipped returns 207.
