OKE has two error families: **failures are values** a Flow returns
(`{ data: null, error: { code, data } }`), while **framework errors are thrown**
for invariant violations (permanent numeric code + cause + fix).

```text
OKE1110  domain table not found — migrations have not been applied.
         → run `oke db migrate` against this environment.
         https://oke.omqkhafi.dev/e/1110
```

Codes are stable after the domain-range renumber — match them safely across upgrades
(see the Unreleased Breaking Changes mapping if you still have a pre-renumber note).

<Callout title="The one rule">
  Switch on `error.code` for Flow failures. Catch / match `OKE####` only for thrown framework
  invariants. Do not treat a typed `NotFound` as an unhandled exception.
</Callout>

## Smallest Example

<Steps>

<Step>
### Return a typed failure

```typescript
on(
  http.get({
    in: z.object({ id: z.string() }),
    errors: { NotFound: z.object({ id: z.string() }) },
  }),
  flow({
    do: async ({ id }, fx) => {
      const [row] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
      if (!row) return fx.fail("NotFound", { id });
      return row;
    },
  }),
);
```

</Step>

<Step>
### Handle it on the client

```typescript
const { data, error } = await api.notes.get({ id });
if (error?.code === "NotFound") {
  // value — not a throw
}
```

</Step>

</Steps>

## Localized messages

Typed failures and OKE codes ship English and Arabic ICU catalogs. Locale comes
from `Accept-Language` (matched to `i18n.locales`); fallback is `i18n.default`.

| Surface            | Keys                                       | How it appears                           |
| ------------------ | ------------------------------------------ | ---------------------------------------- |
| `fx.fail` / `fail` | `errors.{code}` · `errors.{code}.{reason}` | Optional `error.message` on the envelope |
| Thrown `OkeError`  | `oke.{code}.cause` · `oke.{code}.fix`      | Cause + fix lines in the thrown message  |

Override via `defineLocale`. Pass `fail(code, data, { message })` for a custom
string. Custom app codes stay message-less until registered. Full catalogs:
[i18n](/docs/reference/i18n).

## OKE numeric codes

| Code   | Name                   | Cause                                                  | Fix                                                       |
| ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
| `1001` | undeclared read        | Flow reads a resource not in `effects.reads`           | Add it to the flow's `effects.reads`                      |
| `1002` | undeclared write       | Flow writes a resource not in `effects.writes`         | Add it to the flow's `effects.writes`                     |
| `1003` | undeclared emit        | Flow emits a signal not in `effects.emits`             | Add it to the flow's `effects.emits`                      |
| `1004` | undeclared send        | Flow sends a template not in `effects.sends`           | Add it to the flow's `effects.sends`                      |
| `1005` | undeclared ask         | Flow asks a prompt not in `effects.asks`               | Add it to the flow's `effects.asks`                       |
| `1006` | undeclared secret      | Flow reads a secret not in `effects.secrets`           | Add it to the flow's `effects.secrets`                    |
| `1007` | undeclared call        | Flow calls a flow not in `effects.calls`               | Add it to the flow's `effects.calls`                      |
| `1008` | undeclared fetch       | Flow fetches a host not in `effects.fetches`           | Add the hostname to the flow's `effects.fetches`          |
| `1009` | undeclared embed       | Flow embeds with a model not in `effects.embeds`       | Add it to the flow's `effects.embeds`                     |
| `1020` | no effects declared    | Flow has no `effects` and no Manifest to infer from    | Run `oke build` / `oke dev`, or declare effects           |
| `1030` | adopt barrel stale     | A `src/flows/<unit>` folder was not adopted            | Run `oke dev` or `oke build` to regenerate `generated.ts` |
| `1040` | HTTP path unresolved   | Pathless `http.get()` never received a file-tree stamp | Import `@/flows/generated`, or pass `http.get("/…")`      |
| `1041` | HTTP route clash       | Two HTTP flows share the same method + path            | Give each flow a unique method + path                     |
| `1045` | HTTP flow unnamed      | Adopted HTTP flow still has no `unit.export`           | Export from `flows/<unit>/` or pass a named `flow`        |
| `1050` | live exposure dup      | Same signal, gates, and match on two GET routes        | Change the gate or path-param filter                      |
| `1060` | MCP tool duplicate     | Two MCP tool bindings share the same tool name         | Give each MCP tool exposure a unique name                 |
| `1070` | flow name duplicate    | Two Flows share the same Manifest / `fx.call` name     | Give at least one an explicit `flow("…")` or tree export  |
| `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once`    | Use `signal.broadcast`, or bind only one Flow             |
| `1072` | flow unnamed           | Signal / Clock consumer still has no `unit.export`     | Export from `flows/<unit>/` or pass a named `flow`        |
| `1110` | schema missing         | Domain table absent in `prod` — no auto-DDL            | Run `oke db migrate` against this environment             |
| `1210` | live resume gap        | `Last-Event-ID` is not on the retained tape            | Reconnect without the cursor; remaining tape replays      |
| `1240` | orphan emit            | Emit with zero subscribers and `optional` false        | Add `on(signal, …)` or declare `optional: true`           |
| `1250` | signal schema          | Emit payload failed the signal's `schema`              | Pass a payload that matches `schema`, or remove it        |
| `1605` | channel schema         | Send payload failed the template's `schema`            | Fix template `data` payload or the template `schema`      |
| `1810` | tenant required        | Tenant-scoped op with no `fx.tenant.id`                | `switchTenant`, signed `tid`, or tenant header            |
| `1820` | tenant not member      | Client-supplied tenant id is not a membership          | Pick from `listTenants` or add the user as a member       |
| `1830` | tenant unknown scope   | Tenant role used an invented or `console:*` scope      | Use a declared application scope                          |

