Task Progress (SSE)

Live progress stream for long-running background tasks via Server-Sent Events backed by Redis pub/sub.

Overview

Long-running background tasks (Validation Kit, FS-Research, synthesis, etc.) expose a Server-Sent Events stream so clients can render live progress without polling. The stream is backed by a Redis pub/sub channel that the task runner publishes to on every phase change, with database polling as a fallback if Redis is unavailable.

GET /api/tasks/{task_id}/progress

This endpoint is for in-progress tracking. Once a task reaches a terminal status, fetch the final result from GET /api/tasks/{task_id} — see Background Tasks.

How It Works

The endpoint is a streaming text/event-stream response that follows this sequence:

  1. Auth check — verifies the caller is a member of the task's workspace
  2. Initial state — emits the current DB state of the task immediately, so reconnecting clients get caught up
  3. Redis subscribe — subscribes to praxiom:task_progress:{task_id} for real-time updates
  4. DB fallback — if Redis is unavailable, polls the database every 3 seconds
  5. Auto-close — closes the stream when the task reaches a terminal status (completed, failed, or cancelled)

Response headers are set for streaming:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

Redis Channel

The task runner publishes JSON payloads to a per-task channel:

praxiom:task_progress:{task_id}

Every report_progress call inside the runner both publishes to this channel and persists the latest phase, progress_pct, and message into BackgroundTask.result_data, so polling clients and reconnecting SSE clients see a consistent view.

Event Payload

The SSE stream emits one event type — a data: line containing a JSON object. There are no custom event: names; clients read event.data and parse JSON.

Every payload carries these fields:

FieldTypeDescription
idstring (UUID)The task ID
statusstringpending, running, completed, failed, or cancelled
task_typestringvalidation_kit, fs_research, research, synthesis, etc.
progress_pctinteger0–100
phasestringTask-specific phase slug (see below)
messagestringHuman-readable progress message
started_atstring | nullISO 8601 timestamp
completed_atstring | nullISO 8601 timestamp (null until terminal)
heartbeat_atstring | nullISO 8601 timestamp of last runner heartbeat
errorstring | nullError message if status === "failed"
result_dataobjectFull result_data blob from the DB row

Phase Slugs by Task Type

Task TypePhases
validation_kitassumption_map (15%), research_plan (50%), interview_guide (80%), complete (100%)
fs_researchstarting (5%), phase_1_complete (50%), phase_2_complete (90%), running (interim)
Other typesrunning (interim), phase reported in message

Example Events

Progress update mid-run:

{
  "id": "b4c5d6e7-8901-2345-abcd-ef0123456789",
  "status": "running",
  "task_type": "validation_kit",
  "progress_pct": 50,
  "phase": "research_plan",
  "message": "Building your research plan...",
  "started_at": "2026-04-21T14:22:10.000Z",
  "completed_at": null,
  "heartbeat_at": "2026-04-21T14:23:41.120Z",
  "error": null,
  "result_data": {
    "phase": "research_plan",
    "progress_pct": 50,
    "message": "Building your research plan..."
  }
}

Terminal success event:

{
  "id": "b4c5d6e7-8901-2345-abcd-ef0123456789",
  "status": "completed",
  "task_type": "validation_kit",
  "progress_pct": 100,
  "phase": "complete",
  "message": "Your Validation Kit is ready — 3 documents generated.",
  "started_at": "2026-04-21T14:22:10.000Z",
  "completed_at": "2026-04-21T14:25:52.410Z",
  "heartbeat_at": "2026-04-21T14:25:52.410Z",
  "error": null,
  "result_data": {
    "document_ids": ["uuid-1", "uuid-2", "uuid-3"],
    "summary": "Generated Validation Kit for Acme: ..."
  }
}

Terminal failure event:

{
  "id": "b4c5d6e7-8901-2345-abcd-ef0123456789",
  "status": "failed",
  "task_type": "validation_kit",
  "progress_pct": 50,
  "phase": "research_plan",
  "message": "Building your research plan...",
  "started_at": "2026-04-21T14:22:10.000Z",
  "completed_at": "2026-04-21T14:26:10.000Z",
  "heartbeat_at": "2026-04-21T14:26:10.000Z",
  "error": "Validation Kit task timed out after 240s",
  "result_data": { "...": "..." }
}

Status Values

The client should treat the following status values as terminal and close the EventSource:

StatusMeaning
pendingQueued but not yet started (not terminal)
runningExecuting (not terminal)
completedFinished successfully (terminal)
failedErrored or timed out (terminal) — check error field
cancelledCancelled by the user via POST /api/tasks/{id}/cancel (terminal)

JavaScript Client

const source = new EventSource(
  `https://api.praxiomai.xyz/api/tasks/${taskId}/progress`,
  { withCredentials: true },
);

source.onmessage = (event) => {
  const data = JSON.parse(event.data);

  updateUI({
    phase: data.phase,
    pct: data.progress_pct,
    message: data.message,
  });

  if (["completed", "failed", "cancelled"].includes(data.status)) {
    source.close();

    if (data.status === "completed") {
      // Final payload is already in data.result_data,
      // or fetch via GET /api/tasks/{taskId} for the canonical row
      onComplete(data.result_data);
    } else {
      onError(data.error || `Task ${data.status}`);
    }
  }
};

source.onerror = (err) => {
  console.warn("SSE connection error — will auto-reconnect", err);
  // EventSource reconnects automatically; no action needed
};

EventSource does not support custom headers. If your auth scheme requires a bearer token, use a cookie-based session or proxy the stream through a fetch-based SSE client.

Relationship to Other Endpoints

EndpointUse For
POST /api/tasks/spawnCreate a task — returns the task_id to subscribe to
GET /api/tasks/{task_id}/progressThis endpoint — live SSE stream
GET /api/tasks/{task_id}/statusJSON snapshot of current state (lightweight polling alternative)
GET /api/tasks/{task_id}Canonical task record — use after completed to fetch the final result_data
POST /api/tasks/{task_id}/cancelCancel a pending/running task — will emit a cancelled terminal event

The /status endpoint returns the exact same payload shape as each SSE event, so clients that prefer polling can use it with no translation layer.

Timeouts & Reconnection

  • Per-task timeout — Tasks have a runner-side timeout (240s for validation_kit, 600s for fs_research, 300s default). On timeout, the task transitions to failed with error: "... timed out after Ns".
  • Redis idle timeout — If Redis delivers no message for 10 seconds, the stream falls back to a DB snapshot and re-emits current state, keeping the connection alive.
  • DB polling limit — The DB fallback caps at 200 iterations (~10 minutes at 3s intervals) as a safety limit.
  • Auto-reconnect — Browser EventSource reconnects automatically on connection drop. Because the stream emits current state on every (re)connect, clients resume without losing their place.
  • Access control — Every connection re-verifies workspace membership; a task whose workspace you lose access to will return 403 FORBIDDEN on reconnect.

What's Next

See Background Tasks for spawning and managing tasks, or the Validation Kit guide for a concrete end-to-end example.

Was this helpful?