Skip to content

Troubleshooting

Symptoms, in the order you are likely to meet them, with the cause and the fix. Where a failure carries a TQL-* code, the error code reference has the raising site; this page is the other direction — you have a symptom and no code.

The CLI is not on your PATH. Check the install method you used (getting-started.md) and verify with tesseraql --version. With the distribution archive, it is the unpacked bin/ directory that goes on PATH, not the jar.

The CLI prints this instead of a stack trace when it cannot reach the database at all. Three things to check, in order:

  1. Is the database running? docker compose up -d starts the one the scaffold ships.
  2. Does tesseraql.datasources.main.jdbcUrl point at it? A --jdbc-url argument overrides both the config and any running embedded database.
  3. Are you expecting an embedded database? A tesseraql dev --embedded-db in another terminal leaves a work/embedded-db.jdbc marker, and the database commands pick it up — but only when you pass --app ..

The login page appears and no password works

Section titled “The login page appears and no password works”

The identity store is not seeded. No application ships with a user. dev detects this at startup and prints the command; if you missed it:

Terminal window
printf 'change-me' > admin.pw
tesseraql identity-schema --app . --admin-login admin --admin-password-file admin.pw

The hint is suppressed when password login is switched off (tesseraql.console.login.password.enabled), so if you see no hint and no user works, check that key. Full surface: authentication.md.

The framework’s artifacts are on Maven Central, so nothing needs configuring — the scaffolded pom.xml declares no repository because it needs none. A resolution failure is a version that is not published yet, or a mirror in your ~/.m2/settings.xml that does not proxy Central; behind an internal mirror see proxy.md.

Earlier releases were read from GitHub Packages and needed a read:packages token in ~/.m2/settings.xml. That is no longer required, and the entry can be removed.

--embedded-db refuses to start on an existing directory

Section titled “--embedded-db refuses to start on an existing directory”

A persistent data directory is pinned to the PostgreSQL version that initialized it, so a CLI upgrade never silently makes your data unopenable. Run tesseraql embedded-db info ./pgdata — it reports where the directory stands and prints the upgrade procedure when one applies.

Only the PostgreSQL driver and the CSV codec are in the base distribution. Oracle, SQL Server, MySQL, and the pdf/excel/s3 modules are opt-in because their licences differ:

Terminal window
tesseraql modules add com.oracle.database.jdbc:ojdbc11 --app .

That edits tesseraql.yml and writes modules.lock. See getting-started.md.

The server does not watch the filesystem by default. Routes mount at start, when Studio applies an edit, or when --watch sees the file change:

Terminal window
tesseraql dev --watch

Jobs, queue consumers, and config/ changes still need a full restart, whichever loop you use.

TQL-SQL-2103 — referenced SQL file is missing

Section titled “TQL-SQL-2103 — referenced SQL file is missing”

A source’s sql.file: resolves relative to the route document’s own directory, not the app root. A route at web/orders/get.yml naming search.sql looks for web/orders/search.sql. The linter reports it, and the runtime refuses to start on it with the same code and the same sentence, naming the route and the key (route 'orders' sources.main.file: referenced SQL file is missing: search.sql). Under dev --watch the route serves its compile error until the file is back, and the reload that brings it back clears the stub on its own.

TQL-YAML-1075 / TQL-TPL-2001 — a file the document names is outside the app, or nowhere

Section titled “TQL-YAML-1075 / TQL-TPL-2001 — a file the document names is outside the app, or nowhere”

Every file a document names by relative path — a statement, an export’s template:, a page’s response.html.template — may sit anywhere inside the application home: ../order.sql from web/orders/detail/, ../../shared/report.xlsx, a subdirectory. One that resolves outside the home (../../../outside.sql, an absolute path elsewhere) is refused with TQL-YAML-1075 naming the document, the key and the value — by the linter, the admission gate, the boot, the hot reload and job run alike; move the file under the home. A page template is looked for beside the document, then under the application’s templates/; one found in neither is TQL-TPL-2001, at lint as at boot.

TQL-SQL-2101 / 2102 — an expression or a SQL file does not parse

Section titled “TQL-SQL-2101 / 2102 — an expression or a SQL file does not parse”

