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:
- Auth check — verifies the caller is a member of the task's workspace
- Initial state — emits the current DB state of the task immediately, so reconnecting clients get caught up
- Redis subscribe — subscribes to
praxiom:task_progress:{task_id}for real-time updates - DB fallback — if Redis is unavailable, polls the database every 3 seconds
- Auto-close — closes the stream when the task reaches a terminal status (
completed,failed, orcancelled)
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:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | The task ID |
status | string | pending, running, completed, failed, or cancelled |
task_type | string | validation_kit, fs_research, research, synthesis, etc. |
progress_pct | integer | 0–100 |
phase | string | Task-specific phase slug (see below) |
message | string | Human-readable progress message |
started_at | string | null | ISO 8601 timestamp |
completed_at | string | null | ISO 8601 timestamp (null until terminal) |
heartbeat_at | string | null | ISO 8601 timestamp of last runner heartbeat |
error | string | null | Error message if status === "failed" |
result_data | object | Full result_data blob from the DB row |
Phase Slugs by Task Type
| Task Type | Phases |
|---|---|
validation_kit | assumption_map (15%), research_plan (50%), interview_guide (80%), complete (100%) |
fs_research | starting (5%), phase_1_complete (50%), phase_2_complete (90%), running (interim) |
| Other types | running (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:
| Status | Meaning |
|---|---|
pending | Queued but not yet started (not terminal) |
running | Executing (not terminal) |
completed | Finished successfully (terminal) |
failed | Errored or timed out (terminal) — check error field |
cancelled | Cancelled 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
| Endpoint | Use For |
|---|---|
POST /api/tasks/spawn | Create a task — returns the task_id to subscribe to |
GET /api/tasks/{task_id}/progress | This endpoint — live SSE stream |
GET /api/tasks/{task_id}/status | JSON 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}/cancel | Cancel 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 forfs_research, 300s default). On timeout, the task transitions tofailedwitherror: "... 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
EventSourcereconnects 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 FORBIDDENon 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?