# The next_action contract

  The client reads its next move out of the response instead of guessing it.

## What it is

Every response from the rail carries a `next_action` object. Its `type` names the move; where
a call is required, `endpoint` carries the fully formed URL to make it against.

```json
{
  "next_action": {
    "type": "check_eligibility",
    "endpoint": "/api/v2/patients/SIL-UPI-0091774/eligibility/SEL-2026-08-14-0091/benefits"
  }
}
```

Take the endpoint from the response. Do not construct it. What comes back already has the
identifiers interpolated, so you get `/api/v2/patients/SIL-UPI-0091774/covers` rather than a
template you have to fill.

## Why it exists

There are two reasons, and the second is the one that bites in production.

The first is that it keeps payer branching out of your client. Payers differ on whether a
preauthorisation is needed, on whether a reservation must be created explicitly or is made
for you, and on whether doctor sign-off is routed elsewhere first. Those differences reach
you as different `next_action` values, so you do not keep a code path per payer.

The second is that it handles legitimate skips. Hard-coding the sequence works until a step
is correctly skipped: an authorisation already granted and still valid, a reservation
auto-created on approval, a preauth that turned out not to be required. A client that walks
a fixed list either repeats work or stalls. Reading `next_action` avoids both, and it takes
no conditionals on your side.

## The full enum

<Wire
  rows={[
    { call: "select_cover", does: "Render the cover chooser and call cover selection", keep: "endpoint" },
    { call: "check_eligibility", does: "A cover was selected, so check eligibility against it", keep: "endpoint" },
    { call: "select_benefit", does: "Show the benefit or item picker, fresh or because the previous choice is now blocked", keep: "endpoint" },
    { call: "assign_location", does: "A service location must be set before anything else proceeds", keep: "—" },
    { call: "authenticate", does: "Offer the consent factors listed in available_factors", keep: "available_factors" },
    { call: "submit_challenge", does: "A one-time code was dispatched, so collect and submit it", keep: "endpoint, expires_at" },
    { call: "submit_preauth", does: "Authorisation granted and an item needs preauthorisation", keep: "endpoint" },
    { call: "manual_confirmation", does: "Preauth is required but confirmed outside the API, so proceed provisionally", keep: "—" },
    { call: "await_decision", does: "Poll the endpoint, or wait for the registered callback", keep: "endpoint, poll_after_ms" },
    { call: "reserve", does: "An approval needs an explicit reservation call", keep: "endpoint" },
    { call: "fix", does: "Pre-flight found blockers, and reasons[] names each one with a fix_stage", keep: "reasons[]" },
    { call: "submit", does: "Every gate has passed, so close the claim", keep: "endpoint" },
    { call: "none", does: "Nothing further is required right now", keep: "—" },
  ]}
/>

## How to build against it

<BuildSequence
  steps={[
    "Model next_action.type as an enum in your own code, with an explicit unknown case. New values can be added; an unknown value should surface as 'unhandled step' rather than crashing or silently stopping.",
    "Write one dispatcher that switches on the type. Every screen in your flow is reached from it. Resist adding a second path 'just for this case'.",
    "Always take endpoint from the response. If your code contains a string-concatenated rail URL, it will break the next time a payer changes something.",
    "Treat await_decision as a state, not a wait. Persist it, honour poll_after_ms, and let the callback settle it if one arrives first.",
    "Treat none as terminal for the current step, not for the flow. It means nothing more is needed here, and your own workflow decides what happens next.",
    "Log the type on every transition. When a flow stalls in production, the sequence of next_action values tells you where it stopped.",
  ]}
/>

## The common mix-up

`none` and `await_decision` look similar from a UI perspective, because in both cases nothing
is required of the operator right now. They are not the same.

`none` means the rail has nothing further for you at this point. `await_decision` means a
decision is outstanding somewhere else and will arrive, and that you are expected to be
listening. Treat the second as the first and a preauth approval can sit unnoticed for a
day.

## Errors carry their own routing

Failures use the same discipline. Every error carries `error`, `message`, `path` and
`fix_stage`. Validation returns every reason at once rather than one at a time, so a
correction takes one round rather than a queue of them.

`fix_stage` tells you where in the sequence the correction belongs. Combined with
`next_action.type: "fix"` and the `reasons[]` array at pre-flight, you can route an operator
straight to the screen that can resolve the problem instead of asking them to hunt.

**Next:** see it drive a real sequence on the [golden path](/rail/stops/Identify).