Lint reports both where they are written: 2102 for a 2-way SQL template the parser cannot read (an unterminated directive, a bind without its dummy), 2101 for an expression — a /*%if*/ directive’s, a validate: rule’s, a statusWhen:, headersWhen:, step when:, notification when:/recipient: or workflow guard — naming the line where it has one. A string literal knows the escapes \', \" and \\ only, so '\d' is 2101; the regex class is written '\\d'. A literal matches() pattern that does not compile is 2101 too. The runtime refuses a request that renders such a file with the same code, so a route that answers 2101 on every request has a SQL file the lint would have refused.

TQL-SQL-2123 — a SQL file carries a transaction-control statement

Section titled “TQL-SQL-2123 — a SQL file carries a transaction-control statement”

A 2-way SQL file never owns its transaction: the command pipeline does on a request, the test runner does for a case (and always rolls it back), the Studio sandbox does for a console run. A commit; at the end of the file — a DBA script’s trailing terminator, an Oracle habit — ended the transaction the runner thought it owned, so the case’s write persisted behind a green result and every later run added one more row. The parser refuses the file, naming the line and the statement, on every surface: remove the statement. A PL/SQL or T-SQL block that starts with BEGIN and continues with a statement is not one; BEGIN alone, BEGIN WORK and BEGIN TRANSACTION are.

TQL-YAML-1410 / 1411 — the suite’s case names

Section titled “TQL-YAML-1410 / 1411 — the suite’s case names”

The reports join results to cases by name, so a name declared twice across the app’s suites is refused (1410, both files named): before, both twins showed the first result and a failing duplicate rendered green on the portal’s route page. A --case filter naming no case is refused too (1411, exit 2): a CI step pinned to a since-renamed case used to run nothing, exit 0, and with --report overwrite the overlay with an all-green run of nothing.

TQL-SQL-2122 — an expression met an operand it cannot evaluate

Section titled “TQL-SQL-2122 — an expression met an operand it cannot evaluate”

A relational comparison (<, >, <=, >=) on a null or on two values of unrelated kinds, arithmetic on a non-number, a division by zero, or a matches() pattern bound at request time that does not compile. The sentence names the operator and the operand kinds. It answers 500 on purpose: the request is ordinary — an optional input left out — and the template is what is defective. Guard the site (minPrice != null && minPrice > 0), or declare the input required; a validate: rule on an optional field takes the guard as its when:.

TQL-YAML-1066 / 1067 — the recipe reads a piece the document does not declare

Section titled “TQL-YAML-1066 / 1067 — the recipe reads a piece the document does not declare”

A query-json, command-json or webhook route answers through response.json: or response.redirect:; a page or query-html route renders response.html: or response.file:. A document with neither — no response: block at all, or one holding only session:, or the page arm on a JSON recipe — is TQL-YAML-1066, naming the route and what the block does declare. Every source and step runs through one arm — sql: { file: … }, contract:, service:, http: (with its url:), or sequence: on a command step — and one that names none is TQL-YAML-1067. A file-import route needs its import: block and one steps: entry with a file; an export recipe needs a main source with a file to read; each is TQL-YAML-1041. All are lint errors and boot refusals with the same sentence.

TQL-YAML-1069 / 1070 / 1071 — a source that never binds

Section titled “TQL-YAML-1069 / 1070 / 1071 — a source that never binds”

An input is fed by what the route declares: the path, the query, the form or JSON body. A request header is not among them — Host, Accept, Priority and Cookie arrive on every browser request under ordinary names, so an input spelled like one would be filled by the browser, not the caller. The one place a header is a source is a service: binding’s params: (cookie: header.Cookie), read from the wire; the same spelling on a statement’s params:, a validation rule’s, an enrichment’s or an export’s after: is TQL-YAML-1069. body.<name> on a GET route is TQL-YAML-1070: a GET carries no body, so the value was null on every request — declare the input and read query.<name>. body.<name> naming a field the route does not declare under input: is TQL-YAML-1071 when the route rejects unknown fields (the default): a request carrying the field is refused by the mass-assignment guard before anything binds. 1069 is a boot refusal as well; the other two are lint errors.

TQL-YAML-1072 — an input’s default: is not a value the input accepts

