crewAIInc/crewAI · error · ValueError

NL2SQLTool is configured in read-only mode and blocked a '{c

Error message

NL2SQLTool is configured in read-only mode and blocked a '{command}' statement. To allow write operations set allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true.

What it means

The core read-only guard: if the first keyword of a (non-CTE, non-EXPLAIN-resolved) statement is in _WRITE_COMMANDS (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, ...) and allow_dml=False, NL2SQLTool raises ValueError telling you to set allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true. With allow_dml=True it only logs a warning and executes.

Source

Thrown at lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py:374

                            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:
            # Unknown command — block by default unless DML is explicitly enabled
            if not self.allow_dml:
                raise ValueError(
                    f"NL2SQLTool blocked an unrecognised SQL command '{command}'. "
                    f"Only {sorted(_READ_ONLY_COMMANDS)} are allowed in read-only "
                    f"mode."
                )

    @staticmethod

View on GitHub (pinned to 754d7323be)

Solutions

  1. If the write is intended: NL2SQLTool(db_uri=..., allow_dml=True) or export CREWAI_NL2SQL_ALLOW_DML=true (env var also overrides at runtime).
  2. If not intended: fix the agent prompt to forbid writes and re-run with a SELECT.
  3. Point writes at a separate tool instance/connection with a write-capable, low-privilege DB user instead of flipping the shared tool.

Example fix

# before
tool = NL2SQLTool(db_uri=uri)
tool._run("DELETE FROM sessions WHERE expired")  # ValueError

# after
tool = NL2SQLTool(db_uri=uri, allow_dml=True)
tool._run("DELETE FROM sessions WHERE expired")
Defensive patterns

Strategy: validation

Validate before calling

WRITE_CMDS = {"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "TRUNCATE", "MERGE", "REPLACE"}

def first_keyword(sql: str) -> str:
    return sql.strip().lstrip("(").split()[0].upper().rstrip(";") if sql.strip() else ""

if not tool.allow_dml and first_keyword(sql) in WRITE_CMDS:
    raise PermissionError(f"write statement {first_keyword(sql)} not permitted")

Type guard

def is_read_only_sql(sql: str) -> bool:
    kw = sql.strip().lstrip("(").split()[0].upper().rstrip(";") if sql.strip() else ""
    return kw in {"SELECT", "SHOW", "DESCRIBE", "EXPLAIN"}

Try / catch

try:
    tool._run(sql)
except ValueError as e:
    if "read-only mode" in str(e):
        if is_authorized_write(sql):
            result = write_tool._run(sql)  # allow_dml=True instance
        else:
            raise

Prevention

When it happens

Trigger: Calling the tool with any plain write statement ('DELETE FROM users', "UPDATE t SET x=1", 'DROP TABLE t', 'INSERT INTO ...') in the default read-only configuration.

Common situations: Agents asked to 'clean up the table'; prompts that encourage DDL; shared demo tools left read-only on purpose while a script tries to seed data; forgetting that allow_dml defaults to False.

Related errors


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/aebf364fcc92dddb. Report an issue: GitHub.