Skip to content

Troubleshooting

Guides · Troubleshooting

Errors grouped by where they surface. If you hit something not listed here, open a Discussion.


No askdb.config.* or .config/askdb.* found in …

Section titled “No askdb.config.* or .config/askdb.* found in …”

askdb ask and askdb introspect print this when the current directory has no config. bootstrapAskDbEnv() throws a similar error (… found under <cwd>) when you embed the libraries.

AskDB looks for askdb.config.<ext> (then .config/askdb.<ext>) in the current working directory only — in a monorepo it won’t find a config in a parent directory. Supported extensions, in precedence order: ts, mts, cts, js, mjs, cjs.

If the project has no config yet, create one with npx askdb init. If it has one, run the CLI from the directory that holds the config.

Terminal window
cd apps/api && npx askdb ask --question "..."

When embedding the libraries, the equivalent knob is the cwd option: bootstrapAskDbEnv({ cwd: "/path/to/project" }).

Set ASKDB_DEBUG=1 to print the stack trace.

Section titled “Set ASKDB_DEBUG=1 to print the stack trace.”

The CLI hit an unexpected error, most often a config file that exists but fails to load: a syntax error, a missing import, or dependencies that aren’t installed yet. The line above the hint is the error message, and it usually names the file and line.

Rerun with ASKDB_DEBUG=1 to see the stack trace:

Terminal window
ASKDB_DEBUG=1 npx askdb ask --question "..."

askdb studio doesn’t stop on a config that fails to load: it prints askdb-studio: warning: <path> could not be loaded, so Studio is ignoring it: … and starts without it. Fix the file and restart Studio; the setup wizard never overwrites it.

SchemaParseError: Cannot read schema at …

Section titled “SchemaParseError: Cannot read schema at …”

The path passed to --schema (CLI) or loadSchema() (library) doesn’t exist or isn’t a valid schema artifact. Valid inputs are:

  • A schema directory (e.g. my-app.schema/)
  • A bundled JSON file (e.g. my-app.schema.json)
  • A schema.json file inside a directory (the name matches in any case, so Schema.json works too)

Run ls my-app.schema/ to confirm the directory was created by askdb introspect. If it’s empty, the introspection step didn’t complete successfully.

SchemaParseError: Refusing to load … as a standalone physical layer

Section titled “SchemaParseError: Refusing to load … as a standalone physical layer”