Section titled “TQL-YAML-1072 — an input’s default: is not a value the input accepts”

A declared default: is what the binder hands every request that omits the field, so it is judged as a caller’s value would be, where the declaration is read. It is parsed into the declared type (default: abc on type: number, default: yesterday on type: date) and held to the field’s constraints (default: up outside enum: [asc, desc], default: 0 under min: 1, a sort default naming a column outside columns:). A default on an array input is refused too — nothing binds it. The same predicate refuses the boot; the omitting request then binds the typed value, so a quoted "5" on an integer input reaches the statement as a number. Write the default as the caller would write the value.

TQL-YAML-1073 / 1074 — a response literal the edge writes as given

Section titled “TQL-YAML-1073 / 1074 — a response literal the edge writes as given”

A file response’s body is written as UTF-8 whatever contentType: says, so charset=Shift_JIS was a header contradicting its bytes: declare charset=utf-8 or omit the parameter (TQL-YAML-1073). A redirect location: is written as given, so whitespace at either end reaches the wire — a trailing space as %20, and a leading one keeps the base path off the value (the join applies it only to a value starting with /), so the browser resolves the redirect relative to the current page, outside the application. Quote the literal without the space (TQL-YAML-1074). Both are lint errors and boot refusals.

TQL-YAML-1076 — a filename placeholder the site cannot resolve

Section titled “TQL-YAML-1076 — a filename placeholder the site cannot resolve”

A download or delivered name is a template — orders-{params.month}.csv on a route, report-{batch.businessDate}.csv on a job step — and a placeholder that would not resolve renders _ (or, outside the {dotted.path} grammar, stays in the name braces on). The linter and the boot name the placeholder and why. A spelling with a hyphen or a call ({batch.business-date}, {now()}) is outside the grammar. A root the site does not carry: a route resolves params, query, path, body, tenant, request, flags, preference and, when authenticated, principal; a job step resolves params, steps, batch, tenant. A params. or query. name is not declared under input:, or a path. name is not in the route’s URL. Declare the input, spell the path parameter as the URL does, or drop the placeholder. {key} is the split export’s and is judged by TQL-YAML-1041.

TQL-YAML-1077 — a source’s cache: where nothing can be held

Section titled “TQL-YAML-1077 — a source’s cache: where nothing can be held”

A source’s cache: {maxAge, tables} asks the runtime to hold the rows its statement produced (response-shaping.md). The linter and the boot name the source and why nothing can be held there. A command-json, webhook, queue-consume or file-import route, or a step: a hold inside or after the write is a stale read of it. A query-export or file-export: the statement streams and is never held. An MCP tool that writes. A contract:, service:, http: or spool: arm: no statement text to key. A mode: update or call. A maxAge: that is missing, not a duration or not positive. A tables: that is missing, empty or carrying a blank name. Move the hold to a sql: { file: … } source in mode query of a query-json, query-html or page route, and declare both keys. On an enrich: entry the same code refuses a hold over a source: reference, which fetches nothing, and tables: on an http: reference, which nothing stamps — an http: reference takes maxAge: alone. The route-level cache: block is the HTTP one and is judged by TQL-YAML-1025.

TQL-YAML-1412 — a policy rule names two conditions

Section titled “TQL-YAML-1412 — a policy rule names two conditions”

An anyOf rule is one condition: role:, permission: or claim:. A rule naming two — {role: ADMIN, permission: orders.approve} — used to apply the first the runtime recognised and drop the rest, so the permission holder was refused with the rule lint-clean. Split it into one rule per condition (the policy grants when any holds), or bundle the shared authority into a role. A rule naming none of the three is logged at boot and the policy denies everyone, as before.

TQL-SEC-4135 / 4139 / 4152 / 4153 — a response header the wire cannot carry

Section titled “TQL-SEC-4135 / 4139 / 4152 / 4153 — a response header the wire cannot carry”

