Skip to content

Troubleshooting + FAQ

Real fixes for the things that bite people first.


Setup

zlens says "command not found"

In a scaffolded project the CLI is a local dev dependency, exposed through npm scripts (npm run notebook, npm run compile, …). For ad-hoc commands use npx:

npx zlens --version
npx zlens certify blocks/revenue_by_month.dql

If npx zlens still fails, re-run npm install — the dependency link probably broke.

With a global install (npm i -g @zlens/cli), a bare zlens that reports "command not found" almost always means one of three things:

npx -p @zlens/cli@latest zlens --version   # escape hatch: no PATH needed at all
which -a zlens          # a stale Homebrew / venv / old global copy shadowing it?
hash -r                 # clear the shell's cached path, or just open a new terminal
npm prefix -g           # ensure "<that path>/bin" is on your $PATH
npm i -g @zlens/cli@latest --allow-scripts=better-sqlite3   # a failed earlier install never links the bin

The last one is the usual culprit for existing users: before 1.6.30, a global install could fail on Node 23/24 (the current LTS is Node 24). A failed npm i -g never creates the zlens symlink, so the command is missing even though "install" appeared to run. Reinstalling @latest on any Node ≥20 fixes it — see How do I upgrade ZLens? below.

"ZLens could not load better-sqlite3" (npm 12)

npm 12 blocks a dependency's install script unless package.json lists it in allowScripts. better-sqlite3, which holds ZLens's local index, downloads its native binary in that script, so without the entry npm install prints install scripts blocked because they are not covered by allowScripts and every ZLens command that opens the index stops with this message (older ZLens versions printed "Could not locate the bindings file"). In the project folder:

npm install-scripts approve better-sqlite3
npm rebuild better-sqlite3

To keep fresh clones working, add the entry to package.json (projects from create-zlens-app and the samples already have it):

"allowScripts": {
  "better-sqlite3": true
}

The DuckDB driver has the same script. ZLens allows it in .dql/connectors/package.json and rebuilds the driver the next time the workbench starts or zlens sync warehouse runs; by hand:

npm --prefix .dql/connectors install-scripts approve duckdb
npm --prefix .dql/connectors rebuild duckdb

A global install has no package.json: reinstall with npm install -g @zlens/cli --allow-scripts=better-sqlite3. npm run doctor checks both packages.

How do I upgrade ZLens?

ZLens is plain npm packages — upgrading is just installing the latest version.

# Global CLI (npm 12 needs --allow-scripts=better-sqlite3)
npm i -g @zlens/cli@latest --allow-scripts=better-sqlite3 && zlens --version

# Project-local CLI (recommended)
npm i -D @zlens/cli@latest && npx zlens --version

You do not need a working zlens to upgrade, and you do not need to downgrade Node — 1.6.30+ installs on Node 20, 22, and 24. If zlens --version still shows the old number afterward, run hash -r (or open a new shell) and check which -a zlens for a shadowing copy.

zlens --version works, but cd dql says "no such file or directory"

The CLI install only installs the command. It does not create a project folder. From your dbt repo root, initialize ZLens first:

zlens init ./dql
cd dql
zlens doctor

Use zlens init . only if you want ZLens folders directly in the current repo root.

Optional connector package errors

DuckDB and Snowflake drivers are installed project-locally when needed. If the Settings → Database says a driver package is missing, click Install connector or run the printed project-local install command. If a native package fails to build on a fresh OS install, check Node 20, 22, or 24 LTS and local build tools.

node -v
npm -v

Then retry the connector install. For local project dependencies, wiping and reinstalling can help:

rm -rf node_modules package-lock.json
npm install

(Building the framework repo from source? Use pnpm install && pnpm -r build.)

Preview doesn't open

zlens preview opens your default browser. If you're on a headless box or the open command silently fails:

zlens preview blocks/revenue_by_month.dql --no-open

It still serves on localhost; the URL is printed in the console output.

"DuckDB file … is in use by another process"

