Using MySQL as a Tool for n8n Agents - Flexible Queries without SQL Injection

07 Mar 2026 - tsp
Last update 07 Mar 2026
Reading time 8 mins

When I started using n8n in combination with the MySQL node I somewhat struggled with the documentation. I wished there was a single clear simple recipe describing how to give an n8n AI agent access to a MySQL database by providing a list of parameters, being able to write some filter code and specify the SQL statement yourself.

Most examples either:

The pattern described here works reliably in practice and allows an agent to query any allowed table in measurement databases flexibly while still maintaining proper boundaries.

Basic Idea

The concept is simple:

Never allow the LLM to generate raw SQL if your data should not be corrupted (though it is interesting to see how an LLM handles it’s own database when it gets arbitrary access, just make sure to sandbox the environment properly).

Create a Minimal Database User (Least Privilege)

If you don’t yet have a dedicated database user for n8n, create one with minimal permissions. For example:

CREATE USER 'n8nuser' IDENTIFIED WITH mysql_native_password BY 'REPLACE_ME';

GRANT SELECT ON exampledb.* TO 'n8nuser';
GRANT SELECT ON exampledb2.* TO 'n8nuser';

For most reporting or sensor databases SELECT is enough. Avoid granting statements with side effects like INSERT, UPDATE, DELETE, etc. unless you really need them. This is the same principle as for any web application - only grant the minimal required permissions.

Write a Strict Tool Description

You can let n8n auto-generate the tool description, but it is usually better to define it manually, especially when not restricting access in a restrictive manner. A strict and explicit description helps the agent understand the schema and also prevents hallucinated tables.

A short example could look like the following:

Execute a query in the historical measurement database (temperatures and humidities as well as information about the present sensors)

* table has to be one of the following table names (excluding the column descriptions):
   * humiditysensors(id, label, description)
   * temperaturesensors(id, label, description)
   * humidityvalues(ts, sensorid, humidity) are the humidity values of the sensors with sensor id sensorid at unix time ts.
   * temperaturevalues(ts, sensorid, temp) is the temperature of the sensor with sensorid (foreign key to temperaturesensors) at time ts (unix timestamp)

This does two things:

Defining Parameters with $fromAI

This has been there in nearly all tutorials but it was never said explicit enough for me to get it immediately. To define a parameter one is accessing it with the fromAI method using:

$fromAI('parametername', 'description', 'datatype', 'default')

The parameter is created the moment the expression is evaluated. Note that each parameter can only be defined once. This means one cannot use fromAI multiple times to access the same parameter. Instead one has to read it once, store it in an variable and use the variable afterwards. Trying to reuse $fromAI directly in multiple places fails the node.

Building the SQL Query

In the MySQL node you can enable Execute SQL and generate the query dynamically with JavaScript. This is powerful, but it is of course also prone for SQL injection. The safe pattern is:

The following shows an example implementation that allows the agent to access the tables mentioned above in an arbitrary fashion. In addition it will order the results from tables that include timestamps by timestamp in descending order:


SELECT 
{{
  (() => {
    // Read AI parameters only once
    const t = String($fromAI('table', 'The table name', 'string', '') || '')
      .trim()
      .toLowerCase();

    // Whitelist mapping
    const allowed = {
      humiditysensors: '`humiditysensors`',
      humidityvalues: '`humidityvalues`',
      temperaturesensors: '`temperaturesensors`',
      temperaturevalues: '`temperaturevalues`',
    };

    const tablesWithTs = new Set(['temperaturevalues', 'humidityvalues']);

    if (!allowed[t]) {
      throw new Error(`Disallowed or unknown table key: ${t}`);
    }

    const orderClause =
      tablesWithTs.has(t)
        ? ' ORDER BY `ts` DESC'
        : '';

    const selector =
      tablesWithTs.has(t)
        ? '*, FROM_UNIXTIME(ts) AS ts_readable'
        : '*';

    return `${selector} FROM ${allowed[t]}${orderClause}`;
  })()
}}
LIMIT 50;

What this does:

This approach prevents the agent from injecting arbitrary SQL fragments.

Always Add a LIMIT

Never allow unlimited queries. If you do, you will regret it. Also do not make it configurable in an unbounded fashion. Not even when it looks like it works on the first hand. If you forget the LIMIT, sooner or later the agent will try to read an entire table which typically leads to:

A safe default is something like:

LIMIT 50

For more advanced setups you can add pagination. Also do not forget to limit the maximum number of iterations that your agent can loop. Your financial account will thank you.

Security Rules You Should Actually Follow

Treat LLM input exactly like user input. This means:

LLMs are not malicious but they are very creative. And creativity plus databases without guardrails tends to produce unpleasant surprises.

Useful Extensions

Once the basic version works, you can extend the system safely. Typical improvements include:

This allows scaling the system without turning the SQL builder into a huge block of logic.

Conclusion

With a MySQL tool node, strict tool descriptions, $fromAI parameters and a whitelist-based SQL builder you can create a flexible agent-driven database interface without exposing your database to SQL injection. Just keep in mind:

Never trust LLM input, treat it like user input.

This article is tagged:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplainsQu98equt9ewh@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support