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:
- The dashboard id in
dql.app.json'shomepagedoesn't match any.dqldfile'sidfield, or - You renamed a
.dqldfile 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:
- You're not in the App view. The persona switcher only renders when
mainView === 'apps'. Click the Apps activity bar icon first. - The App has no members. Open
dql.app.json— themembers[]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:
- The block declares
@rls("branch", "{user.branch}")(and@rls("region", …)). - The App has
rlsBindingsmappingrole: "branch_viewer"→from: "branch". - The member's
attributes.branchis 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
ollamashowstrue, you have no API keys set. That's fine — the agent will use Ollama. Ifollamareturnsfalse, 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_FOUNDorCOLUMN_NOT_FOUND: make the active connection point at the warehouse containing the dbt-built relations, rundbt buildwhen 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_ERRORorAMBIGUOUS_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:
- 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.
- Wrong signing secret. Compare with the Basic Information page of your Slack app — not the Bot User OAuth Token (which is different).
- Stale request. The verifier rejects requests > 5 minutes old.
Clock skew is rare but check
dateon 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.
dbtImportfilters 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:
- Pin the test data (commit a small CSV that drives the test).
- Loosen the assertion (a lower bound rather than an exact
==). - Move volatile expectations to
invariants(which are documentation for the agent, not executable). Noteassertcompares 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:
- Block lifecycle and certification:
packages/dql-governance/src/certifier.ts - Apps + dashboards parsing:
packages/dql-core/src/apps/ - Persona registry:
packages/dql-project/src/persona.ts - RLS lowering:
packages/dql-compiler/src/ir/lowering.ts:440(applyRLSDecorators) - Agent answer loop:
packages/dql-agent/src/answer-loop.ts - Slack server:
packages/dql-slack/src/server.ts
If you find a bug or rough edge in any of these:
- File an issue at https://github.com/duckcode-ai-labs/zlens/issues.
- PRs welcome — every package has tests in
*.test.tsnext to the source it covers.
Need more?¶
- The end-to-end story: tutorials.
- The roadmap: ROADMAP.md.