# Progress

> Publish a live, bounded status value for operators without changing durable control flow.

A three-minute import looks identical to a stuck one from the outside: both sit in `active`.
Progress closes that gap. It is a mutable, latest-value-only projection of what the handler is
doing right now — operator data, not control flow. After a retry, progress cannot tell
Workhorse to skip a stage; that is what [checkpoints](/docs/durable-execution) are for.

## Publish the latest value

Call `HandlerContext.setProgress` with any JSON value your operator surface understands.
Each accepted update replaces the previous one.

<Tabs items={["TypeScript", "Python", "Go"]}>
  <Tab value="TypeScript">
    ```ts
    await ctx.setProgress({ phase: "reading", processed: 0 });
    for await (const batch of readBatches(payload.source)) {
      await importBatch(batch);
      processed += batch.length;
      await ctx.setProgress({ phase: "importing", processed });
    }
    ```
  </Tab>
  <Tab value="Python">
    ```python
    context.set_progress({"phase": "reading", "processed": 0})
    for batch in read_batches(payload["source"]):
        import_batch(batch)
        processed += len(batch)
        context.set_progress({"phase": "importing", "processed": processed})
    ```
  </Tab>
  <Tab value="Go">
    ```go
    progress, err := handler.SetProgress(map[string]any{"phase": "reading", "processed": 0})
    for batch := range readBatches(payload.Source) {
        if err := importBatch(batch); err != nil {
            return nil, err
        }
        processed += len(batch)
        progress, err = handler.SetProgress(map[string]any{"phase": "importing", "processed": processed})
    }
    ```
  </Tab>
</Tabs>

`HandlerContext.getProgress` returns the latest retained value from inside a handler. Outside
one, `Admin.getProgress` reads it by job ID, and a custom runtime can write through
`Queue.updateProgress` with the claimed job and worker identity. The
[dashboard](/docs/dashboard) renders the value live.

Python exposes the handler methods as `set_progress` and `get_progress`. Go exposes `SetProgress`
and `GetProgress`. Their returned progress records carry the same revision and ownership provenance
as the TypeScript record.

## Why progress is rate-limited

Every changed value updates one `job_progress` row and appends a small `progress_updated`
event. A tight loop — say, one update per imported row — would churn the database without
making the interface more useful; no operator reads a thousand updates a second.

PostgreSQL therefore size-caps the value and rate-limits changed writes. A caller that
updates too quickly gets `ProgressRateLimitError`, which says when it may retry. In the loop
above, updating once per batch stays comfortably inside the limit.

Repeating an identical value is a free no-op: it does not advance the revision or append an
event. You can call `setProgress` defensively without paying for it.

## What survives, and who may write

Each accepted update records the attempt, fence token, worker ID, revision, and timestamps.
The latest value survives retries and terminal materialization, until retention removes the
job identity — so a failed job's last reported phase remains readable evidence.

Only the current unexpired owner can write. If a stale generation tries — an old handler
still running after its lease was recovered — it gets `ProgressLeaseLostError`, so a zombie
cannot overwrite the newer activation's status.

Keep the division of labor sharp: progress is what an operator watches; a checkpoint is what
another activation reuses. If losing the value would change what your code does next, it is
not progress — make it a checkpoint.

## Next

- [Durable execution](/docs/durable-execution) — save immutable completion evidence
- [Queries and timelines](/docs/queries) — read job and lifecycle state
- [Dashboard](/docs/dashboard) — show progress to operators

---

Exact value limits, update cadence, revisions, and event fields:
[architecture reference](https://github.com/stablemates/workhorse/blob/main/docs/architecture.md#job_progress).
