crewAIInc/crewAI · error · ValueError
NL2SQLTool blocked an unrecognised SQL command '{main_cmd}'
Error message
NL2SQLTool blocked an unrecognised SQL command '{main_cmd}' after a CTE. Only {sorted(_READ_ONLY_COMMANDS)} are allowed in read-only mode. What it means
After read-only CTEs, if the main statement's first keyword is neither a write command nor in _READ_ONLY_COMMANDS (SELECT/SHOW/DESCRIBE/EXPLAIN), the tool blocks it as unrecognised. This is a deny-by-default allowlist: dialect-specific or unusual statements after a CTE are rejected in read-only mode.
Source
Thrown at lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py:365
main_query = _extract_main_query_after_cte(stmt)
if main_query:
main_cmd = main_query.split()[0].upper().rstrip(";")
if main_cmd in _WRITE_COMMANDS:
if not self.allow_dml:
raise ValueError(
f"NL2SQLTool is configured in read-only mode and blocked a "
f"'{main_cmd}' statement after a CTE. To allow write "
f"operations set allow_dml=True or "
f"CREWAI_NL2SQL_ALLOW_DML=true."
)
logger.warning(
"NL2SQLTool: executing '%s' after CTE because allow_dml=True.",
main_cmd,
)
elif main_cmd not in _READ_ONLY_COMMANDS:
if not self.allow_dml:
raise ValueError(
f"NL2SQLTool blocked an unrecognised SQL command '{main_cmd}' "
f"after a CTE. Only {sorted(_READ_ONLY_COMMANDS)} are allowed "
f"in read-only mode."
)
return
if command in _WRITE_COMMANDS:
if not self.allow_dml:
raise ValueError(
f"NL2SQLTool is configured in read-only mode and blocked a "
f"'{command}' statement. To allow write operations set "
f"allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true."
)
logger.warning(
"NL2SQLTool: executing write statement '%s' because allow_dml=True.",
command,
)
elif command not in _READ_ONLY_COMMANDS:View on GitHub (pinned to 754d7323be)
Solutions
- Remove the non-read statement or move it to a separate call against an engine configured for it.
- Add the missing read-only statement outside a CTE form if supported, or use plain SELECT equivalents.
- Set allow_dml=True only if you accept that ALL writes become possible — there is no per-keyword bypass.
Example fix
# before (blocked)
tool._run("WITH recent AS (SELECT * FROM logs) SET work_mem = '64MB'")
# after
tool._run("WITH recent AS (SELECT * FROM logs) SELECT * FROM recent LIMIT 10") Defensive patterns
Strategy: validation
Validate before calling
READ_ONLY = {"SELECT", "SHOW", "DESCRIBE", "EXPLAIN"}
def tail_cmd_after_cte(sql: str) -> str:
idx = sql.rfind(")")
tail = sql[idx+1:].strip() if idx != -1 else ""
return tail.split()[0].upper().rstrip(";") if tail.split() else ""
if not allow_dml and tail_cmd_after_cte(sql) not in READ_ONLY:
raise ValueError(f"command after CTE not allowed: {tail_cmd_after_cte(sql)}") Try / catch
try:
tool._run(sql)
except ValueError as e:
if "after a CTE" in str(e):
# rephrase to plain SELECT or run via direct engine
... Prevention
- Keep session/admin commands off NL2SQLTool; use a raw engine for those.
- Teach the model the allowlist: SELECT, SHOW, DESCRIBE, EXPLAIN.
When it happens
Trigger: Running 'WITH x AS (SELECT 1) VACUUM t', 'WITH x AS (...) SET search_path TO public', or any other statement whose leading keyword is not in the allowlist, while allow_dml=False.
Common situations: Portability statements like SET/USE/PRAGMA appended after CTEs; new or dialect-specific SQL keywords the allowlist does not know; LLM hallucinating an exotic command after a CTE.
Related errors
- NL2SQLTool blocked an unrecognised SQL command '{command}'.
- NL2SQLTool blocked a multi-statement query in read-only mode
- NL2SQLTool is configured in read-only mode and blocked a wri
- NL2SQLTool is configured in read-only mode and blocked a '{m
- NL2SQLTool is configured in read-only mode and blocked a '{c
AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15).
Data as JSON: /api/errors/6a17eee993075cda.
Report an issue: GitHub.