DuckDB lets only one process open a database file at a time. While zlens notebook is running on the project, zlens certify, zlens model discover --validate, zlens model validate-relationship, zlens sync warehouse and zlens doctor send their read-only queries to it instead of opening the file again, and print Running … through zlens notebook (pid …). You see this error when the file is held by something else: a workbench from an older ZLens, a workbench on a different folder that points at the same file, dbt build, or a SQL client. Stop that process (Ctrl+C in the workbench's terminal) and run the command again. The workbench shows the same message when another program holds the file.


Authoring blocks

zlens certify says tests-pass: ✗ but the data looks right

The certifier runs the block against defaultConnection from dql.config.json (or the connection you pass with --connection). If your local DuckDB doesn't have the tables but your prod warehouse does, you'll see different test results. Pin the connection explicitly:

zlens certify blocks/foo.dql --connection duckdb

— and make sure the connection actually contains the tables the block reads (for a dbt project, run dbt build first).

Block parse error in blocks/foo.dql

The most common cause is query = "..." (single-line) where the SQL contains characters that need escaping. Use triple-quoted strings:

query = """
  SELECT 1
"""

Block doesn't show up in the manifest

zlens compile only scans blocks/, dashboards/, workbooks/, and apps/<id>/blocks/ (when configured). If your block lives somewhere else, add the directory to dql.config.json:

{
  "extraBlockDirs": ["custom_blocks"]
}

Two blocks have the same name

The manifest keys by block.name (the string after block "..."). If two .dql files both declare block "foo", the second one silently overwrites the first — but zlens validate catches it as an error. Run zlens validate early.


Apps + Local Policies

zlens app build says "homepage references unknown dashboard"

Either:

  1. The dashboard id in dql.app.json's homepage doesn't match any .dqld file's id field, or
  2. You renamed a .dqld file but kept the homepage id pointing at the old name.

Open apps/<id>/dashboards/*.dqld and confirm the id field matches.

Persona switcher does nothing

Two common causes:

  1. You're not in the App view. The persona switcher only renders when mainView === 'apps'. Click the Apps activity bar icon first.
  2. The App has no members. Open dql.app.json — the members[] array must be non-empty.

Verify on the server:

curl -s http://127.0.0.1:3475/api/persona | jq
curl -s http://127.0.0.1:3475/api/apps    | jq

Branch viewer sees rows from other branches

@rls only narrows when the block carries the decorator. Confirm:

  1. The block declares @rls("branch", "{user.branch}") (and @rls("region", …)).
  2. The App has rlsBindings mapping role: "branch_viewer" → from: "branch".
  3. The member's attributes.branch is set.

Trace the resolution:

curl -s http://127.0.0.1:3475/api/persona | jq '.persona.rlsContext'

rlsContext should show the substituted values. If it's {}, the App binding didn't match the member's roles + attributes.

policies[].domain = "*" doesn't match my block's domain

Wildcard * matches every domain. If you want "any domain in the cards org", make policies explicit per domain (cards, cards.fraud, cards.chargebacks) — the engine doesn't currently parse hierarchical domains.


Agent

zlens agent ask says "KG not built"

Run the reindex:

zlens agent reindex

That's the build step for .dql/cache/agent-kg.sqlite. It's also a no-op fast on rebuilds because the manifest fingerprint is checked first.

Provider returns nothing

Check available() per-provider:

node -e "
  const a = require('@zlens/agent');
  for (const name of ['claude','openai','gemini','ollama']) {
    a.buildProvider(name).available().then(ok => console.log(name, ok));
  }
"

If only ollama shows true, you have no API keys set. That's fine — the agent will use Ollama. If ollama returns false, the daemon isn't running. Start it: ollama serve.

Every certified block answer cites the same block, even when wrong

The FTS5 retrieval threshold is too loose for your domain. Tune in packages/dql-agent/src/answer-loop.ts:

const CERTIFIED_HIT_THRESHOLD = 0.18;   // raise to 0.30 for stricter matching
const HARD_NEGATIVE_RATIO     = 0.5;    // lower to 0.3 to disqualify on fewer downvotes

Rebuild and reindex.

Ask found the right metric but says the governed query failed

Open How it was answered on the failed answer. The inspector keeps the exact plan and, when compilation reached them, the DQL and SQL that were attempted. Its final section shows a stable failure code and only the repairs allowed for that failure.

  • RELATION_NOT_FOUND or COLUMN_NOT_FOUND: make the active connection point at the warehouse containing the dbt-built relations, run dbt build when appropriate, then use Refresh snapshot and prepare retry. Do not replace the governed relation with a similarly named table.
  • PERMISSION_DENIED: change to an explicitly authorized connection or request access. ZLens will not retry another source automatically.
  • DIALECT_ERROR or AMBIGUOUS_COLUMN: open the retained DQL/SQL in Notebook, edit the derived copy, and rerun through the normal guards. The source run and its receipt remain unchanged.
  • TIMEOUT: retry the same bounded plan. Research is not started implicitly.
  • RESULT_CONTRACT_MISMATCH: the query did not return every field promised by the plan, so the result cannot be narrated or saved as a complete answer.

For “today” and other relative periods, latest_complete also needs a governed reporting-time dimension and an authorized freshness observation. ZLens performs one bounded lookup using the already selected semantic metric/time members. A missing table or permission during that lookup is reported with the same stable warehouse failure code.

Ask cannot connect a metric to a customer or time dimension

Check the semantic model rather than adding a broad repository search. The metric capability must declare its primary entity, additivity, supported customer/time dimensions and roles, relationship path, time grains, freshness policy, operations, outputs, and adapter. Domain Skills may choose defaults such as timezone, completeness, comparison alignment, or ranking period, but they cannot invent a member, authorize a join, or override an incompatible metric.

Repository text search remains discovery evidence for dbt models and columns when governed coverage is absent. It can help build a review-required SQL plan; it never upgrades a grep match into semantic relationship proof.

Slack bot returns 401 "Bad signature"

In order of likelihood:

  1. Body modification. Slack signs the raw body. Some reverse proxies strip whitespace or rewrite content-type. Bypass them or move the bot directly behind ngrok.
  2. Wrong signing secret. Compare with the Basic Information page of your Slack app — not the Bot User OAuth Token (which is different).
  3. Stale request. The verifier rejects requests > 5 minutes old. Clock skew is rare but check date on your server.

Slack bot reply never arrives

The bot acks immediately ("Working on it…") then posts the real reply via response_url. If the response URL POST fails (firewall, DNS, timeouts), Slack will not retry. Check the bot logs for the failed fetch — zlens slack serve doesn't yet have structured logging; pipe to a file or wrap with 2>&1 | tee dql-slack.log.


CI / zlens verify

zlens verify says "drift" but the diff looks empty

The diff prints terse change descriptions. To see the full per-block diff, use --format json:

zlens verify --format json | jq

Common silent drift causes:

  • A block test result changed (a stat was non-deterministic).
  • A semantic-layer YAML moved.
  • dbtImport filters changed.

If you can't pin it, force a fresh compile and inspect:

mv dql-manifest.json /tmp/old-manifest.json
zlens compile
diff /tmp/old-manifest.json dql-manifest.json | head -80

zlens certify is non-deterministic in CI

If a block test like assert row_count >= 100 flickers, the underlying data is non-deterministic. Either:

  1. Pin the test data (commit a small CSV that drives the test).
  2. Loosen the assertion (a lower bound rather than an exact ==).
  3. Move volatile expectations to invariants (which are documentation for the agent, not executable). Note assert compares a single returned column to a value (assert <column> <op> <value>) — wrap any aggregation in the block's SQL and assert on the resulting column.

When in doubt, look at the source

The architecture is small and readable. Bookmark these:

If you find a bug or rough edge in any of these:

  1. File an issue at https://github.com/duckcode-ai-labs/zlens/issues.
  2. PRs welcome — every package has tests in *.test.ts next to the source it covers.

Need more?