crewAIInc/crewAI · error · ValueError
NL2SQLTool is configured in read-only mode and blocked a '{m
Error message
NL2SQLTool is configured in read-only mode and blocked a '{main_cmd}' statement after a CTE. To allow write operations set allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true. What it means
For WITH statements whose CTEs are read-only, NL2SQLTool extracts the main query after the CTE definitions (_extract_main_query_after_cte) and checks its first keyword. If that keyword is in _WRITE_COMMANDS (DELETE, UPDATE, INSERT, ...), read-only mode raises ValueError — e.g. 'WITH x AS (SELECT 1) DELETE FROM users' is a write.
Source
Thrown at lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py:353
if not self.allow_dml:
raise ValueError(
f"NL2SQLTool is configured in read-only mode and blocked a "
f"writable CTE containing a '{found}' statement. To allow "
f"write operations set allow_dml=True or "
f"CREWAI_NL2SQL_ALLOW_DML=true."
)
logger.warning(
"NL2SQLTool: executing writable CTE with '%s' because allow_dml=True.",
found,
)
return
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
View on GitHub (pinned to 754d7323be)
Solutions
- If the mutation is intended, opt in: allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true.
- Otherwise strip the write part and keep only read statements.
- Tell the agent in the tool description/prompt that only SELECT/SHOW/DESCRIBE/EXPLAIN are allowed.
Example fix
# before (blocked)
tool._run("WITH t AS (SELECT id FROM users) DELETE FROM orders WHERE user_id IN (SELECT id FROM t)")
# after
NL2SQLTool(db_uri=uri, allow_dml=True) # explicit opt-in for writes Defensive patterns
Strategy: validation
Validate before calling
WRITE_CMDS = {"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE", "MERGE"}
def main_cmd_after_cte(sql: str) -> str:
import re
matches = list(re.finditer(r"AS\s*\(", sql))
if matches:
# naive: text after last top-level ')'
idx = sql.rfind(")")
tail = sql[idx+1:].strip()
return tail.split()[0].upper() if tail.split() else ""
return ""
if not allow_dml and main_cmd_after_cte(sql) in WRITE_CMDS:
raise ValueError("write after CTE blocked") Try / catch
try:
tool._run(sql)
except ValueError as e:
if "statement after a CTE" in str(e):
route_to_write_tool_or_reject(sql) Prevention
- Treat any statement whose first keyword is WITH as potentially mutating — validate before submission.
- Give the agent explicit read vs write tools instead of one multipurpose tool.
When it happens
Trigger: Passing 'WITH cte AS (SELECT ...) DELETE/UPDATE/INSERT/...' with allow_dml=False. LLMs commonly emit 'WITH latest AS (...) UPDATE ...' style statements on Postgres.
Common situations: Agent-generated CTE-wrapped mutations; refactoring of write scripts to use CTEs while the tool remains in default read-only mode.
Related errors
- NL2SQLTool is configured in read-only mode and blocked a wri
- NL2SQLTool is configured in read-only mode and blocked a '{c
- NL2SQLTool blocked a multi-statement query in read-only mode
- NL2SQLTool blocked an unrecognised SQL command '{main_cmd}'
- NL2SQLTool blocked an unrecognised SQL command '{command}'.
AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15).
Data as JSON: /api/errors/f8d05b7030359289.
Report an issue: GitHub.