Docs Product
Decisions
A decision is KIFF’s answer to “is this action allowed, right now?” — produced before the action runs. This page covers what’s evaluated, the four outcomes, and the lifecycle from proposal to recorded decision.
What this does
Your runtime proposes an action. KIFF evaluates that proposal against
the entity’s current state and the control’s policy, and returns a
decision. Your runtime then acts on the answer: run the action on
allowed, withhold it otherwise. The decision happens at the gate, not
after the fact.
The decision flow
your runtime: about to run an action
│
▼
propose to KIFF ──► KIFF evaluates:
│ • entity's current state (from events)
│ • the control's policy + thresholds
│ • required parameters present?
│ • does the actor's identity hold authority?
▼
decision returned: allowed | blocked | approval_required | invalid
│
▼
your runtime acts on it:
allowed → run the action
blocked / invalid → do not run
approval_required → hold until a human resolves it
│
▼
one signed receipt recorded, either way
The four outcomes
allowed— the proposal satisfies the policy for the current state. Your runtime runs the action.blocked— a governance answer: the proposal is well-formed, but the action is not permitted in this state or breaks a rule (over a threshold, wrong state, missing authority). KIFF evaluated it and said no. Your runtime does not run it.approval_required— the action needs a human sign-off first. It is held; your runtime does not run it until the approval is resolved.invalid— the proposal is a broken request, not a governance answer: it is malformed or misconfigured against the contract — a required parameter is missing, a parameter fails the contract’s constraints, the contract is invalid, or it has no executor. There was nothing to decide; the request itself needs fixing.
A useful invariant: anything that is not exactly allowed means the
action should not run. When KIFF runs in enforce mode through the guard,
that withholding is the default behavior, and an error fails closed
(withheld) rather than silently allowing. See How the guard
works for the fail-safe posture.
blocked vs invalid — a decision vs a broken request
The two are distinct on purpose, and your integration should treat them differently:
blockedis a governance decision. KIFF looked at a valid request and refused it. It returns successfully (the decision is the result), counts as a governed operation, and surfaces in your evidence and alerts — it’s a real thing that happened.invalidis not a governance decision. The request never got as far as one — either it was malformed/misconfigured, or the data plane couldn’t evaluate it right then. Either way it comes back as an error (not a success), is not counted as a governed operation, and does not raise a governance alert. Two sub-cases, and they call for different handling:- Malformed or misconfigured (missing/invalid parameter, invalid
contract, no executor) → a client/config error (an HTTP
4xx) that is yours to fix — the same way a bad request to any API is the caller’s to correct, not a policy event. - Transient data-plane error (the runtime, state store, or action
registry was briefly unavailable) → returned as an HTTP
5xxyou should retry, not fix; nothing was wrong with your request.
- Malformed or misconfigured (missing/invalid parameter, invalid
contract, no executor) → a client/config error (an HTTP
So a missing or invalid parameter is invalid (fix your request), not
blocked (a policy said no). Branch on this: on blocked, respect the
decision; on invalid, fix a 4xx or retry a 5xx.
What data is evaluated
A decision is made against:
- the entity’s state at the moment of the proposal, derived from the events KIFF has recorded — not a value the caller asserts;
- the control’s policy — permitted states, required parameters, thresholds, approval rules — which you defined;
- the actor’s identity, resolved server-side. The roles a request carries are descriptive; authority is resolved from policy keyed to an authenticated identity, never from what the caller claims about itself.
The proposal / decision lifecycle
- Proposal — your runtime submits the action it intends to take, with the entity and parameters.
- Evaluation — KIFF reads the current state and applies the policy.
- Decision — one of the four outcomes is returned, synchronously, before execution.
- Hold (if
approval_required) — the proposal waits for a human resolution rather than running. - Record — exactly one signed receipt captures what was decided and on what basis. See Receipts & proof packs.
Reading an entity’s lifecycle
The proposals, validations, approvals, and executions KIFF has recorded for a single entity can be read back as one chronological, read-only view — the entity’s governed history in order, assembled from records KIFF already keeps. It’s a projection, not a new decision path: it changes nothing and simply surfaces what happened. Like every read surface it is scoped to your tenant — you only ever see your own entities — and a valid entity with no history yet returns an empty lifecycle rather than an error.
Example shape
The exact request/response contract for the decide endpoint and the
guard’s enforce mode lives with the runnable code in the public guard
repo, kiff/kiff-guard, and on
each framework page. Conceptually, your runtime sends the
action and entity, and receives a decision with an outcome and a trace
reference for the receipt:
proposal: action=ISSUE_REFUND entity=order/ord_4821 amount_cents=2500
decision: outcome=blocked reason=state_not_allowed (order already FULLY_REFUNDED)
→ one receipt recorded; your runtime does not issue the refund

A blocked decision from a clean demo tenant — the action, the entity state at decision time, and the reason (state_not_allowed).
What KIFF does not do here
KIFF returns the decision. It does not execute the action on allowed —
your runtime does — and it does not undo anything downstream. The
decision proves what was authorized before execution; it does not prove
what your system did afterward. That distinction is the heart of
Receipts & proof packs.