<Callout title="Effects are usually inferred">
  The 1001–1007 · 1008 · 1009 family exists for flows that declare effects explicitly. Most apps
  never write an `effects` block — inference covers them — so seeing one of these means an explicit
  declaration drifted from the code.
</Callout>

## Gate denials (typed failures)

Returned, not thrown — the request never reached `do`:

| Code           | When                                         | Payload                                                                                    |
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `Unauthorized` | Policy denied, request not authenticated     | —                                                                                          |
| `Forbidden`    | Policy denied, authenticated but not allowed | `gate`, `reason` (`tenant_required` · `not_member` · `unknown_scope` · `session_only` · …) |
| `RateLimited`  | Rate gate budget exhausted                   | `retryAfterMs`                                                                             |

## Framework validation failures

| Code                   | When                                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `ValidationError`      | Input failed the `in` schema, or a list param isn't whitelisted (`unknown list param "x"`)                          |
| `NotFound`             | A `store.resource` get/update/remove hit a missing row                                                              |
| `InvalidQuery`         | QUERY missing `Content-Type` (`reason: missing_content_type`) or body isn't JSON (`inconsistent_content`) — **400** |
| `UnsupportedMediaType` | QUERY `Content-Type` is present but not `application/json` — **415**, `Accept-Query` lists JSON                     |

## Subsystem errors

Thrown by specific subsystems — each names its own cause:

