---
title: "Admin Inbox Classify Backfill"
method: POST
path: "/v1/admin/inbox/classify-backfill"
tags: ["admin-inbox-backfill"]
---

# Admin Inbox Classify Backfill

`POST /v1/admin/inbox/classify-backfill`

## Request

- Base URL: `https://api.storyflo.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.storyflo.com/v1/admin/inbox/classify-backfill`
- Auth: none declared

Classify ``limit`` of the oldest ``reply_intent IS NULL`` inbound
rows through the ingest-time bridge. One-shot, resumable, idempotent.

Every row goes through
:func:`app.outreach.inbound_reply_bridge.classify_inbound_message` —
the identical call the M365 poller and the ``/v1/intake/email`` writer
make. Consequences, all inherited rather than re-implemented:

* **Nothing is sent.** The bridge has no send path; neither does this
  route. Lead-class intents (tell_me_more / yes_keep_going /
  who_are_you) are stamped ``awaiting_action`` with the intent
  attached and left for a human — never templated, never answered.
* **Nothing is marked auto-handled** that wasn't already. A row the
  pre-existing auto-responder machinery had already closed is
  recorded as ``auto_handled`` rather than re-opened.
* **Machine mail** (bounce / auto-response) is skipped, and is
  already outside the candidate predicate.
* **UNRELATED stays NULL**, so the admin intent filter keeps meaning.
* **Already-classified rows are never re-stamped.**

Resume by feeding ``next_cursor`` back in. ``drained`` true (and
``next_cursor`` null) means the sweep read past the END of the
candidate set. Re-invoking with a stale cursor, or with none, is
safe — it just re-reads rows that are now no-ops.

``unclassified_replies`` HAS A FLOOR AND DOES NOT REACH ZERO.
``UNRELATED`` rows (which now include machine-notice mail) are left
at ``reply_intent`` NULL on purpose, so they stay inside the tile's
predicate forever. ``drained`` — not ``unclassified_after == 0`` —
is the completion signal, and when ``drained`` is true this response
echoes ``unclassified_floor``: the number the tile will sit at from
here. Before 2026-08-08 a caller that re-POSTed without threading
``next_cursor`` saw ``drained=false`` forever once the stampable
rows ran out, because each call re-read the same page of unstampable
ones; a non-productive page now walks on by itself.

``scan_capped`` true means the opposite: the sweep walked
``MAX_SCAN_PER_CALL`` rows without stamping anything and stopped
with a cursor. Re-invoke with that cursor.

Cost: zero. The classifier is pure regex (``re`` + ``enum``, no
network, no model call), so a full 254-row drain bills $0.00. The
``limit`` cap bounds transaction count and request duration, not
spend.

## Query parameters

- `limit` integer: Rows to look at this invocation.
- `cursor` string, nullable: Opaque resume token from a previous response's next_cursor. Omit to start from the oldest unclassified row.

## Headers

- `x-storyflo-email-token` string, nullable

## Response `200`

Successful Response

- object

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/storyflo/apis/storyflo-inference-router.md) · [All operations](https://skmtc.dev/storyflo/apis/storyflo-inference-router/llms.txt) · [OpenAPI document](https://skmtc.dev/storyflo/apis/storyflo-inference-router/revisions/c3a1a735ac7a?raw)
