Errors
OKE codes, gate denials, and subsystem errors — stable numeric codes with causes and fixes.
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).
OKE1110 domain table not found — migrations have not been applied.
→ run `oke db migrate` against this environment.
https://oke.omqkhafi.dev/e/1110Codes 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).
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.
Smallest Example
Return a typed failure
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;
},
}),
);Handle it on the client
const { data, error } = await api.notes.get({ id });
if (error?.code === "NotFound") {
// value — not a throw
}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.
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 |
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.
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
Explicit effects drifted from do. Add the missing ledger entry or remove the hand-written
effects block so inference covers the Flow.
Import @/flows/generated, or pass an explicit path: http.get("/users/:id").
A resource mount and a handwritten route share the same method + path. Drop one binding.
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.
Domain tables missing — prod has no auto-DDL. Run oke db migrate against that environment
(CLI).
These are gate denials returned as values before do runs — not thrown OKE codes. Fix the policy,
membership, or rate budget. See Gate.
A vault contract has no value in any resolution layer. The error lists every gap — set the missing names (Vault).
Learn more
- i18n — catalogs,
fx.t, locale matching - Flow —
fx.failand the response envelope - Gate — where the three denials come from
- CLI —
oke db migrateand friends - fx — how failures and effects surface