> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent shrinkage: reading the per-agent state-history timeline

> Shrinkage is the paid-time gap between scheduled hours and productive hours. This page defines shrinkage, the per-agent state-transition event stream that backs it, the productive vs non-productive buckets, and how a supervisor reads the agent detail page.

# Agent shrinkage and state history

Shrinkage is the gap between what you pay an agent for and the time they are actually productive. The agent detail page measures it from a per-agent event stream: every state flip — `available`, `busy`, `wrapup`, `away`, `offline` — appends one row, and the dashboard aggregates those rows into per-state durations you read as "productive vs not productive" time. This page defines shrinkage, the event schema behind it, the productive vs non-productive buckets, and the supervisor workflow on **Voice → Agents → \[agent]**.

The state machine itself — the five states and who moves each edge — is defined in [Agent presence and aux-code lifecycle](/concepts/agent-presence-lifecycle). This page covers the measurement surface on top of it.

## 1. What shrinkage measures

Shrinkage is measured productive time against paid time. In contact-centre terms it is usually expressed as a percentage:

```text theme={null}
shrinkage % = (non-productive time / paid time) × 100
```

The non-productive numerator is the time an agent spends in `paused` (on break, training, lunch) plus `offline` (logged out) during their shift. The productive denominator groups `available`, `busy`, and `wrapup` — the agent is either waiting for a call, on a call, or doing post-call work; all three are on-the-clock and counted as productive.

Because every transition writes an append-only row with an exact timestamp, shrinkage is a deterministic aggregate, not an estimate. A supervisor can answer "how much of Tuesday was this agent actually productive" from one per-day read.

## 2. The event stream

Every accepted state change writes one row to the per-agent history. The row carries the prior state, the new state, an optional reason, and an exact timestamp:

| Field        | Type            | Meaning                                                                                                                                   |
| ------------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`   | string          | The agent this row belongs to.                                                                                                            |
| `from_state` | string \| null  | The state the agent left. `null` on the very first row of the history (no prior state existed).                                           |
| `to_state`   | string          | The state the agent entered: `available`, `busy`, `wrapup`, `away`, or `offline`.                                                         |
| `reason`     | string \| null  | Free-form note for supervisor-forced transitions or a pause code submission. `null` for organic transitions driven by the call lifecycle. |
| `changed_at` | ISO-8601 string | The exact instant the transition was accepted.                                                                                            |

The read endpoint is `GET /api/v1/voice/agents/:id/state-history`, with optional `from`, `to` (both ISO datetimes), and `limit` (default 500, max 5000) query parameters. When omitted, `from` defaults to 24 hours ago and `to` defaults to now. Rows come back newest-first, ordered by `changed_at` descending:

```json theme={null}
{
  "agent_id": "agt_01h...",
  "from": "2026-09-28T00:00:00.000Z",
  "to": "2026-09-28T23:59:59.999Z",
  "rows": [
    {
      "id": "evt_9f2...",
      "from_state": "available",
      "to_state": "away",
      "reason": "lunch",
      "changed_at": "2026-09-28T12:02:11.402Z"
    },
    {
      "id": "evt_9e8...",
      "from_state": "wrapup",
      "to_state": "available",
      "reason": null,
      "changed_at": "2026-09-28T08:41:03.118Z"
    }
  ]
}
```

Access is self-or-supervisor: an agent reads their own stream; reading another agent's stream requires an `owner`, `admin`, or `supervisor` role, because break-compliance rows are sensitive.

## 3. Bucketing productive against non-productive time

The dashboard groups the five states into two buckets and credits each row's elapsed time to its `to_state`:

| Bucket         | States                        | Why it counts                                                                         |
| -------------- | ----------------------------- | ------------------------------------------------------------------------------------- |
| Productive     | `available`, `busy`, `wrapup` | Wait-for-call, on-call, and post-call work are all part of the paid productive shift. |
| Non-productive | `away` (paused), `offline`    | Break codes and logged-out time are the shrinkage numerator.                          |

The duration aggregate walks the chronological sequence and credits the time between consecutive transitions to the state the agent was in. The most recent transition accrues live time up to the end of the queried window, so an agent currently in `available` keeps accruing productive time while you watch. On the detail page the aggregate is rendered as five per-state KPI tiles formatted as `Xh Ym`; the same helper the page uses is exported for reuse.

## 4. Supervisor workflow

Open **Voice → Wallboard** and click an agent tile, or navigate directly to **Voice → Agents → \[agent id]**. The page is gated to supervisor-class roles (`owner`, `admin`); an agent always has access to their own detail view.

Steps:

1. Pick a single day with the date selector. The page defaults to today; the read is a single-day window (the event-stream endpoint caps the row limit, so multi-day exports are a separate surface, not the wallboard view).
2. Read the five KPI tiles — per-state durations in `Xh Ym`. A state with no credited time shows `—` so you can distinguish "absent" from "trace".
3. Scan the transitions table — newest first — with a per-row duration. The current (most recent) row has no duration because the agent is still in that state.
4. Read bucket badges: the `available` tile is the productive anchor; `away` and `offline` are the shrinkage contributors. A long `away` tile with a `reason` like `lunch` or `training` tells you whether the shrinkage is scheduled or a leak.

Badge colouring is deliberately quiet — `available` is highlighted so a supervisor's eye lands on currently-productive, while `busy`/`wrapup`/`away`/`offline` use neutral greys to avoid colour-noise on a wallboard that many people share.

## 5. Reading the numbers against quality and scheduling

Shrinkage tells you how time was spent; it does not tell you whether the productive time was good. Pair the timeline with two sibling surfaces:

* **Voice Agent Quality** ([/voice/vaqi](/voice/vaqi)) — the per-call quality index (latency, turn-taking, barge-in) for AI voice agents, so "productive time" also has a quality read.
* **WFM model** ([/concepts/wfm-model](/concepts/wfm-model)) — the schedule-adherence model. Shrinkage feeds the adherence numerator: an agent whose `away`/`offline` time spikes during a scheduled shift shows up both here and as an adherence miss.

## Cross-links

* [Agent presence and aux-code lifecycle](/concepts/agent-presence-lifecycle) — the five-state machine and who moves each edge.
* [Attendant console](/voice/attendant-console) — the live supervisor grid that consumes the same open event.
* [Voice Agent Quality (VAQI)](/voice/vaqi) — quality read for the productive-time bucket.
* [WFM model](/concepts/wfm-model) — scheduling/adherence take on the shrinkage numerator.
