07 Mar 2026 - tsp
Last update 07 Mar 2026
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:
SELECT, INSERT and UPDATE methods which are restricted with
respect to arbitrary table access, arbitrary ordering and similar ideasThe 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.

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).
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.
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:
$fromAIThis 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.
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:
temperaturevalues)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:
SHOW TABLES statement and passing its result of course).ts DESC.ts_readable) is generated using FROM_UNIXTIME().This approach prevents the agent from injecting arbitrary SQL fragments.
LIMITNever 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:
n8n workers blockingA 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.
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.
Once the basic version works, you can extend the system safely. Typical improvements include:
sensorid parameters and parse it strictly as an integer.WHERE ts BETWEEN ... AND ... but enforce maximum time spans.AVG(temp) GROUP BY hour again through explicit whitelists.This allows scaling the system without turning the SQL builder into a huge block of logic.
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:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplainsQu98equt9ewh@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/