-
Notifications
You must be signed in to change notification settings - Fork 0
Error Codes
Every framework failure is an UltimateError carrying a stable X_* code, a concrete cause, and the exact command or edit that fixes it. Same content in the terminal, the dev overlay, the HTTP body, the MCP tool result and --json.
X_DB_DRIFT: schema differs from migrations
cause: table "posts" has column "publish_at" not present in any migration
fix: x db gen "add publish_at"
{"code":"X_DB_DRIFT","title":"schema differs from migrations","cause":"table \"posts\" has column \"publish_at\" not present in any migration","fix":"x db gen \"add publish_at\"","docs":"https://ultimate.dev/errors/X_DB_DRIFT"}| Rule | Detail |
|---|---|
| Lookup |
x errors explain <CODE> --json, or the MCP errors.explain tool — same content as this page |
| Uniqueness | one code is owned by exactly one package; a collision throws X_ERROR_CODE_DUPLICATE at registration |
| Stability | codes never change meaning. A renamed concept gets a new code and the old one stays documented |
fix |
always runnable or editable as written. If a fix reads "do the right thing", that is a bug — file it |
Bare Error
|
never thrown by the framework. Anything caught is normalised through toUltimateError()
|
| Registration | a code exists when its owning package calls registerErrorCodes(). That one call is what makes it explainable, unique and documented-or-fail — a code emitted as a Finding rather than thrown is registered the same way |
| Enforcement | the errors step of x verify fails on an empty or advice-only fix (X_ERROR_FIX_INVALID), on a declared code with no row on this page (X_ERROR_CODE_UNDOCUMENTED), and on a row this page presents as live that no package registers (X_ERROR_CODE_UNREGISTERED) |
As of 2026-08 every code above Reserved codes resolves through x errors explain, with one exception the gate knows about: this repository's own gate scripts (X_BOUNDARY_VIOLATION, X_ROADMAP_*, X_REFERENCE_APP_*, X_SETUP_INSTALL_FAILED, X_ADMIN_FLATTENER_VIOLATION) never ship, so no package may own them. See Troubleshooting for symptom-first triage and CLI reference for the commands named in the fixes.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ABORTED |
operation aborted | the request's AbortSignal fired: client disconnected or a deadline passed |
none — stop work and return |
X_INTERNAL |
unexpected internal framework error | a non-Ultimate error escaped into framework code | fix the failure named in cause, then re-run |
X_INVARIANT |
invariant violated | framework state that "cannot happen" happened | report it with x doctor --json output attached |
X_UNREACHABLE |
unreachable branch was reached | an exhaustive switch met a value the type says cannot exist |
narrow the union at the call site |
X_NOT_IMPLEMENTED |
this driver does not implement the requested feature | an interface-complete driver whose remote half is unwritten | use the default driver, or implement the named method |
X_NO_CONTEXT |
no request context is active | framework code called outside the ALS context | runWithContext(createContext({ … }), fn) |
X_SERVICE_MISSING |
service is not registered on the request context |
ctx.<service> used without providing it |
pass it in createContext({ services: { … } })
|
X_SERVICE_DUPLICATE |
a service name is registered twice | two defineService('name', ...) calls used the same name |
rename one of the two declarations |
X_ROLE_INVALID |
ROLE is not a known runtime role |
typo or an old role name in the env | set ROLE to web, sync, worker, scheduler, migrate or replicator
|
X_DRAINING |
process is draining and refuses new work | work arrived after SIGTERM | retry against another replica; the LB should already have removed this one |
X_SHUTDOWN_TIMEOUT |
graceful shutdown exceeded its deadline | an in-flight handler outlived DRAIN_TIMEOUT
|
raise configureLifecycle({ deadlineMs }) or shorten the slow handler |
X_ID_INVALID |
value is not a valid id | a hand-built string passed where a typed id is required | generate ids with uuid() / typedId<'post'>() from @ultimat3/core
|
X_CURSOR_INVALID |
pagination cursor is malformed, tampered with or from another query | signature mismatch — an edited cursor, or ULTIMATE_CURSOR_SECRET rotated — or a cursor built for a different query, filter or sort order |
drop the cursor and request the first page (after: null) |
X_CURSOR_SECRET_DEV |
cursors are signed with the shipped development key |
ULTIMATE_CURSOR_SECRET is unset in a production environment, so the signing key is the constant published in @ultimat3/core and a client can forge a page position. Reported by x doctor, never thrown |
export ULTIMATE_CURSOR_SECRET="$(openssl rand -hex 32)" |
X_ERROR_CODE_DUPLICATE |
error code registered twice | two packages declared the same code | rename the colliding code in the registering package's src/errors.ts
|
X_ERROR_REPORTER_DSN_INVALID |
the error monitor DSN is malformed |
SENTRY_DSN is not a URL, carries no public key, names no project, or uses a scheme that is not http/https. Thrown at boot rather than at the first failure, because a monitor that was never connected looks exactly like an app that never failed |
set SENTRY_DSN=https://<publicKey>@<host>/<projectId> in .env, then run x env check
|
X_REGISTRAR_MISSING |
no registrar is loaded for a primitive kind | the owning package is absent from the graph, so nothing announced a registrar — defineApi({ queries }) without @ultimat3/query. meta.kind names the kind, and the owner is @ultimat3/<kind>; importing it is what announces |
bun add @ultimat3/<kind> |
X_REGISTRAR_CONFLICT |
two different registrars are loaded for one primitive kind | two copies of @ultimat3/<kind> in the dependency tree, each with its own registry, so half the primitives register where nothing reads them. bun pm why @ultimat3/<kind> names the dependents when ranges genuinely disagree |
bun update @ultimat3/<kind> |
Counter, gauge and histogram live in @ultimat3/core (metrics.ts), aggregate in process, and are read through one collectMetrics(). The wire format is a driver: MetricExporter is the seam, the default is a no-op, and metricsText() renders the Prometheus/OpenMetrics scrape body behind METRICS_PATH. The framework ships no OTLP client — pointing OTEL_EXPORTER_OTLP_ENDPOINT at a collector means supplying a MetricExporter that speaks it, or scraping /metrics with an agent that does.
The three series docker/helm autoscales on are http_requests_total (the counter rps is derived from), connections and queue_depth — SCALING_METRICS in runtime-metrics.ts maps each ScalingSignal to its series, so the role table and the chart cannot drift apart.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_METRIC_NAME_INVALID |
metric name is malformed or already declared with another kind | a name that is not lowercase snake_case — dotted OTel names survive OTLP but not a Prometheus scrape, and the autoscaler reads the scrape — or one name declared as both a counter and a gauge |
rename the instrument: counter('http_requests_total'), one metric name to one kind |
X_METRIC_VALUE_INVALID |
metric value is not recordable | a NaN/Infinity observation, or a counter decremented — a counter is a cumulative total and only goes up |
pass a finite value, and use gauge(name) for a number that can fall |
One pipeline in @ultimat3/core serves storage, seo and pwa. It decodes and encodes PNG and JPEG only; WebP, AVIF, GIF and SVG are identified and measured — enough to inline width/height and keep CLS at 0 — but never synthesised. A variant in one of those formats comes from a CDN or an ImageTransformDriver, never from a silent passthrough.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_IMAGE_UNSUPPORTED |
the built-in image pipeline cannot read or write this format | a WebP/AVIF source, a request for an AVIF variant, or a colour that is not hex or transparent
|
transformImageBytes(bytes, { format: 'png' }) (or 'jpeg'), or pass an ImageTransformDriver that produces the format — meta.format names the one refused |
X_IMAGE_DECODE_FAILED |
image bytes are malformed, truncated or internally inconsistent | a partial upload, a corrupted file, or a header that disagrees with the data that follows |
file <path> to confirm the type, then re-export the image and retry |
X_IMAGE_TOO_LARGE |
image exceeds the pipeline pixel ceiling | a header declaring more than 64 megapixels — usually a decompression bomb, occasionally a real scan |
transformImageBytes(bytes, { width: 4000 }) before it reaches the pipeline, or raise MAX_IMAGE_PIXELS deliberately |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_CONFIG_INVALID |
a configuration this process cannot boot on — env or app.config.ts
|
a field failed its schema, a required field is missing, or two keys that each parse contradict each other (selectMailDriver, selectPurgeDriver) |
x doctor --json and fix the named key; see Configuration
|
X_ENV_MISSING |
required environment variables are missing or invalid | a key absent at boot; validation runs before the server listens |
x env check --fix, then set the keys it names |
X_ENVIRONMENT_INVALID |
ULTIMATE_ENV is not a known environment |
a spelling the framework does not use (prod, dev, preview). NODE_ENV is only a fallback and is never policed — CI images legitimately set it to anything |
export ULTIMATE_ENV=production — one of development, test, staging, production
|
X_ENV_EXAMPLE_DRIFT |
.env.example does not declare every variable the schema requires |
a key added to defineEnv() and not to the committed example, so the next clone boots into X_ENV_MISSING for a variable nobody documented |
x env example — the example is a projection of the schema, never a second list |
X_BUN_VERSION |
Bun is older than the framework floor | Bun < 1.3 | bun upgrade |
X_NOT_IN_APP |
command must run inside an Ultimate app | no app.config.ts at or above the cwd |
x new myapp && cd myapp |
X_PORT_IN_USE |
the dev port is taken | another x dev or an unrelated process holds it |
x dev --port 3001, or stop the other process |
X_PORT_INVALID |
PORT is not a TCP port number |
a platform-injected PORT that is empty, non-numeric or out of range — never defaulted past, because a web role that quietly bound 3000 fails the platform's health probe with nothing in the log that names the cause |
docker run -e PORT=3000 <image> |
X_ROLE_UNKNOWN |
ROLE names something that is not a role |
a typo in a compose file, a Helm value or a docker run — one image runs every role, so the typo would otherwise start a process that serves nothing and reports healthy |
docker run -e ROLE=web <image> — one of web, sync, worker, scheduler, replicator, migrate
|
One committed file, secrets.enc.json, sealed with AES-256-GCM under a 32-byte master key read from ULTIMATE_SECRETS_KEY first and the gitignored .secrets.key second. Values are decrypted into the process environment by installSecrets() and read back through the app's own envSchema — a secret is an env var, so it has one declaration, one mask and one reader. See CLI reference for x secrets.
The envelope carries a key id: a domain-separated, truncated SHA-256 of the master key, safe to commit. It is what makes "you used the wrong key" and "this file was edited after it was sealed" two different codes instead of one unexplained decryption failure.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_SECRETS_KEY_MISSING |
no master key for the encrypted secrets file |
ULTIMATE_SECRETS_KEY is unset and .secrets.key does not exist, while a sealed file does. Fatal at boot on purpose — an app that started without its secrets authenticates against nothing and still reports healthy |
export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)", or x secrets init in a repo that has no key yet |
X_SECRETS_KEY_INVALID |
the master key is not 32 bytes of hex | a truncated paste, or a platform secret that carried quotes. An AES-256 key is exactly 64 lowercase hex characters | export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)" |
X_SECRETS_KEY_MISMATCH |
the secrets file was sealed with a different master key | a key from another environment, or a x secrets rotate interrupted between its two writes. Both key ids are named in the cause |
git checkout -- secrets.enc.json, or point ULTIMATE_SECRETS_KEY at the key whose id the file names |
X_SECRETS_FILE_MISSING |
the encrypted secrets file does not exist |
x secrets show/edit/set/rotate in a repo that never ran init. Never an empty listing — "no secrets" and "no file" are different facts |
x secrets init |
X_SECRETS_FILE_INVALID |
the encrypted secrets file is not a readable envelope | a merge conflict marker, a truncated write, an unknown v or alg, or a header field that is not base64. Nothing reached authentication, so this is not tampering |
git checkout -- secrets.enc.json — the file is written only by x secrets, never by hand |
X_SECRETS_TAMPERED |
the secrets file failed its authentication tag | under a key whose id already matched, the ciphertext or the header bound into it as AAD changed after sealing. GCM refuses rather than returning garbage |
git checkout -- secrets.enc.json, then x secrets show --json
|
X_SECRETS_PLAINTEXT_INVALID |
the decrypted secrets are not a flat map of env values | an x secrets edit buffer saved as something other than one flat JSON object, a key that is not an environment variable name, or an empty value. Refused before sealing, so a file that opens always installs |
x secrets edit — the buffer is {"SESSION_SECRET": "s3cr3t"}
|
X_SECRETS_EDITOR_MISSING |
no $EDITOR to open the decrypted secrets in |
neither VISUAL nor EDITOR is set. No editor is guessed — opening a decrypted file in a surprise program is the last place a default belongs |
EDITOR=nano x secrets edit, or write one value without an editor: printf %s "$TOKEN" | x secrets set STRIPE_KEY
|
X_SECRETS_EDIT_FAILED |
the editor exited non-zero, so nothing was resealed | a crash or a deliberate abort. The buffer is discarded and the committed file is untouched | x secrets edit |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_VALIDATION_FAILED |
value did not match its schema | a parse boundary rejected input | read cause for the failing path; correct the value or widen the schema deliberately |
X_SCHEMA_UNSUPPORTED |
the active schema provider cannot do this | a Standard Schema implementation without JSON Schema export | drop the configureSchemaProvider() call and use t, the shipped dependency-free builtin provider |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ROUTE_NOT_FOUND |
no route matches this request | path typo, or the route was never generated |
x routes --json, then x g route <path>
|
X_METHOD_NOT_ALLOWED |
route exists but not for this method | GET against an action endpoint | call it with the method in cause, or add that method |
X_ROUTE_CONFLICT |
two routes claim the same path | a copied route file |
x routes --json, then remove or rename one |
X_BODY_INVALID |
request body failed its schema | client sent the wrong shape |
x actions describe <name> --json, then match the input schema |
X_UNAUTHENTICATED |
route requires an authenticated actor | no session cookie or bearer token | send credentials, or set the route's auth to 'public'
|
X_FORBIDDEN |
policy denied this actor | the actor exists but the policy said no | x policy explain <path> --json |
X_RATE_LIMITED |
rate limit exhausted for this key | bucket empty | retry after Retry-After, or raise rateLimit.buckets in app.config.ts
|
X_BUILD_SKEW |
client build id does not match the server build id | a tab open across an incompatible deploy | reload; the SW fetches the new build manifest. See PWA and offline |
X_SERVER_NOT_STARTED |
server handle used before start()
|
reading url() too early in a test |
await createServer({ … }).start() first |
X_PIPELINE_NO_RESPONSE |
a pipeline stage produced no response | a middleware returned undefined
|
return a Response from the stage or from the handler |
X_NO_REQUEST |
the inbound request is not in scope here |
useRequestCookie() / setRedirect() called from a job, a task or a module body |
read it inside a route handler, an action or a page; a job gets the value from its payload |
X_ERROR_STATUS_INVALID |
an error code cannot be mapped to that status |
registerErrorStatus() given a framework-owned code, a status outside 100–599, or a second, different status for one code |
x errors list --json, then map a code this app owns to a status the framework does not already hold |
A denial is X_FORBIDDEN, above — @ultimat3/policy owns it and every surface adapter throws it, so there is one code for "the policy said no" wherever it was decided.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_POLICY_MISSING |
an action was declared without a policy | a new action shipped with no policy:
|
add policy: can('<resource>:<verb>'), or allow('public') to say so explicitly |
X_PERMISSION_UNKNOWN |
permission string is not in the permission set | typo, or a permission never declared | add it to definePermissions([…])
|
X_TENANCY_UNSCOPED |
a tenant-scoped query has no org predicate | a repo call that forgot the tenant | pass { orgId }, or wrap the plan with orgScoped(entity, orgId, plan)
|
X_ACTION_POLICY_MISSING |
an action was registered without a policy | build-time check on the action registry | add policy: can('<name>') to the declaration |
X_QUERY_POLICY_MISSING |
a query was registered without a policy | same, for reads | add policy: can('<name>') to the query |
X_QUERY_NOT_PAGEABLE |
a read returned rows with no id, so a cursor cannot name a position | a projection or aggregate that drops the primary key — the id is the tiebreak that makes the sort order total | return the key from the query's sql:, e.g. db.posts.select({ id: true, … })
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_SESSION_EXPIRED |
session passed its idle or absolute expiry |
cause names which of the two clocks ran out |
POST /auth/sign-in { email, password } for a fresh session, or raise session.absoluteTtlMs / session.idleTtlMs in defineAuth
|
X_MFA_REQUIRED |
password proven, second factor outstanding | the user has TOTP enrolled |
POST /auth/mfa/verify { code } with the 6-digit code, then retry |
X_OAUTH_STATE_INVALID |
state, nonce or PKCE verifier did not match | a replayed callback URL, a handshake that expired, or a token minted for another browser | restart the flow at GET /auth/oauth/<provider> — a callback URL is single-use |
X_OAUTH_EXCHANGE_FAILED |
the provider refused the exchange or returned no usable identity | wrong client secret, an unregistered redirect_uri, a spent code, or a missing scope |
meta.stage is token or userinfo: for token, match <PROVIDER>_CLIENT_SECRET and the registered redirect_uri, then x doctor --json; for userinfo, restart at GET /auth/oauth/<provider>
|
X_OAUTH_TOKEN_INVALID |
the id token failed its issuer, audience or expiry check | the client id in .env is not the one the authorize URL was built with, or this host's clock is skewed |
match <PROVIDER>_CLIENT_ID to the id beginOAuth() used, then restart the flow |
X_PASSWORD_WEAK |
strength check rejected the password | too short, or a known-common password | choose a longer, uncommon password — or relax defineAuth({ password: { minLength } })
|
X_ACCOUNT_LOCKED |
per-ip or per-account bucket is inside its lockout | repeated failed attempts |
x auth unlock <key> — cause names the key and the remaining seconds — or raise defineAuth({ rateLimit })
|
X_API_KEY_INVALID |
key unknown, revoked, expired or wrong | one shape for all four — a precise message is an enumeration oracle |
x auth keys list --json, then issue a fresh key |
X_AUTH_WRITE_FAILED |
an adapter write returned no row, so it cannot be confirmed | an insert … returning * wrote nothing — the table is missing its migration, or a trigger or RLS policy swallowed the row |
x db migrate, then confirm the table with x db query "select 1 from x_users limit 1" --json
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_DB_DRIFT |
schema differs from migrations | a column exists in code but in no migration |
x db gen "<describe the change>" then x db migrate
|
X_ENTITY_DUPLICATE |
two entities claim the same name | copy-pasted entity({ name })
|
rename one; x entities list --json
|
X_INVARIANT_VIOLATED |
a domain invariant rejected this row | a CHECK or a declared invariant failed |
x entity explain <entity> --json to see the invariant and its SQL |
X_NOT_FOUND |
no row for that id | stale id, wrong tenant, or already deleted | confirm with x db query "select id from <table> limit 5" --json
|
X_WRITE_UNFILTERED |
deleteWhere() or updateWhere() named no filter columns |
an empty filter, or one whose only value came back undefined — the two look identical at the call site, and either would reach every row |
name the columns that bound it, e.g. db.likes.deleteWhere({ postId, userId }). A deliberate whole-table write is a migration: x db gen "<name>"
|
X_PATCH_EMPTY |
updateWhere() named no columns to write |
an empty patch, or one whose only value came back undefined — reporting "n rows updated" for a statement that set nothing is the silent no-op the count exists to prevent |
name the columns to write, e.g. db.participants.updateWhere({ conversationId, userId }, { lastReadAt })
|
X_DB_UNAVAILABLE |
cannot reach the database | nothing listening on DATABASE_URL, a statement the embedded driver refused, or @electric-sql/pglite not installed for a pglite:// url |
set DATABASE_URL to a reachable Postgres, or x dev for the embedded PGlite — bun add @electric-sql/pglite when the url is pglite://
|
X_MIGRATION_CONFLICT |
the migration ledger disagrees with this build | a ledger row from an app version this build does not ship, or an applied migration whose file was edited so its checksum moved |
x db status --json — then deploy the version cause names, or x db gen "fix <migration>". Never edit an applied migration |
X_MIGRATION_IRREVERSIBLE |
this migration cannot be reversed without data loss | a generated plan that drops a column or a table |
x db gen "<name>" --allow-destructive, or keep the column and deprecate it |
X_BRANCH_EXISTS |
that branch database already exists |
x db branch create <name> twice — or a drop aimed at the database this session is connected to |
x db branch drop <name>, then re-create, or pick another name. To drop the connected one, reconnect elsewhere first: DATABASE_URL=.../postgres
|
X_SQL_UNSAFE |
SQL was built by string interpolation | a non-scalar interpolated into a sql template, or an identifier or branch name that is not [a-z0-9_-]+
|
pass a scalar (it becomes $n), a nested sql fragment, or wrap audited SQL in raw() — cause numbers the interpolation |
X_READONLY_VIOLATION |
a mutating statement reached a read-only client | an INSERT/UPDATE/DELETE sent through readOnly(db())
|
use db() instead of readOnly(db()), or rewrite the statement as a SELECT |
X_DB_GEN_FAILED / X_DB_MIGRATE_FAILED / X_DB_BRANCH_FAILED / X_DB_STUDIO_FAILED
|
the underlying x db step failed |
Postgres rejected the statement, or the DB is unreachable | read cause — it carries the SQL error verbatim |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ACTION_DUPLICATE |
two actions registered under one name | duplicate export names across features | rename one; names are global. x actions list --json
|
X_INPUT_INVALID |
input failed the action's schema | wrong shape from a client or an agent | x actions describe <name> --json |
X_OUTPUT_INVALID |
the handler returned a value its output schema rejects |
the handler drifted from the declared output |
x actions describe <name> --json, then fix the handler or the schema |
X_ACTION_FOREIGN |
a value that is not an action was projected as one | a hand-rolled object with kind: 'action', or an action from a duplicated copy of @ultimat3/action
|
declare it as export const name = action({ input, output, policy, handle })
|
X_ACTION_UNREGISTERED |
an action was projected before it was registered, so it has no name |
.tool() / .client() / .job() / .openapi() on an action registerActions() never named |
registerActions(await import('./actions')) at boot, before mounting routes |
X_IDEMPOTENCY_CONFLICT |
idempotency key reused with a different payload, or still in flight | a retried request mutated its body | send a fresh Idempotency-Key for a different payload; otherwise retry after the first settles |
X_CONTRACT_DRIFT |
the published contract changed | input/output shape moved without a version bump | give new inputs a .default(), or bump the package version |
X_RPC_FAILED |
the typed client could not reach the action or the query | gateway, network, or a non-JSON response | check the gateway, then x actions describe <name> --json (x queries describe for a read) |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_QUERY_DUPLICATE |
two queries registered under one name | duplicated export | rename one; x queries list --json
|
X_QUERY_FOREIGN |
a value that is not a query was projected as one | a hand-rolled object with kind: 'query', or a query from a duplicated copy of @ultimat3/query
|
declare it as export const name = query({ input, policy, sql })
|
X_QUERY_UNREGISTERED |
a query was projected before it was registered |
.tool() / .client() / .live() on a read registerQueries() never named |
registerQueries(await import('./live')) at boot, before serving reads |
X_MATCHER_UNSUPPORTED |
the live matcher cannot evaluate this SQL incrementally | a join, aggregate or unbounded predicate under live: true
|
simplify the sql, add orderBy + limit, or drop live
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_IDEMPOTENCY_REQUIRED |
the job has no idempotencyKey
|
the field was omitted — normally a compile error, checked again at registration | add idempotencyKey: (input) => '<stable>:' + input.id
|
X_JOB_DUPLICATE |
a name or a live key already has a job | a second enqueue inside the dedupe window, or a second job/task registered under a name already taken | pass onConflict: 'dedupe' or make the key narrower; for a name clash, rename one export |
X_STEP_DUPLICATE |
two step.run calls share a name |
copy-paste inside run()
|
rename one — step names are the replay key |
X_JOB_MAX_ATTEMPTS |
the job exhausted its retries | a step kept failing |
x jobs show <id> --json for the step trace, then x jobs retry <id>
|
X_JOB_TIMEOUT |
a job exceeded its wall-clock limit | one long step | raise timeout, or split the work into more step.run() calls |
X_OUTBOX_NO_TX |
enqueue outside a transaction |
<job>.enqueue called with no ambient tx |
wrap in ctx.tx(async (tx) => …), or enqueue with { outbox: false } deliberately |
X_DRIVER_UNAVAILABLE |
the queue driver is unreachable | Redis/NATS down, or the URL is wrong |
x doctor --json; check the driver URL in app.config.ts
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_TOPIC_FORBIDDEN |
the actor may not subscribe to this topic | no guard, or the guard denied | hub.guard('<topic>', ({ actor }) => …) |
X_SUBSCRIPTION_LIMIT |
socket or tenant hit its subscription cap | a component subscribing in a loop | raise realtime.limits.perSocket / perTenant, or unsubscribe unused live queries |
X_PROTOCOL_VERSION |
client and sync node disagree on the wire protocol | a client from an older build |
x build and redeploy the client; the node sends update-available before draining |
X_CURSOR_STALE |
the resume LSN is outside the change buffer | a long disconnect | pass snapshot to resumeFrom() so the fallback re-snapshots |
X_REBASE_CONFLICT |
a local mutation could not be rebased | server state moved incompatibly | set conflict: 'server-wins', or return a row from custom(merge)
|
X_TRANSPORT_UNAVAILABLE |
the fanout bus is unreachable | the nats-server NATS_URL names is down or misconfigured |
x doctor — then check NATS_URL
|
X_TRANSPORT_PROTOCOL |
the bus does not speak the protocol this build speaks | nats-server older than 2.11, JetStream not enabled, or something that is not NATS on the port | run nats:2.11-alpine or newer with -js; reconnecting cannot fix it, so this never retries |
X_REPLICATION_FAILED |
the replication connection was refused |
wal_level not logical, no publication, a slot another replicator holds, wrong credentials |
the message names the object; the fix is the exact ALTER SYSTEM / CREATE PUBLICATION / pg_drop_replication_slot statement |
X_REPLICATION_PROTOCOL |
the WAL stream cannot be decoded | a server or pgoutput version this build does not speak, or a proxy on the replication port |
x doctor db — point the URL at postgres itself, on a server ≥ 14 |
X_REPLICATOR_SLOT_HELD |
another replicator already owns this database |
replicator scaled to more than one replica, or a rolling deploy started the new replicator before the old one exited — nothing is wrong with this process, the database already has its one replicator |
scale the replicator to 1 per database: kubectl scale deploy/replicator --replicas=1
|
X_LIVE_CLIENT_MISSING |
a realtime hook ran with no LiveClient registered |
useLive / useConnection / useMutation / useMutationQueue on a page whose entry never registered one |
setLiveClient(new LiveClient({ signal: createSignal, connect, buildId })) in the app entry, above the first render |
X_LIVE_ROW_UNIDENTIFIED |
a live query returned a row with no id | a live: true read whose projection selects columns but not the primary key — patches, cursors and the local store all address a row by id
|
select the primary key in that query's sql(), or drop live: true from it |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_CACHE_TAG_UNKNOWN |
a tag no entity declared | typo in invalidates: [tag.pots]
|
x manifest to regenerate the tag graph, then fix the tag |
X_CACHE_TOO_LARGE |
one entry exceeds the tier's byte budget | caching a whole row set | raise cache.<tier>.maxBytes, or cache a projection |
X_CACHE_DRIVER_UNAVAILABLE |
a tier's backing store is missing | no Redis binding, or a purge driver built without FASTLY_API_TOKEN / CLOUDFLARE_API_TOKEN
|
provision the tier, or drop it from app.config.ts
|
X_CACHE_PURGE_FAILED |
the CDN refused a purge | a wrong or unscoped API token, a zone without tag purge, a throttle, a key carrying whitespace or a comma |
meta.retryable === true → the identical purge can land again; false → set the env key the fix names, then x dev
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_STORAGE_CHECKSUM_MISMATCH |
bytes do not match the declared checksum | a truncated upload, or a sha256 computed over different bytes than the ones sent | recompute the checksum over the exact bytes you send, or omit it and let the driver hash |
X_STORAGE_DISK_UNKNOWN |
no disk with that name is configured |
disk('<name>') for a disk storage.disks never declared |
add "<name>" to storage.disks in app.config.ts, or call one of the names cause lists |
X_STORAGE_NOT_FOUND |
no object at that key | a stale key, the wrong disk, or an object already deleted | list the prefix and compare: await disk('<disk>').list({ prefix: '<dir>' })
|
X_STORAGE_PATH_UNSAFE |
object key escapes its prefix | an absolute key, a .. segment, or a backslash |
build the key with scopedKey(orgId, ...parts) — relative, forward slashes, no ..
|
X_STORAGE_TOO_LARGE |
payload exceeds the upload size limit | a file over the policy's maxBytes; cause carries both numbers |
uploadPolicy({ maxBytes: <bytes> }) for a higher limit, or compress the file first |
X_STORAGE_TYPE_REJECTED |
content type is not allowed for this upload | a type absent from allowedContentTypes, or magic bytes that disagree with the declared Content-Type — the bytes win |
add the declared type to allowedContentTypes in the upload policy, or re-upload with the Content-Type cause sniffed |
X_STORAGE_URL_INVALID |
signed URL does not match its signature | an edited constraint (x-max, x-ct, the key), a URL outside the mounted base, a PUT grant replayed as a GET, or a Content-Type header the signature does not cover |
mint a fresh one with grantUpload({ disk, orgId, request }) and send it unedited — every constraint is inside the signature |
X_STORAGE_URL_EXPIRED |
signed URL is past its expiry | a form left open longer than the grant's window | call grantUpload({ disk, orgId, request, expiresInMs }) again — a longer expiresInMs widens the window the signature grants |
X_STORAGE_ORG_MISMATCH |
object key belongs to another org | a key from one tenant used while the actor resolves to another; the signature can be perfectly valid and this still fires | build it with scopedKey('<orgId>', ...parts), and pass the actor's org as orgId — never one read off the request |
X_STORAGE_UPLOAD_FAILED |
the signed upload was refused | the presigned PUT answered non-2xx, or never reached the disk (status: 0) |
call uploadFile({ file, grant }) again for a fresh grant; a 4xx here is the constraint named in the body, not a transport fault |
X_FLAG_EXPIRED is the only code in the framework that is reported and never thrown: an overdue temporary flag is a debt, not an outage, so isEnabled() hands it to the configured ErrorReporter as a warning (rate-limited to one report per key per hour) and still answers its targeting. Everything else here fails at declaration time.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_FLAG_DUPLICATE |
two flags were declared with the same key | a copy-pasted defineFlag(), so one of the two declarations decides nothing |
rename one of the two defineFlag({ key: '<key>' }) declarations |
X_FLAG_EXPIRED |
a temporary flag is past its expiry and is still being evaluated | scaffolding that outlived the change it was wrapping; cause names the owner and how overdue it is |
delete the branch and its defineFlag() declaration, or move it to kind: 'permanent' if it is a real product switch |
X_FLAG_EXPIRY_INVALID |
a temporary flag has no usable expiry date |
expiresAt absent or unparseable — reachable from a store snapshot or plain JS; in TypeScript the union already refuses it |
set expiresAt to an ISO-8601 date such as '2026-12-01' in defineFlag({ key: '<key>' })
|
X_FLAG_TARGETING_INVALID |
flag targeting is out of range or malformed |
rollout: 0.5 (a fraction read as a percentage means nobody), a rollout outside 0-100, or default: true beside a rollout |
set rollout to a whole percentage 0-100 in defineFlag({ key: '<key>' }), and leave default false when a rollout decides |
X_FLAG_UNKNOWN |
no flag is declared under this key | a typo at the isEnabled() call site — answered as an error rather than a silent false, which would be a branch that never runs and never says so |
declare it with defineFlag({ key: '<key>', … }), or correct the key at the call site |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ROUTE_MODE_INVALID |
render mode not allowed on this surface |
stream on a site/ route |
use static, isr or ssr in site/; stream, spa or ssr in app/
|
X_ROUTE_OFFLINE_MISSING |
the route's offline strategy is missing or contradictory |
precache on an ssr route |
set a compatible offline, or change the render mode |
X_ROUTE_META_MISSING |
required metadata missing | no meta.title, or no description on a site/ route |
add it to meta in the route file |
X_ROUTE_UNNORMALIZED |
a route was registered without defineRoute
|
registerRoute({ config }) was handed the author's own object, so meta and budget were never normalized and every descriptor reader would read them wrong |
wrap it: registerRoute({ file, config: defineRoute({ … }) })
|
X_ROUTE_DUPLICATE |
two route files resolve to one URL | a copied page directory | delete or rename one |
X_ROUTE_FILE_INVALID |
a route file is not named for its surface |
site/pricing.tsx or site/blog/index.tsx instead of <dir>/page.tsx
|
mkdir -p <dir> && git mv <file> <dir>/page.tsx — route.ts under api/
|
X_STORAGE_UNWRITABLE |
the storage disk this process needs cannot be written to | a hardened container (readOnlyRootFilesystem: true) with no writable volume at the embedded disk's root, or S3_ENDPOINT set with S3_BUCKET empty |
mount a writable volume at the path in the message, or set S3_ENDPOINT + S3_BUCKET to use object storage. x doctor --json shows which binding this process resolved |
X_ROUTE_LOAD_INVALID |
a route declared a load that is not a function |
load: someData instead of load: () => someData
|
make load a function of ({ params, url }) returning the page data, or remove it |
X_ROUTE_LOAD_FAILED |
a route's load threw while resolving its data |
the query, client call or fetch inside load failed; the message names the path |
fix the load for that path, or return a fallback so the page can still render. An UltimateError thrown by the loader passes through untouched, with its own code |
X_SURFACE_BOUNDARY |
a surface imported across the hard boundary |
site/ reached app/, transitively |
x fix boundary <file>, or move the shared module out of shared/ui
|
X_BOUNDARY_SITE_TO_APP |
site/ imported app/
|
the classic transitive import | the chain is printed in cause; break it at the named hop |
X_BOUNDARY_APP_TO_API |
app/ imported api/ at runtime |
a value import instead of import type
|
use import type, and call the typed client |
X_BOUNDARY_ROUTE_TO_DB |
a route touched the database | SQL in a page file | move it into repo.ts and call a query |
X_BOUNDARY_SERVICE_TO_HTTP |
a service imported HTTP | request awareness inside business logic | take the values as arguments so a job can reuse the service |
X_BOUNDARY_SHARED_LEAF |
shared/ imported a surface |
shared/ is a leaf |
invert the dependency |
X_ADMIN_FLATTENER_VIOLATION |
a production @ultimat3/admin file read $meta or called $describe() directly |
a new admin module reached past entity-columns.ts for column facts |
take AdminColumnFacts from entity-columns.ts instead |
X_BUDGET_EXCEEDED |
a route blew its JS or LCP budget | a heavy transitive import |
x routes --json for the chain; move it behind hydrate: 'interaction' or raise the budget deliberately |
X_BUDGET_UNMEASURED |
a route declares a JS or LCP budget the build never measured | the route is absent from .x/build-stats.json
|
x build, then x verify
|
X_PRERENDER_FAILED |
a prerendered path threw during build |
prerender() returned an id that no longer resolves |
fix the data source, or narrow prerender()
|
X_STYLES_GLOBAL_MISSING |
a surface renders documents whose CSS defines no :root custom properties |
nothing in the app's module graph imports the global layer, so every var(--color-*) and var(--space-*) a component emits resolves to nothing and the browser drops the declaration |
add apps/web/shared/global.scss containing @use '@ultimat3/ui/global.scss'; and apps/web/shared/global.ts containing import './global.scss'; — x new scaffolds both |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_SEO_META_MISSING |
a site/ route is missing required metadata |
no title, description or og.image
|
add it to meta; the file is named in cause
|
X_SEO_META_TOO_LONG |
title or description exceeds what results render | a description over 160 chars | shorten it — cause carries the measured length |
X_SEO_DUPLICATE_META |
two routes share a title or description | copied metadata | make each page's meta unique |
X_SEO_CANONICAL_MISMATCH |
canonical URL does not match the route path | a hand-written canonical | delete it — canonicals come from the route table |
X_LD_INVALID |
JSON-LD node is missing a required schema.org field | an ld.* helper called with a partial object |
supply the field named in cause
|
X_SEO_BUDGET_EXCEEDED |
route exceeded its performance budget | a js/css/lcp/cls/inp budget broken in the SEO report |
x routes --json for the route's budget, then cut the regression cause names |
X_SITEMAP_TOO_LARGE |
sitemap exceeds the 50,000-entry limit | too many prerendered URLs in one file | enable sitemap index splitting in app.config.ts
|
X_IMAGE_QUERY_INVALID |
a minted image URL's ?w=/?q= value is present but unusable |
?w=0, ?w=-5, ?q=150 on a URL responsiveImage() minted |
request a positive integer, e.g. ?w=640 (quality is 1-100, e.g. ?q=75) |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_PWA_NO_OFFLINE_FALLBACK |
pwa.offline.fallback is not set |
the required field was removed | set pwa: { offline: { fallback: '/offline' } }
|
X_PWA_ICON_MISSING |
no source icon to generate from | the configured icon path does not exist | add an SVG or >=1024px PNG and point pwa.icon at it |
X_PWA_MANIFEST_INVALID |
the generated web manifest failed validation | a bad start_url or scope
|
fix the pwa block; cause names the field |
X_SW_SCOPE_INVALID |
the service-worker scope cannot serve the routes it precaches | a scope narrower than the app | serve sw.js from the app root |
X_BUILD_ID_MISSING |
no immutable build ID | a build produced outside x build
|
build with x build; never use a timestamp or latest
|
X_PWA_STRATEGY_EXHAUSTED |
a caching strategy had no cache, no network and no fallback |
staleWhileRevalidate with no StrategyOptions.fallback and the network failed |
pass fallback to the strategy, or set pwa.offline.fallback
|
X_PWA_SYNC_FLUSH_FAILED |
the background-sync outbox flush was rejected |
POST /_x/outbox/flush returned a non-2xx |
confirm @ultimat3/realtime mounts the flush endpoint and it returns 2xx on success |
X_PWA_SYNC_INCOMPLETE |
the background-sync outbox flush left mutations queued | the flush endpoint reported remaining > 0
|
check the realtime outbox worker is draining, or raise the sync retry ceiling |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_LOCALE_UNSUPPORTED |
locale is not in the supported set | a URL or header requesting an unconfigured locale | x i18n add <locale> |
X_CATALOG_MISSING_KEYS |
a catalog is missing keys used in source | a new t() key with no translation |
x i18n sync <locale> |
X_CATALOG_INVALID |
a catalog entry is malformed | bad interpolation or a non-string value | x i18n check --json |
X_CURRENCY_UNKNOWN |
currency code not in the currency table | a typo, or a currency the table lacks | x money add-currency <ISO> --exponent <n> |
X_CURRENCY_MISMATCH |
two Money values in different currencies |
adding EUR to USD |
convert(value, '<target>', rate) first |
X_MONEY_NOT_INTEGER |
a Money.minor that is not an integer |
a float leaked in |
fromDecimal('12.99', 'USD'), or pass an explicit rounding mode |
X_ALLOCATION_INVALID |
split ratios or part count are unusable | zero parts, or all-zero ratios | pass a positive part count, or finite non-negative ratios |
X_RATE_MISSING |
no FX rate for the pair | no RateProvider registered |
register one — there is no default, because a wrong rate is worse than a missing one |
X_TIMEZONE_INVALID |
not an IANA zone |
CET, +02:00, or a typo |
use Europe/Berlin, America/New_York, UTC
|
X_INSTANT_INVALID |
not a parseable instant | a date string with no offset | pass ISO-8601 with Z or an offset |
X_DURATION_INVALID |
not a parseable duration | '3 days' |
use '3d', '2h30m', '250ms', or ISO-8601 PT2H30M
|
X_CRON_INVALID |
not a parseable cron expression | wrong field count | 5 fields (m h dom mon dow) or 6 with seconds |
X_SCHEDULE_INVALID |
a wall-clock field is out of range |
hour: 24 or minute: -1 in a schedule spec — cause names the field and its range |
pass an integer inside the range cause prints; wall-clock fields are never wrapped or clamped, because a silently shifted schedule is worse than a failed one |
X_DST_AMBIGUOUS |
the local time occurs twice | a fall-back overlap | pass { overlap: 'first' } or { overlap: 'second' }
|
X_DST_NONEXISTENT |
the local time does not exist | a spring-forward gap | pass { gap: 'next' } or { gap: 'previous' }
|
X_LOCALE_INVALID |
not a well-formed BCP 47 tag |
en_US, '', or a raw Accept-Language value reaching describeCron
|
pass en, en-GB, de-DE — screen header input with Intl.DateTimeFormat.supportedLocalesOf([tag])
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_MAIL_LOCALE_MISSING |
send() was called without a locale |
a JS caller, or a cast that dropped the required field | send(mail, data, { to, locale: ctx.locale }) |
X_MAIL_TEMPLATE_UNKNOWN |
no mail is registered under that id | the module holding defineMail({ id }) was never imported (also raised for an unregistered layout) |
export the defineMail and import it at boot |
X_MAIL_DUPLICATE |
two mails claim the same id | a copy-pasted defineMail
|
rename one of the two declarations |
X_MAIL_TEXT_MISSING |
the rendered mail has no plain-text part | a template of images and buttons only | add a text-bearing block: blocks.paragraph('mail.<id>.body')
|
X_MAIL_DRIVER_UNAVAILABLE |
no mail driver is configured |
setMailDriver was never called |
setMailDriver(createMemoryDriver()) in dev, createSmtpDriver({ url: env.SMTP_URL }) live |
X_MAIL_HEADER_INVALID |
a header value carries a line break | interpolated data with a CR/LF reached Subject — header injection |
reject the value at its own boundary — t.string().pattern(/^[^\r\n]*$/) on the field that feeds the header — then re-send. Never strip the break: a silently rewritten header hides the injection attempt |
X_MAIL_SEND_FAILED |
the mail transport refused the message | a rejected recipient, bad credentials, a throttle, a dead socket, or a peer that never completes a reply |
meta.retryable === true → requeue with sendMailJob; false → fix what meta.stage names, then x doctor --json. meta.stage is the SendStage union in packages/mail/src/errors.ts: auth → SMTP_URL credentials, tls → openssl s_client -connect <host>:465 for the implicit-TLS certificate, starttls → openssl s_client -starttls smtp -connect <host>:587, recipient → the address, reply → point SMTP_URL at the SMTP port itself, since a proxy or an HTTP port answers like this |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_MCP_TOOL_UNKNOWN |
no such tool for this caller | a stale tool name, or one this caller may not see | call tools/list to read the catalog |
X_MCP_ARGS_INVALID |
tool arguments failed the input schema | guessed arguments | re-read inputSchema from tools/list and resend |
X_MCP_SCOPE_DENIED |
the connection's token does not carry the tool's scope | a read token calling a write tool |
x token grant <scope>, then reconnect — scopes are fixed for the life of a connection |
X_MCP_QUERY_REJECTED |
db.query was not given one read-only statement |
empty input, a batch, a non-read leading keyword, a mutating keyword anywhere at statement level (including a data-modifying CTE), a locking clause (FOR UPDATE/FOR SHARE), EXPLAIN ANALYZE, or a call from a banned function family matched by prefix (pg_read_*, pg_ls_*, lo_*, dblink, pg_advisory_*, pg_sleep*, set_config, …) |
send exactly one read-only SELECT/WITH/EXPLAIN/SHOW/TABLE/VALUES statement. db.query has no write path at all: change data by calling an action exposed with mcp: { expose: true }, and change schema with db.migrate after x db branch <name>
|
X_MCP_NOT_BRANCH_DB |
db.migrate was aimed at a database that is not a branch |
a production-tagged target, or any shared/long-lived database |
x db branch <name>, then retry db.migrate; production migrates through ROLE=migrate in the deploy hook, never through MCP |
X_MCP_PROTOCOL |
the MCP handshake or auth is wrong | missing bearer token, or a user-shaped actor | send Authorization: Bearer <token>; resolve MCP callers as agents |
X_MCP_TOOL_UNDECLARED |
defineAppMcp lists a primitive that declares no MCP exposure |
an action or query named in actions:/queries: with no mcp block, or mcp: { expose: false }
|
add mcp: { expose: true, description } beside the primitive's policy — or drop it from the list and let include: 'exposed' project what opted in |
X_MCP_TOOL_UNSAFE |
an MCP tool declares no policy | a hand-written tools: entry written without policy
|
add policy: '<resource>:<verb>', reusing the permission its action uses |
X_MCP_TOOL_DUPLICATE |
two primitives project to one MCP tool name | a tools: record key colliding with an exposed action, or an action and a query sharing a name |
rename one — the tool name is the primitive's export name, or the tools record key |
X_MCP_SCOPE_UNKNOWN |
defineAppMcp scopes a tool this server does not project | a scopes entry in defineAppMcp names a tool by a name absent from the projected catalog — a typo, or the primitive was renamed, or dropped from actions:/queries:/tools:
|
spell it as one of the projected tool names cause lists, or drop it from that scopes entry |
X_MCP_SCOPE_CONFLICT |
two scopes claim one MCP tool | one tool name listed under two different scopes entries, or under one entry while the tool already carries a different scope of its own |
keep the tool under the single scope a token must hold for it, and remove the other entry |
X_AI_BUDGET_EXCEEDED |
a model call would exceed its budget | prompt too large, or cost cap reached | raise ai.budget for the scope, or shorten the prompt |
X_AI_GATEWAY_MISSING |
an llm() action ran with no gateway installed |
boot never called configureAi
|
configureAi({ gateway: createGateway({ providers: [new AnthropicProvider()] }) }) at boot |
X_AI_PROMPT_VERSION |
prompt version or slots are wrong | an edited prompt with no version bump, or a missing variable | bump the version in definePrompt, then x manifest
|
X_AI_PROVIDER_UNAVAILABLE |
the model provider is unreachable | a non-2xx, an in-band error event, or a stream cut before message_stop
|
retry a 429/5xx (the gateway already does); fix the request a 4xx names |
X_AI_KEY_MISSING |
the provider API key is not set |
ANTHROPIC_API_KEY / EMBEDDINGS_API_KEY unset and no apiKey passed |
export ANTHROPIC_API_KEY=<key>, or pass { apiKey } to the provider |
X_AI_REQUEST_INVALID |
the provider would reject this request | a reasoning control the chosen model does not have — effort or adaptive thinking on a pre-4.6 model, or thinking: 'disabled' above a model's effort cap |
set model: on the llm() declaration to one whose MODELS row has the control, or drop effort/thinking from definePrompt — editing the template alone leaves the control in the request |
X_LLM_OUTPUT_INVALID |
structured output failed its schema on the answer and on the repair turn | the model would not produce the shape | describe the shape in the prompt template and bump its version, or widen output in the llm() declaration |
X_LLM_REFUSED |
the model declined the request | the provider's safety classifiers refused; stopDetails.category names why |
set model: on the llm() declaration to another blessed model (the thrown fix: names one), or edit the template in definePrompt and bump its version |
X_LLM_TRUNCATED |
the answer hit its maxTokens ceiling before it was complete |
the ceiling is below what the output schema asks for | set maxTokens: on the llm() declaration to double the ceiling the fix: names, or drop fields from its output schema |
X_EVAL_THRESHOLD |
an eval scored below its tolerance | a prompt edit regressed cases against the recorded baseline |
x test <eval> for per-case scores; ULTIMATE_EVAL_RECORD=1 x test eval to accept new numbers as a reviewed diff |
X_EVAL_BASELINE_MISSING |
an eval has no recorded baseline to gate against | a new eval, or a baseline: that is not import.meta.resolve('./…')
|
ULTIMATE_EVAL_RECORD=1 x test eval, then commit the baseline file |
X_EVAL_BASELINE_INVALID |
a recorded baseline cannot be read | a hand-edited or half-merged baseline file |
ULTIMATE_EVAL_RECORD=1 x test eval to re-record it |
X_EVAL_MISSING |
a prompt has no eval | a definePrompt with no defineEval naming it |
add defineEval({ prompt, cases, scorers, tolerance, baseline }) beside the prompt |
X_EVAL_RECORDING |
the gate ran with baseline recording switched on |
ULTIMATE_EVAL_RECORD was exported in the shell, or set on the CI job, that ran x verify
|
env -u ULTIMATE_EVAL_RECORD x verify — record with ULTIMATE_EVAL_RECORD=1 x test eval instead |
X_VECTOR_DIM_MISMATCH |
embedding dimensions differ from the store | the embedder model changed | use the original embedder, or x ai reindex
|
X_VECTOR_SCOPE_WIDENED |
a derived vector scope tried to leave its tenant | a handler re-scoped the store it was handed | derive from the unscoped store: vectorStore.scoped({ tenant })
|
X_AI_EMBEDDER_INVALID |
an Embedder returned fewer vectors than texts it was given |
embedOne got an empty batch back from embed([text])
|
return one vector per input text from embed(), in the order the texts arrived |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ADMIN_ENTITY_UNKNOWN |
the admin references an entity that does not exist | a renamed entity |
x g entity <name>, then x manifest
|
X_ADMIN_POLICY_MISSING |
an admin-exposed subject has no policy | a screen added without authz | add policy: can('<subject>') to the definition |
X_ADMIN_FIELD_UNSUPPORTED |
a column type the admin cannot render | an exotic Postgres type | supply a custom field renderer, or hide the column |
X_ADMIN_DENIED |
the actor may not use this admin surface | missing admin:* permission |
grant the permission through the normal policy layer |
X_ADMIN_TOOL_FORBIDDEN |
an admin MCP tool was called without permission | agent acting beyond its user | nothing to fix — the policy is correct |
X_ADMIN_INVALID |
an admin tool's arguments failed the resource schema | the agent built a row from a stale tool schema |
x manifest, then re-read the tool's JSON Schema from tools/list
|
X_DEV_DASHBOARD_IN_PROD |
/_x was mounted outside dev |
the dev dashboard shipped in the image | delete the /_x mount from the production entrypoint |
X_MANIFEST_DRIFT |
a committed manifest differs from the code | a primitive changed without regenerating, or the file was hand-edited so its buildId no longer hashes its own contents |
x manifest — bun run manifest for this repo's own framework.manifest.json
|
X_MANIFEST_STALE |
openapi.json is stale |
the committed spec does not match the actions the code registers |
x manifest, then commit |
X_MANIFEST_BREAKING |
a published contract was removed or narrowed | a breaking change with no version bump | bump the major version, or restore the contract |
X_AGENTS_MD_MISSING |
no AGENTS.md
|
the human-authored file was deleted | write it by hand — short: stack, commands, conventions |
X_AGENTS_MD_TOO_LARGE |
AGENTS.md grew past its cap |
generated facts pasted into it | move facts into x.manifest.json; keep conventions only |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_TEST_DB_UNAVAILABLE |
no Postgres for the test template | nothing listening | run x dev (embedded Postgres), or set TEST_DATABASE_URL
|
X_TEST_FIXTURE_UNAVAILABLE |
a declared fixture has no driver in this process | destructuring page, budget, signIn, deploy or subscribe with no browser or replicator installed |
defineFixtures({ <name>: () => yourDriver() }) in the test preload — cause names what the fixture needs |
X_TEST_FIXTURE_UNKNOWN |
a test requested a fixture nobody registered | a destructured fixture name no defineFixtures call declares |
defineFixtures({ <name>: () => buildIt() }) at test setup — cause lists the registered names |
X_TEST_NETWORK_OFFLINE |
the test network is offline | a request made after network.offline() or network.drop()
|
network.online() before the call — or assert the offline path instead of the request |
X_TEST_NETWORK_SEALED |
a test tried to reach the network | an unmocked external call |
mockFetch('<url>', …), or allowHost('<host>') if it must be real |
X_TEST_NONDETERMINISTIC |
a test read wall-clock time or unseeded randomness |
Date.now() in the code under test |
wrap in frozenClock() / seededRandom(), or remove the read |
X_TEST_EVAL_THRESHOLD |
an evalTest() score fell below its threshold |
a prompt or output regressed against options.threshold
|
improve the prompt under test, or lower the threshold passed to evalTest()
|
X_TEST_SCHEMA_EXPECTED |
a matcher expected a Standard Schema and got something else |
toRejectInput/toAcceptInput called on an action instead of action.input
|
call toRejectInput(action.input) — the schema, not the action or the query |
X_TEST_JOB_EXPECTED |
a matcher expected a job declaration and got something else |
toEmitSteps/recordSteps called on job.run or an unrelated value |
call toEmitSteps(myJob) with the job export, not toEmitSteps(myJob.run)
|
X_TEST_NETWORK_RACE |
a request raced unsealNetwork() and lost the patched fetch |
unsealNetwork() called while a request from the same seal was still in flight |
do not call unsealNetwork() while a request from the same test is still pending |
X_TEST_FACTORY_TRAIT_UNKNOWN |
a factory was asked for a trait it does not declare | a typo at the call site, or a trait never added — the same symptom, so cause lists every declared trait to tell them apart |
declare it: defineFactory(<table>, { traits: { <trait>: { /* columns */ } } }) |
X_TEST_FACTORY_NOT_PERSISTED |
create() ran with no persister installed |
nothing was attempted — distinct from an insert that failed |
usePersister({ insert: (table, row) => repoFor(table).insert(row) }) in the test preload — or build() for an in-memory row that needs no database |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_TOKEN_UNKNOWN |
design token role does not exist | a typo, or a raw colour that was never a token | use a role from @ultimat3/ui tokens; see Theming
|
X_THEME_INVALID |
theme is not light or dark
|
a bad data-theme value |
set light or dark, or clear the attribute to follow the OS |
X_UI_RUNTIME_MISSING |
a host capability the component needs is absent |
IntersectionObserver or localStorage unavailable |
render the no-JS fallback path |
X_UI_INVALID_VALUE |
a formatting component received an unrenderable value |
NaN money, or an invalid date |
fix the value upstream; formatting never guesses |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_CLI_UNKNOWN_COMMAND |
not a command | a typo |
x help — the suggestion is in fix
|
X_CLI_BAD_FLAG |
flag rejected | unknown flag, or a bad value | x <command> --help |
X_CLI_UNEXPECTED |
the CLI itself failed | a bug, or a broken environment |
x doctor --json and attach it to an issue |
X_VERIFY_FAILED |
one or more verify steps failed | the gate is red |
x verify --json — every step's findings arrive in one run |
X_TYPECHECK_FAILED |
tsc failed |
a type error anywhere in the workspace | bunx tsc -b --pretty false |
X_LINT_FAILED |
Biome failed |
any, a default export, a bare Error, a raw hex colour |
bunx biome check --write . |
X_TEST_FAILED |
a test type failed | a red test | the fix is the exact bun test … invocation the step ran |
X_TEST_NO_FILES |
the test selection matched no files | a type or --filter that matches nothing, or x test run outside the app |
drop the filter (x test), or point it at the right root: x test --cwd <repo root>. A green run over zero files is the most expensive false pass |
X_TEST_SHARD_FAILED |
a test shard exited non-zero | a red test inside one worker of a sharded run | the fix replays that shard alone, carrying the whole selection that produced it: x test <type> [--filter <text>] [--sample <n>] --workers <n> --worker <i>. Dropping --sample would reshard a different corpus, so the fix keeps it |
X_VERIFY_SUITE_VANISHED |
a step the committed floor requires had nothing left to check |
x.verify.json names a step this repo already ran, and its suite was deleted — so the step reported as skipped and the gate would have stayed green |
restore the suite, or delete that name from x.verify.json in the commit that says why — then x verify --json
|
X_FILE_TOO_LONG |
a source file is over 500 lines | one file doing several jobs | split it; the fix names the file |
X_PACKAGE_SHAPE |
a workspace package is missing a contract file | a package added by hand | bun run scripts/new-package.ts <pkg> --only <file> |
X_ERROR_FIX_INVALID |
an error's fix line is not a runnable instruction | the errors step read a fix: that is empty, or one that advises (check, make sure, try, see the docs) with no command, call or file path in it |
the fix names the offending <file>:<line> — rewrite that fix: as a runnable command, a call, or an edit naming a file |
X_ERROR_CODE_UNDOCUMENTED |
a shipped error code has no row in the error reference | a package declared an X_* code and the same pull request never added its row |
add a row for the code to wiki/Error-Codes.md — this page |
X_ERROR_CODE_UNREGISTERED |
the error reference documents a code no package registers | a row written for a code that was renamed, never built, or emitted as a Finding without a registerErrorCodes() call |
register it in the owning package's src/errors.ts, or move its row under Reserved codes
|
X_APP_PACKAGE_INVALID |
the app's package.json has no usable name/version — the manifest never fabricates app@0.0.0, because its version is the compatibility gate |
a malformed or hand-trimmed package.json
|
bun pm pkg set name=<app> version=0.1.0 |
X_BUILD_FAILED |
x build failed |
a static check or the bundler | read cause; the failing step is named |
X_BUILD_ENTRY_MISSING |
the build target's entry file is not in the app | an app scaffolded before x new wrote apps/web/server.ts and apps/web/prerender.ts, or one whose docker/Dockerfile was deleted — refused before the builder is spawned, because bun build's own "module not found" names no owner |
the fix names the file; x new <name> --dry-run --json lists every entry a build needs |
X_DEPLOY_FAILED |
a deploy step failed | the compose/helm command exited non-zero | run the printed command directly for full output |
X_SETUP_INSTALL_FAILED |
bun install failed during bin/setup
|
a conflicted lockfile, or a half-written node_modules
|
rm -rf node_modules bun.lock && bun install |
X_RELEASE_VERSION_SKEW |
a workspace is not at the lockstep version | a package bumped on its own, or a release that stopped half-way |
bun run scripts/release.ts --bump patch --dry-run --json to see the realignment, then run it without --dry-run and review the package.json diff |
X_TRUST_PUBLISHER_MISSING |
a published package has no OIDC trusted publisher | a package added after the trust rollout, or a config revoked on npmjs.com — release day then falls back to token auth, which this repo has no token for | bun run scripts/trust-publishers.ts |
X_TRUST_PUBLISHER_FAILED |
attaching a trusted publisher failed | npm rejected the call for a reason other than auth — most often E409, a stale config from an earlier attempt that must be revoked before it can be replaced |
the fix names the package; revoke the old one (npm trust list <pkg> --json, then npm trust revoke <pkg> --id <id>) and re-run bun run scripts/trust-publishers.ts
|
X_TRUST_2FA_REQUIRED |
npm refused the trust change on the credential's class | a granular access token that bypasses two-factor auth — attaching a publisher is an account-level trust change, so npm requires a session that carries 2FA, and no OTP flag rescues a bypass token |
npm login, then bun run scripts/trust-publishers.ts
|
X_TRUST_NPM_TOO_OLD |
the npm on PATH has no trust subcommand |
npm trust shipped in npm 12; Node 22 and Bun both bundle older |
npm install -g npm@12 |
X_GENERATE_CONFLICT |
a generator would overwrite a file | the name is taken |
x g … --force, or choose another name |
X_GENERATE_JSON_INVALID |
a generator's own merge: 'json' output does not parse as a JSON object |
a bug in the template that produced it — never a file already on disk, which X_GENERATE_CONFLICT covers |
fix the template that emits it, then bun test packages/cli/src/cmd-generate.test.ts
|
X_SCAFFOLD_PATH_ESCAPE |
a generated path resolves outside the scaffold sandbox | a .. segment or an absolute path in a template's GeneratedFile.path
|
make the path relative to the app root with no .., then bun test packages/cli/src/scaffold-typecheck.contract.test.ts
|
X_NOT_IMPLEMENTED |
a planned command, or a driver whose remote half is unwritten | one of the commands in CLI reference's planned table | the fix names the closest shipped command — never "not a command", which would send you looking for a typo |
X_ERROR_CODE_UNKNOWN |
no package registered this error code | a typo, or a code from a package this process could not import |
x errors list --json — the nearest real code is in fix, and data.unavailable names any package that would not load |
X_DECLARATION_UNKNOWN |
no declaration with this name is registered | a typo, or a module that never imported |
x actions list --json (or queries / entities); the nearest real name is in fix
|
X_JOB_UNKNOWN |
the queue holds no job with this id | a stale id, or a job already reaped | x jobs ls --json |
X_FIX_TARGET_UNKNOWN |
the named file is not one of the app's source files | a path outside apps/*/{site,app,api,shared}, or a typo |
x fix boundary <nearest real path> — fix carries it |
X_ROADMAP_FILE_MISSING |
docs/idea/14-roadmap.md does not exist, so no status or artifact can be checked |
the roadmap was deleted or moved — every other roadmap rule would otherwise pass silently | git checkout -- docs/idea/14-roadmap.md |
X_ROADMAP_STATUS_MISSING |
a milestone has no row, or its row's status cell holds neither ✅ nor 🚧 |
docs/idea/14-roadmap.md edited without keeping the marker |
put ✅ or 🚧 in the second cell of the row fix names, then bun run scripts/roadmap.ts --json
|
X_ROADMAP_MILESTONE_UNVERIFIED |
a milestone the table marks ✅ is missing a package or file its own Ships column names | the artifact was deleted or renamed after the milestone was marked shipped |
git checkout -- <the paths in fix>, or put 🚧 in that row's status cell |
X_REFERENCE_APP_REGRESSED |
a step of examples/dummy's own gate that was passing now fails — or the gate produced no step table at all |
a framework change broke the reference app, and the step is not one of the pinned entries in EXPECTED_RED
|
cd examples/dummy && bun run ../../packages/cli/src/bin.ts verify |
X_REFERENCE_APP_PIN_STALE |
a step pinned as failing in EXPECTED_RED now passes |
the app was repaired and the pin was not lowered — the ratchet only shrinks | delete the named entries from EXPECTED_RED in scripts/reference-app-gate.ts
|
X_REFERENCE_APP_UNREFERENCED |
examples/dummy typechecks but the root tsconfig.json does not reference it |
the app came off the typecheck pin without joining the root tsc -b solution, so the packages' emitted .d.ts are never proved consumable |
add { "path": "./examples/dummy" } to the references array in tsconfig.json
|
Everything above this heading is live: x errors explain <CODE> answers for it today, and the errors step fails the build if it does not (X_ERROR_CODE_UNREGISTERED). Everything below is documented for a different reason and is deliberately outside that rule.
Reserved, not registered. The name is spoken for — the design docs use it and no future code may reuse it — but nothing raises it in this build, so x errors explain refuses it. The right-hand column is what actually happens today.
| Reserved code | What happens today |
|---|---|
X_TIMEOUT |
no deadline is enforced per request. A cancelled request is X_ABORTED; a job past its deadline is X_JOB_TIMEOUT. @ultimat3/http already maps the code to 504 for the build that raises it |
X_MIGRATE_CONCURRENT |
ROLE=migrate takes no advisory lock, so two overlapping deploys both migrate. Serialise them in the deploy pipeline until this ships — roadmap milestone 11 |
X_SW_HAND_EDITED |
sw.js carries no checksum, so a hand edit survives x build and is silently overwritten on the next one |
X_SW_UNCACHEABLE |
an offline strategy contradicting the route's render mode is accepted; X_SW_SCOPE_INVALID covers only the scope half |
X_CACHE_UNTAGGED_QUERY |
a query no tag covers is cached and never invalidated. X_CACHE_TAG_UNKNOWN catches the opposite mistake — a tag no entity declared |
Some design docs predate the implementation. As of 2026-08 these are the mappings; the right-hand column is what the framework actually throws. The old name stays here because a code never changes meaning — a renamed concept gets a new code and the old one keeps its row.
| Design doc name | Implemented code |
|---|---|
X_JOB_NO_IDEMPOTENCY_KEY |
X_IDEMPOTENCY_REQUIRED |
X_JOB_DUPLICATE_STEP |
X_STEP_DUPLICATE |
X_JOB_STEP_FAILED |
X_JOB_MAX_ATTEMPTS (retries exhausted) or X_JOB_TIMEOUT
|
X_SEO_NO_TITLE / X_SEO_NO_DESCRIPTION
|
X_SEO_META_MISSING |
X_BUDGET_EXCEEDED thrown by @ultimat3/seo
|
X_SEO_BUDGET_EXCEEDED — X_BUDGET_EXCEEDED is @ultimat3/render's, and one code is owned by exactly one package |
X_POLICY_DENIED |
X_FORBIDDEN, wherever in the stack the policy decided |
X_PWA_NO_ICON_SOURCE / X_PWA_NO_FALLBACK
|
X_PWA_ICON_MISSING / X_PWA_NO_OFFLINE_FALLBACK — x doctor reports the package's own codes, never a CLI twin of them |
X_BOUNDARY_VIOLATION |
X_BOUNDARY_SITE_TO_APP and the other X_BOUNDARY_* codes. The bare name is still this repo's own tier-table finding, above |
X_TEST_NETWORK_EGRESS |
X_TEST_NETWORK_SEALED |
X_LIVE_QUERY_LIMIT |
X_SUBSCRIPTION_LIMIT |
X_ENV_INVALID |
X_ENV_MISSING |
New codes are added in the owning package's src/errors.ts and registered through registerErrorCodes(); add the row here in the same pull request, or the errors step of x verify fails the build with X_ERROR_CODE_UNDOCUMENTED — see Contributing.
Ultimate — v1.1.0 As of 2026-08. Stable API, semver from here. MIT licensed.
Repository · Issues · Changelog · llms.txt
Edits to these pages are synced from wiki/ in the repository — change the file there, not the wiki, or the next sync overwrites it.
Start
Tutorials
- 1 · First app
- 2 · First feature
- 3 · Auth and admin
- 4 · Jobs and realtime
- 5 · Deploy free
- 6 · Growing up
Primitives
- The eight primitives
- Actions
- Entities and migrations
- Policies and authz
- Queries and live queries
- Jobs and workflows
- Scheduled tasks
- Routes and render modes
Capabilities
Cross-cutting
Reference