---
url: 'https://adk.nht.io/the-loop/gates/lifecycle.md'
description: >-
  Settlement semantics and what suspension actually blocks — the in-memory
  mechanics from open to close.
---

# Gate lifecycle

Settlement and suspension — the mechanical pieces of a gate from open to close.

[Gates](../gates) covers the gate contract and minimum usable shape; [Canonical gate applications](./applications) covers the four use cases gates exist to support; [Durability and integration](./durability-and-plugs) covers observability, the seams gates plug into, and where to go next.

## Settlement semantics

Four outcomes exist because "closed" is not enough information. **Resolved** is a deliberate external decision, and the value must pass the gate schema before it becomes the answer. **Rejected** is a deliberate veto, so there is no payload to validate; **aborted** is a turn-level force-close that does not wait for the resolver; **timeout** means no decision arrived before the SLA fired. You care which one happened because recovery, audit, retries, and operator blame are different for each case.

| Settlement | Triggered by | Promise outcome | Schema check? |
| --- | --- | --- | --- |
| Resolved | [`TurnGate.resolve`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#resolve)`(value)`. | Resolves with the (validated) value. | Yes — runs before settlement. Failed validation throws [`E_INVALID_TURN_GATE_RESOLUTION`](../failure) in the resolver's context; the gate stays open. |
| Rejected | [`TurnGate.reject`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#reject)`(error)`. | Rejects with the supplied error. | No. |
| Aborted | Turn `AbortController` fires, or [`TurnGate.abort`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#abort) is called. | Rejects with [`E_TURN_GATE_ABORTED`](../failure). | No. |
| Timed out | The optional `timeout` (ms) elapsed before any of the above. | Rejects with [`E_TURN_GATE_TIMEOUT`](../failure). | No. |

::: warning Resolve is a *signal*, not the work
[`TurnGate.resolve`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#resolve)`(value)` is the moment the awaiter wakes up — it is not the moment your operator's "Approve" handler runs its business logic. Whatever produced `value` happened earlier. Whatever acts on `value` happens in the middleware on the other side of `await ctx.waitFor`. Putting side effects inside the resolver is a category error.
:::

::: warning A schema failure leaves the gate open
This is deliberate. A malformed approval is not an answer. The gate stays open, the operator retries, and bad data does not get fossilized as a settlement.
:::

::: warning Surface resolver-side schema errors
Schema validation errors are thrown in the resolver's context, not the awaiter's. If the operator clicks Approve and your UI sends a bad payload, the ADK throws [`E_INVALID_TURN_GATE_RESOLUTION`](../failure) back at that resolver call and the gate stays open. Catch that error and show it to the operator, or they are clicking into a black hole.
:::

## What "suspends" actually means

A gate is not a background promise the turn politely steps around. The middleware pipelines are sequential: each middleware does its work, calls `await next()` to hand off to the next middleware, and resumes when the downstream pipeline returns. That means awaiting a gate **before** calling `next()` suspends the entire pipeline at that point — every middleware later in the same pipeline is waiting for the current one to return.

What that does and does not block:

* **Within the same pipeline:** every middleware downstream of the awaiter is blocked. If `turnInputPipeline[3]` awaits a gate before calling `next()`, then `turnInputPipeline[4..n]` does not run, dispatch does not start, and `turnOutputPipeline` does not run until the gate settles. That is the normal pipeline behaviour, not a special gate property. If you intend the rest of the pipeline to run while the gate is open, await it **after** `next()` (run-as-a-post-step pattern) or open the gate inside a tool handler instead.
* **Concurrent gates:** if a middleware opens multiple gates via `Promise.all`, they run concurrently — only the parent middleware is blocked, and only until all settle.
* **Tool handlers awaiting a gate:** the handler blocks; the dispatch iteration that invoked it blocks; the dispatch loop blocks until the handler returns. The same pipeline rule applies — a gate inside a handler holds the iteration open.
* **Event emission:** synchronous. Events emitted before the await reach their listeners; listeners run on their own clock. A gate does not pause already-emitted events. The trap is emit-before-gate: observers see that event regardless of the gate outcome, and they may act on it before you even know whether the gated action is approved.
* **Other turns on the same runner:** unaffected. Each `run()` call has its own pipeline.
* **The abort signal:** still live. Aborting the turn rejects every open gate with [`E_TURN_GATE_ABORTED`](../failure) and unblocks the awaiter.

A turn can hold many concurrent open gates (one per pipeline that is currently mid-await). They settle independently. Aborting the turn aborts all of them.

::: warning Where you open the gate determines what it blocks
A gate awaited before `next()` in `turnInputPipeline[0]` blocks the whole turn from making progress until it settles. A gate awaited inside an `turnOutputPipeline` after `next()` blocks only the rest of the post-dispatch pipeline. A gate inside a tool handler blocks the dispatch iteration that called the tool. Choose the location based on what *must* stop while the gate is open. Get it wrong and you have a pipeline topology bug: work runs too early, waits too long, or observes state from the wrong side of the decision.
:::

::: tip If you find yourself wanting to "let the turn keep going while a gate is open"
Use one of these shapes: (a) open the gate in a `Promise.all` alongside other work inside the same middleware, (b) move the gate to a later pipeline stage so earlier stages can complete first, or (c) open the gate from a tool handler so only that one dispatch iteration is held.

The wrong shape is using `Promise.all` when you actually need sequential gates, or opening the gate before `next()` when you meant to open it after.
:::