| Error                         | Thrown when                                                        | What to do                                                            |
| ----------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `VaultBootError`              | A vault contract has no value in any resolution layer              | Set the missing names — the error lists every gap                     |
| `VaultError` (`UNSUPPORTED`)  | Managed provider unknown                                           | Use an official id, built-in `vault`, or `env`                        |
| `VaultSealed`                 | Console rotate-master while this process holds no master key       | Export `OKE_VAULT_MASTER_KEY` or run `oke vault unseal`               |
| `VaultRotateBusy`             | Master-rotation lease held or a batch is already in flight         | Wait and continue, or resume with `oke vault rotate-master --new-key` |
| `VaultUnsupported`            | Console vault action on a non-builtin backend or missing SQL       | Use `drivers.vault = "vault"` and set `DATABASE_URL`                  |
| `AiPiiBuildError`             | Build: a flow sends PII fields to a third-party model              | Drop the fields or add `allowPii: true` — fields named                |
| `AiSchemaValidationError`     | A model response failed the prompt's `out` schema                  | Fix the prompt or the schema — response didn't conform                |
| `ScheduleNotOverridableError` | Console tried to edit a clock declared without `overridable: true` | Declare it overridable and redeploy                                   |
| `ClockResourceNotFoundError`  | Console action targeted an unknown clock name                      | Check the name against your `clock()` declarations                    |
| `DryRunWriteIsolationError`   | A write attempted inside a dry run                                 | Dry runs never write — use a real run                                 |
| `ManifestValidationError`     | The compiled Manifest failed schema validation                     | Re-run the build; the error names the offending entry                 |
| `CrossPlaneError`             | A user-plane token was used on the operator plane (or vice versa)  | Use the correct principal for the plane                               |
| `AttenuationError`            | A token was used beyond its attenuated scope                       | Re-issue with the needed scope                                        |
| `SessionError`                | Session token invalid, expired, or malformed                       | Re-authenticate                                                       |
| `OperatorError`               | Operator-plane operation failed its checks                         | Error message names the failed check                                  |
| `AccessGrantError`            | Console access grant rejected                                      | Re-request access with a valid grant                                  |
| `UnsupportedPathError`        | A route path shape the router can't compile                        | Simplify the path pattern                                             |

## Troubleshooting

<Accordions>

<Accordion title="I hit OKE1001–1009">
  Explicit `effects` drifted from `do`. Add the missing ledger entry or remove the hand-written
  `effects` block so inference covers the Flow.
</Accordion>

<Accordion title="OKE1040 pathless HTTP never stamped">
  Import `@/flows/generated`, or pass an explicit path: `http.get("/users/:id")`.
</Accordion>

<Accordion title="OKE1041 method + path bound twice">
  A resource mount and a handwritten route share the same method + path. Drop one binding.
</Accordion>

<Accordion title="OKE1072 Signal or Clock flow unnamed">
  Cause: `A {kind} flow on "{trigger}" has no name.`
  Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`. See
  [Signal](/docs/elements/signal) · [Clock](/docs/elements/clock).
</Accordion>

<Accordion title="OKE1071 once signal bound to more than one Flow">
  Cause: `Once signal "{signal}" is bound to more than one Flow ({flows}).` Use `signal.broadcast`
  if each Flow should get a copy, or bind only one Flow. See [Once · Competing
  consumers](/docs/elements/signal/once#competing-consumers-once-vs-broadcast).
</Accordion>

<Accordion title="OKE1110 in production">
  Domain tables missing — prod has no auto-DDL. Run `oke db migrate` against that environment
  ([CLI](/docs/reference/cli)).
</Accordion>

<Accordion title="Unauthorized / Forbidden / RateLimited">
  These are gate denials returned as values before `do` runs — not thrown OKE codes. Fix the policy,
  membership, or rate budget. See [Gate](/docs/elements/gate).
</Accordion>

<Accordion title="VaultBootError at startup">
  A vault contract has no value in any resolution layer. The error lists every gap — set the missing
  names ([Vault](/docs/elements/vault)).
</Accordion>

</Accordions>

## Learn more

- [i18n](/docs/reference/i18n) — catalogs, `fx.t`, locale matching
- [Flow](/docs/elements/flow) — `fx.fail` and the response envelope
- [Gate](/docs/elements/gate) — where the three denials come from
- [CLI](/docs/reference/cli) — `oke db migrate` and friends
- [fx](/docs/reference/fx) — how failures and effects surface

## Next

<Cards>
  <Card title="fx" description="The door that records effects." href="/docs/reference/fx" />
  <Card
    title="Gate"
    description="Where Unauthorized and Forbidden come from."
    href="/docs/elements/gate"
  />
  <Card title="Client" description="Switch on error.code in the browser." href="/docs/client" />
</Cards>
