# `PhoenixKit.API.Flow`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/flow.ex#L1)

Steps 7 and 8 of the request flow, written as small functions over the
executor's request state so `PhoenixKit.API.Executor.execute/4` stays a plain chain:

    ... Flow.merge_header -> validate -> Flow.split_control ->
        Flow.idempotency -> Flow.gate -> run

Each step returns `{:ok, state}` to go on, `{:error, code, detail, context, state}`
to refuse (the executor answers a problem document), or `{:done, state}` when the
answer is already in `state.body` (a preview, a replay): the executor then responds
without running the handler. `finish/1` and `release/1` are called when the
response is sent: they store the answer under the idempotency claim, or give the
claim up.

The request state is the executor's map. Besides its own keys the steps read and set
`control`, `dry_run?`, `input_hash`, `claim`, `replayed?`, `held_again?`, `store?`,
`outcome`, `confirmation`, `preview` and `result` (the answer as a decoded map, for a
transport that wants the map rather than the text). The executor resolves
`request_context` once, before the gate, for every context the steps build.

# `finish`

Called when the answer is sent. Stores a `200` body under the claim when it may be
stored (`Idempotency.storable?/2`, given the tool's own credential fields), otherwise gives
the claim up. Never raises: the handler has already run, so a store that fails (a database
hiccup) is logged and the answer is sent anyway; the claim then runs out with its lease.

# `gate`

Step 8. A read tool passes. `dry_run: true` runs the handler with `ctx.dry_run? == true`
and answers the preview (no code, no approval). A write of a person's live session
(`kind: :user`) never runs on this call, dangerous or not: it is held for the person to
confirm (`session_flow/1`). Any other write call of a key that requires approval goes to
the approval queue, dangerous or not, and a `confirm_token` from such a key is refused.
For every other caller a dangerous call (`dangerous?/1`) goes through the confirmation
flow, and any other call passes.

# `idempotency`

Claims the request's `Idempotency-Key`, if it has one. A dry run never claims.
A public call without a client token has no scope to claim in and ignores the key.

# `merge_header`

The `Idempotency-Key` header of a write call becomes the `idempotency_key` field, so
the schema validates it like the field. Both given and different, or the header sent
twice, is a validation error. A read tool ignores the header, and so does a request
that does not read it (`idempotency_header?: false`, the MCP transport: its key is the
argument), and a write tool that cannot replay (`ControlFields.replay_tool?/1`).

# `redeem_for_session`

The person confirms the write their session's call was held for: the step that takes
the place of `gate/1` in `PhoenixKit.API.Executor.redeem_as/6`. The state has passed the
whole request path again, so this checks only the row: it must be the person's
(`Confirm.lookup_for_user/2`), for the same tool and arguments, still waiting
(`Confirm.redeemable/4`); then the handler runs as a dry run for a fresh fingerprint and
`Confirm.claim_token/3` spends the row, atomically. `{:ok, state}` goes on to run the
handler.

When the values changed since the preview, nothing runs: the row is replaced by a new
one with the fresh preview (`Confirm.reissue_for_user/7`) and the answer is the prompt of
the new row with `"reason" => "changed"`. Any other refusal is `409
confirm_token_invalid` with `context.reason` `invalid` (unknown or somebody else's),
`other_arguments`, `used`, `rejected` or `expired`, without a preview.

# `release`

Called when the request is refused: gives the idempotency claim up. Never raises.

# `split_control`

Takes the control fields out of the validated input. The handler gets what is left;
`input_hash` is computed from it, so a confirming call hashes like its preview.

The input of a write tool must not hold a NUL character (JSON `\u0000`): a code or an
approval keeps the input and its preview in `jsonb`, which cannot store one, so the call
is refused here, before anything is claimed or written.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