A header name is a token — letters, digits and the punctuation !#$%&'*+-.^_|~ or a backtick, nothing else — and the transport refuses any other name where the refusal hangs the response instead of failing it. A default under security.responseHeaders with a space, a colon, a trailing blank or a non-ASCII letter in its name is refused at lint and at boot (TQL-SEC-4135, the value check’s code). One the transport owns (Content-Length, Connection, the tql. namespace) is refused the same way (TQL-SEC-4139). A route’s own headers: key with such a name is TQL-SEC-4152, and the edge answers 500 rather than hanging if one reaches it. TQL-SEC-4153 is a warning: a Content-Disposition whose filename= is built from a placeholder is neither quoted nor encoded on the wire — name a download with response.file: filename:, which is both.

TQL-MCP-4265 / 4266 / 4267 / 4268 / 4269 / 4270 — the MCP endpoint refused the request, not the message

Section titled “TQL-MCP-4265 / 4266 / 4267 / 4268 / 4269 / 4270 — the MCP endpoint refused the request, not the message”

The MCP transport judges the caller before it reads the JSON-RPC message. 4265 (403): the request named an Origin that is neither loopback nor one the server allows — a web page is calling, which the MCP specification’s guard against DNS rebinding refuses; a local client page uses a loopback origin, and the dev server admits others with --allow-origin. 4266 (415): the POST did not declare Content-Type: application/json. 4267 (400): the MCP-Protocol-Version header names a revision the server does not speak — send the one initialize negotiated, or none. 4268 (400): a request after initialize carried no Mcp-Session-Id; 4269 (404): the one it carried names no live session — initialize again. 4270 (413): the body exceeds the dev transport’s 10 MiB ceiling. A body that is not JSON is answered inside the protocol, as JSON-RPC -32700.

TQL-OAUTH-3005 — tesseraql.mcp.resource declared under the stack issuer

Section titled “TQL-OAUTH-3005 — tesseraql.mcp.resource declared under the stack issuer”

The stack’s authorization server names a member’s MCP resource from its address (<origin><base path>/_tesseraql/mcp): its metadata document publishes that name and its grants carry it. A declared tesseraql.mcp.resource is a name no client is told and no token could carry, so the member is refused at boot rather than refusing every token silently. Remove the key; the override is for a standalone runtime behind an external identity provider (oauth.md).

TQL-YAML-1004 and friends — a key is refused on this recipe

Section titled “TQL-YAML-1004 and friends — a key is refused on this recipe”

Keys are recipe-scoped. notify: is command-only, cache: is query-only, refreshOn: is not a form-view key. A source with an http: arm has its own rule (TQL-YAML-1022): query recipes and transactional ones, where the call runs before the write’s transaction. The YAML surface reference lists which root properties apply to which kind and recipe.

A key the document does not have is TQL-YAML-1043, a warning saying it is ignored; a key that moved before v1 is TQL-YAML-1044, an error naming where it went. Both check a block whose shape is fixed — export:, import:, outbox:, errors: — as well as the document itself, so export.sql: is reported rather than dropped.

TQL-VIEW-3304 / 3308 / 3309 — a view names something the route does not declare

Section titled “TQL-VIEW-3304 / 3308 / 3309 — a view names something the route does not declare”

A view’s fields:, source:, children:, panels: and search: are checked against the route it belongs to — for an embedded view, the route hosting it. Usually the input exists under a different name: the column name is the name everywhere, so check the DDL rather than guessing a camel-case variant (identifiers.md). A source name must be main or a sources: key of that route; sql, the pre-0.14 spelling, is neither.

TQL-SEC-4031 — 403 on a route that should be open

Section titled “TQL-SEC-4031 — 403 on a route that should be open”

Routes are deny-by-default. A route is reachable only when it declares how it authenticates, and path-matched defaults under tesseraql.security.defaults.routes may be supplying an auth: you did not intend. Rules are first-match-wins in declaration order, so read the list top to bottom (authentication.md).

TQL-SEC-4032 — CSRF token missing or invalid

Section titled “TQL-SEC-4032 — CSRF token missing or invalid”

A browser write needs the CSRF field in the form. The bundled patterns include it; a hand-written form must too (hypermedia-ui.md). Bearer and API-key routes never require CSRF, so this code on an API route means the route resolved to browser auth.

TQL-SEC-4070 / 4080 — an outbound host is refused

Section titled “TQL-SEC-4070 / 4080 — an outbound host is refused”