You passed a JSON file that isn’t named schema.json, such as prod-schema.json, and its directory also holds tenant-policy.md, tables/, or concepts.md. Loading the file alone would ignore those files, so ask() would run without your tenant policy and without the sensitive: true marks in tables/*.md. AskDB refuses instead.

Pass the directory to --schema or loadSchema(), or rename the file to schema.json. To load only the physical layer, pass the file’s content to loadSchemaFromJson().

SchemaParseError: Missing required field …

Section titled “SchemaParseError: Missing required field …”

The schema artifact is from an older format. Re-run introspection to regenerate it:

Terminal window
npx askdb introspect --url "$DATABASE_URL" --out my-app.schema --schema-id my-app

SqlValidationError: SQL_NOT_SELECT_OR_WITH

Section titled “SqlValidationError: SQL_NOT_SELECT_OR_WITH”

AskDB only returns SELECT or WITH … SELECT statements. The model generated a non-SELECT statement (e.g. INSERT, UPDATE, DROP). This is a guardrail, not a bug.

If you’re seeing this on legitimate read questions, the model may be misreading the schema. Check that your table and column descriptions in the schema artifact don’t imply write operations.

The model returned more than one SQL statement. AskDB refuses multi-statement output. Rephrase the question to ask for a single result, or break it into two separate ask() calls.

SqlValidationError: SQL_COMMENT / SQL_FORBIDDEN_KEYWORD / SQL_FORBIDDEN_FUNCTION

Section titled “SqlValidationError: SQL_COMMENT / SQL_FORBIDDEN_KEYWORD / SQL_FORBIDDEN_FUNCTION”

The generated SQL contains a SQL comment (including # on MySQL), a forbidden keyword (INTO, DELETE, EXEC, WAITFOR, etc.), or a call to a blocked side-effecting function (pg_sleep, set_config, LOAD_FILE, SLEEP, etc.). Keywords are matched as whole, unquoted tokens, so a column like deleted_at or "delete" is fine, but an unquoted column or alias named like a keyword (say, update) also trips them. These are heuristic, defense-in-depth guardrails. If one fires on a legitimate query, file an issue with the question and schema excerpt.

A string literal, quoted identifier, dollar-quoted string, or block comment in the generated SQL never closes, so the validator cannot tell where the statement ends and rejects it. On MySQL or MariaDB, check that the dialect’s backslashEscapes setting matches the server (NO_BACKSLASH_ESCAPES). A mismatch matters beyond false rejections: it also lets SQL through that the server reads differently. See Match your server’s string settings.

Generated SQL references a column or table that doesn’t exist

Section titled “Generated SQL references a column or table that doesn’t exist”

This is the most common failure mode. The model hallucinated an identifier that isn’t in your schema. AskDB doesn’t check generated identifiers against the schema, so your database reports these when you run the query.

Fix: Enrich the relevant tables in your schema artifact with plain-English descriptions. The more context the model has, the less it invents:

Terminal window
npx askdb studio # open the enrichment UI
# or
npx askdb enrich # headless AI-assisted enrichment

If the column exists in the database but the model keeps getting it wrong, check that askdb introspect has been re-run since the column was added, and that its description in the schema artifact isn’t ambiguous.

The SQL is syntactically valid but logically wrong

Section titled “The SQL is syntactically valid but logically wrong”

SQL validation is heuristic — AskDB confirms the statement kind and blocks write keywords and side-effecting functions, but it doesn’t check identifiers and can’t verify that the query answers your question semantically. The quality of answers depends almost entirely on how well the schema artifact describes your data.

Checklist:

  1. Does the relevant table have a description? (description: field in the table’s .md file)
  2. Do column names match how users phrase questions? Add aliases in the schema artifact.
  3. Is this a complex aggregation or join? Add an example question + expected SQL to the table file.
  4. Try a more capable model — gpt-4o vs gpt-4o-mini makes a meaningful difference on complex schemas.

Error: API key not found / AuthenticationError

Section titled “Error: API key not found / AuthenticationError”

The model provider didn’t receive a key. For the CLI, set the environment variable before running:

Terminal window
OPENAI_API_KEY=sk-... npx askdb ask --question "..."

For the library, verify the key is passed when constructing the provider:

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });

See Bring your own model for per-provider configuration.

Error: Model not found / 404 from provider

Section titled “Error: Model not found / 404 from provider”

The model name passed to the provider doesn’t exist or isn’t available on your account tier. The default CLI model is gpt-4o-mini (OpenAI). Override it with OPENAI_MODEL in your askdb.config.ts or pass a different model instance to ask().


Studio shows “driver not installed” or an Install button

Section titled “Studio shows “driver not installed” or an Install button”

Studio’s Execute Query feature (running SQL against a live database) requires an optional peer driver package. Install the one matching your engine:

Terminal window
npm install pg # Postgres
npm install mysql2 # MySQL
npm install better-sqlite3 # SQLite
npm install mssql # SQL Server

After installing, refresh Studio. The driver readiness indicator near the Execute button should clear.

Studio execute is off by default. Check that studio.execute.enabled: true is set in askdb.config.ts, that studio.execute.databaseUrl (or studio.execute.file for SQLite) is set, and that the database is reachable from your machine. Restart Studio after editing the config. Studio resolves the execute connection in this order:

  1. studio.execute.databaseUrl / studio.execute.file
  2. The introspection connection for the same engine, only when studio.execute.useIntrospectionConnection: true

The provider comes from studio.execute.provider, or the active introspection.provider when it’s a live engine.

See Configuration — Studio execute for the full resolution order.


self-signed certificate / TLS error on connection

Section titled “self-signed certificate / TLS error on connection”

SQL Server enables TLS by default. Local and dev instances typically use a self-signed certificate, which Node.js rejects:

Error: self-signed certificate — try running Node.js with --use-system-ca

Add trustServerCertificate=true to the connection URL:

# mssql driver URL format
mssql://sa:pass@localhost:1433/MyDb?trustServerCertificate=true
# sqlserver introspection URL format
sqlserver://localhost:1433;database=MyDb;user=sa;password=pass;trustServerCertificate=true

Your schema has a tenant-policy.md file, which means AskDB requires a tenantScope on every ask() call. Pass one:

const { sql } = await ask({
question,
schema,
model,
dialect,
tenantScope: {
access: {
kind: "ids",
tenantRoot: "table:public.organizations",
ids: [currentUser.orgId],
},
},
});

See Multi-tenancy for the full scope shape.

The tenantRoot in the scope doesn’t match any root defined in tenant-policy.md. Check the id: field of each root in the policy file — the scope must use the same stable table ID (e.g. table:public.organizations).

The scope uses access.kind: "subtree", but ask() couldn’t expand it. Either no resolveTenantDescendants callback was passed, or the callback didn’t return IDs per tenant root. The message says which: it returned an array (the flat shape from before per-root IDs), something other than a plain object, a key that isn’t a tenant root in the subtree, a value that isn’t an array of non-empty string IDs, or no IDs at all. AskDB doesn’t query your database to find descendants, and it won’t fall back to the seed IDs alone. Return an object keyed by root table ID, with the seeds and same-table descendants under the scope’s tenantRoot and each descendant root’s IDs under that root: { "table:public.agencies": ["1"], "table:public.clients": ["5"] }. Or send a multi_root access with each root’s IDs already expanded. Calling resolveTenantSql(), buildTenantPromptBlock() or bindPreparedQuery() directly with an unexpanded subtree scope throws the same error, because none of them walks the hierarchy: expand it first with expandTenantScope(), or rebind through askdb.bind(), which does. See Hierarchical scope.

TenantScopeError: UNRESOLVED_TENANT_PLACEHOLDER

Section titled “TenantScopeError: UNRESOLVED_TENANT_PLACEHOLDER”

The SQL uses a :tenant_<root>_ids placeholder that the scope has no IDs for, so AskDB refuses to return it with the placeholder unfilled. Common causes: the scope covers another root than the one the SQL filters on (a template stored under an agency scope, rebound under a client scope); a covered root has ids: []; the placeholder is written in another casing (:TENANT_AGENCY_IDS) or runs into the next word (:tenant_agency_idsOR); or the scope is global, which binds no IDs at all, so SQL under it must not use a tenant placeholder. At rebind, a template with a placeholder also throws this when the schema has no tenant policy. Re-ask the question under the right scope.

SchemaParseError: Invalid tenant policy: roots … both map to the placeholder …

Section titled “SchemaParseError: Invalid tenant policy: roots … both map to the placeholder …”

Two roots in tenant-policy.md derive the same placeholder. A placeholder is the label lowercased, with every run of characters other than ASCII letters and digits turned into _, so Agency and agency both give :tenant_agency_ids. A label with no ASCII letter or digit (Cyrillic, CJK and so on) uses the root’s table name instead: Клиент on table:public.clients gives :tenant_clients_ids. That fallback can still collide, for example when both table names are non-ASCII too, or when it meets another root’s label. Each root’s IDs are bound through its own placeholder, so a shared one would bind one root’s IDs where the other root’s column is compared. AskDB rejects the policy when it loads, ask() and resolveTenantSql() reject a policy built in code the same way, and Studio refuses to save it. Rename one of the two roots named in the message, using a label with ASCII letters or digits if the fallback collides. See the label row in the field reference.

Your tenant policy uses enforcement: strict, and the generated SQL is missing a required tenant predicate on a tenant-scoped or root table, names a table the policy leaves unclassified, or names a polymorphic table without its type discriminator. AskDB throws instead of returning that SQL. A predicate counts only as the table’s tenant column compared with its placeholder (agency_id = :tenant_agency_ids, or IN (…) / = ANY(…)), ANDed into a WHERE, inner-join ON, or HAVING clause. A literal tenant ID, a column that’s only selected, or a predicate next to an OR doesn’t count, and a query on the root table itself needs the predicate too; see What the tenant check requires. Under a subtree scope, each covered root the query reads needs its own placeholder: filtering clients only through clients.sub_agency_id or a joined agency is rejected, and a covered root you returned no IDs for can’t be read at all (see Hierarchical scope). Inspect the warnings array on the error to see which table triggered it, then re-ask the question or narrow it. bindPreparedQuery() and askdb.bind() run the same check on a stored template under the scope you rebind with, and with enforcement: warn they refuse a failing template unless you pass acceptWarnings: ["tenant"]; the message says so. The check is heuristic: passing it doesn’t prove the query is tenant-safe, so keep tenant isolation enforced in the database. See Run generated SQL safely.

QueryParameterError: MISSING_GUARDRAIL_CONTEXT

Section titled “QueryParameterError: MISSING_GUARDRAIL_CONTEXT”

bindPreparedQuery() was called with two arguments. Since it re-checks the stored template before binding, it needs a third: { schema, tenantScope? }, the same schema and the caller’s scope you pass to ask(). With @askdb/client, call askdb.bind(prepared, values, { tenantScope }) instead; it supplies the schema. A :tenant_* key in the values throws TENANT_VALUE_SUPPLIED: tenant IDs now come from tenantScope. See bindPreparedQuery().


Retriever is supplied but AskDB uses the full DDL anyway

Section titled “Retriever is supplied but AskDB uses the full DDL anyway”

Two reasons this happens:

  1. Below threshold: totalSchemaChunkCount is at or below retrievalThresholdChunks (default 30). Pass the actual chunk count from buildSchemaIndex result:

    const index = await buildSchemaIndex({ schema, embedder });
    const { sql } = await ask({
    ...
    retriever: index.retriever,
    totalSchemaChunkCount: index.stats.chunksTotal,
    });
  2. Outdated artifact format: The retriever is silently skipped for schema artifacts created by very old AskDB versions. Confirm loadSchema() returns an object with a schemaId field; if it doesn’t, re-run askdb introspect to regenerate the artifact.

The question didn’t match any indexed chunks. This usually means the schema artifact descriptions are too sparse for meaningful embedding similarity. Enrich your schema with richer table and column descriptions, then rebuild the index.