Outbound HTTP and poll sources are allow-listed, deny-by-default. Add the host to tesseraql.http.outbound.allowedHosts or tesseraql.connectors.poll.allowedHosts (connectors.md).

A migration that has already been applied was edited. Do not edit applied migrations — add a new one. If the mismatch is a known-good edit, tesseraql migrate repair --app . rewrites the history table; tesseraql migrate info shows the current state first.

The caller does not hold this application’s tql.studio.edit.<name> atom (or the tql.studio.edit.* wildcard). Editing is deny-by-default: grant the atom through IAM Admin or a role that bundles it. See studio.md.

Open the ops console’s Outbox page. A message that exhausted its attempts is dead-lettered with its last error, and Redeliver retries it. If nothing is there at all, the command never enqueued it — check that notify: is declared on the command and that lint passes (TQL-BATCH-5301 names an unconfigured channel).

Three separate causes, distinguishable on the jobs page:

  • A calendar filtered the firing. Calendar-filtered firings leave no execution row by design. The Calendar next column shows the next date the calendar admits.
  • Another node holds it. Firings are claimed cluster-wide so exactly one node runs each.
  • It never fired. Check the trigger, and that TQL-BATCH-4201–4203 did not flag the calendar reference at build time.

See jobs.md and ops-console.md.

An import or export finished but produced nothing

Section titled “An import or export finished but produced nothing”

The Transfers page carries the row counts and the produced file. An import route with onError: skip ends COMPLETED even when rows were rejected — rowCount counts only the applied rows, and the status response lists each rejected row with its number and message. A zero-row COMPLETED therefore means every row was rejected, not that the file was empty (file-transfers.md).

For a batch job’s chunk step the equivalent is the managed tql_job_skips table, which records the row key and message for each skipped row until skipLimit fails the step (jobs.md).

A running export’s rowCount is the rows handed to the codec so far, refreshed every couple of seconds; a FAILED or STOPPED export keeps the count it had reached, and its file is gone. STOPPED is the cooperative stop — the route’s Cancel, the operations console’s, or a runtime shutting down — and the execution’s exit message says which.

TQL-LD-2868 — a completed export’s file cannot be opened

Section titled “TQL-LD-2868 — a completed export’s file cannot be opened”

The transfer says COMPLETED and points at bytes this node cannot read: with the default node-local tesseraql.temp.store: file, a stack member other than the one that ran the export answers its /file with 410, and so does any node after an external cleaner emptied the spool directory. The message names the store. Run the export again, or move the temp store off the node with tesseraql.temp.store: db (file-transfers.md).

TQL-LD-2857 — two splitBy: keys name one file

Section titled “TQL-LD-2857 — two splitBy: keys name one file”

A split export refuses two group keys whose entry names are the same once made safe for a filesystem — the same name outright, or the same up to case, which Windows and macOS read as one file. The message names both keys; either rename the source values or split on a column whose values differ by more than case. Combining marks are kept (an abugida key is spelled as it is) and a name Windows reserves for a device (CON, NUL, COM1…) is prefixed with an underscore rather than refused.

TQL-APP-4206 — a code catalog could not be loaded and has never loaded

Section titled “TQL-APP-4206 — a code catalog could not be loaded and has never loaded”

A page that renders a code, or a command that validates one, answers 500 with this code while every other route serves: the catalog’s table is missing on this environment, or its datasource was down the first time anything asked. The log names the catalog and its tables, and GET /_tesseraql/ops/catalogs shows the catalog as never loaded with the error beside it. Create the table (or bring the datasource back); the store tries again within a few seconds, at once on a maintenance write naming the table, or on POST /_tesseraql/ops/catalogs/{name}/refresh (code-catalogs.md).

Health is DOWN but the application answers

Section titled “Health is DOWN but the application answers”

The health probe covers the datasources, not just the HTTP port. The ops console overview shows the per-datasource probe results behind the roll-up badge (deployment.md).

  • Find the code. Every framework refusal carries TQL-<DOMAIN>-<n>, and the error code reference indexes all of them with the file that raises each — including the ones no page discusses.
  • Check the key. The configuration reference lists every configuration key the framework reads and what reads it.
  • Run the linter. tesseraql lint --app . catches at build time most of what would otherwise surface at request time.