What databricks experimental aitools is, the five commands that matter, why it changes how coding agents and scripts talk to Databricks SQL, and the sharp edges I found while testing it.
Suppose you ask your coding agent a simple question: "Which pickup zip codes have the most taxi trips?"
The data is in a Databricks table, the agent has the Databricks CLI, it should take one command... right?
Without help, it takes many!
The agent lists the SQL warehouses to find an ID, it writes a JSON body for the Statement Execution API and it sends it with databricks api post.
It polls until the state is SUCCEEDED, then it parses result chunks or it skips all of that and writes a Python script with a SQL connector.
Each step is a place to make a mistake, and each mistake costs tokens and time.
The Databricks CLI now has a command group built for exactly this job. It is called aitools.
We run every important command against a real workspace, we look at the sharp edges, and then I explain why I think you should pay attention to it.
Databricks AI Tools in one minute
The Databricks CLI is the command-line tool for Databricks.
You install it on your laptop or in continuous integration (CI), and you sign in with a profile from ~/.databrickscfg.
Data engineers use it to deploy Declarative Automation Bundles, run Lakeflow Jobs, and call the Databricks REST APIs.
aitools is two command groups inside the Databricks CLI:
| Command group | What it does | Who uses it |
|---|---|---|
databricks aitools | Installs Databricks skills and plugins into coding agents | You, once per machine or project |
databricks experimental aitools tools | Runs SQL and reads table metadata, with output that an agent can parse | Your coding agent, your scripts, and you |
A coding agent is an AI assistant that runs commands in your terminal, such as Claude Code, Cursor, or Codex CLI.
A skill is a packaged set of instructions that tells the agent how to do a task. The Databricks skills come from the databricks-agent-skills repository.
A SQL warehouse is the Databricks compute that runs SQL and every tools command below needs one.
The Statement Execution API is the Databricks REST API that runs SQL on a SQL warehouse and the databricks api post is the Databricks CLI command that sends a raw POST request to a REST API, these two are the manual path from the story above.
The second group sits under experimental, and the Databricks CLI is honest about it:
CLI tools to be used by AI agents. These tools are optimized for AI coding agents like Claude Code and Cursor. The tools can change at any time. There are no stability guarantees for these tools. Keep that warning in mind. We come back to it in the negatives.
Our example
Everything in this guide uses one workspace profile and one public table:
Profile: aws-workspace Table: samples.nyctaxi.trips CLI: Databricks CLI v1.26.0 samples is a catalog that Databricks ships with every Unity Catalog workspace, so you can run every command here without creating data.
To keep the commands short, I define a small shell function first:
# ~/.zshrc (or paste into your terminal) ait() { databricks experimental aitools tools "$@"; } From now on, ait query ... means databricks experimental aitools tools query ....
Add -p <profile> if your profile is not the default.
Command 1: get-default-warehouse
Every tools command needs a SQL warehouse, if you do not give one, the Databricks CLI picks one for you.
To see which one it picks:
ait get-default-warehouse --output json { "id": "9810e746a2babb0b", "name": "Serverless Starter Warehouse", "state": "RUNNING" } That is all it does, and that is the point!
The agent does not need to list SQL warehouses and guess.
If you want every command to use a specific SQL warehouse, set an environment variable:
export DATABRICKS_WAREHOUSE_ID=9810e746a2babb0b Or pass --warehouse <id> on each query and statement submit command.
Command 2: query
This is the main command, it runs SQL, waits for the result, and prints it.
ait query " SELECT pickup_zip, count(*) AS trips, round(avg(fare_amount), 2) AS avg_fare FROM samples.nyctaxi.trips GROUP BY 1 ORDER BY 2 DESC LIMIT 3" When an agent runs it (a non-interactive terminal), the output is JSON:
[ { "avg_fare": "10.62", "pickup_zip": "10001", "trips": "1227" }, { "avg_fare": "10.98", "pickup_zip": "10003", "trips": "1181" }, { "avg_fare": "10.91", "pickup_zip": "10011", "trips": "1129" } ] The help text says that in an interactive terminal it renders a table, and large results open an interactive table browser.
I only ran it non-interactively, so I have not seen the browser.
Look at the values again: "1227" is a string, not a number, every value comes back as a string.
That is fine for an agent that reads the text, but a script that sums the column must cast it first.
Many ways to send SQL
query accepts SQL from four places:
ait query "SELECT 1" # positional argument ait query --file report.sql # a file (-f) ait query report.sql # a positional argument that ends in .sql and exists echo "SELECT 1" | ait query # stdin Parameters, not string building
This is the feature I like most!
Use :name markers in the SQL and bind values with --param:
ait query --output csv \ --param zip:INT=10001 \ --param since:DATE=2016-02-14 \ "SELECT tpep_pickup_datetime, fare_amount FROM samples.nyctaxi.trips WHERE pickup_zip = :zip AND tpep_pickup_datetime >= :since ORDER BY 1 LIMIT 3" tpep_pickup_datetime,fare_amount 2016-02-14T00:10:08.000Z,7.5 2016-02-14T00:17:38.000Z,23.0 2016-02-14T00:23:36.000Z,30.0 name=value binds a string, name:TYPE=value binds a typed value, such as DATE or INT.
The Databricks CLI sends an empty value as NULL, it does not support positional ? markers.
Why does this matter? An agent that builds SQL by gluing strings together can, one day, glue in a quote character from your data.
Parameters remove that whole class of bug.
--output csv is useful on its own, you can export a result to a file without a notebook:
ait query --output csv "SELECT * FROM samples.nyctaxi.trips LIMIT 1000" > trips.csv Many queries in parallel
Pass more than one SQL statement and the Databricks CLI runs them at the same time, up to --concurrency statements (default 8).
I passed two queries, the second one has a typo on purpose:
ait query \ "SELECT count(*) AS n FROM samples.nyctaxi.trips" \ "SELECT nope FROM samples.nyctaxi.trips" Error: multiple queries require --output json (got "text"); pass --output json to receive a JSON array of per-statement results The help text says "Multi-query output is always JSON".
In v1.26.0, it is not always JSON: you must ask for it, so add --output json:
ait query --output json \ "SELECT count(*) AS n FROM samples.nyctaxi.trips" \ "SELECT nope FROM samples.nyctaxi.trips" echo "exit=$?" [ { "sql": "SELECT count(*) AS n FROM samples.nyctaxi.trips", "statement_id": "01f1e012-704d-163a-b2d2-17ed3cd4c4f2", "state": "SUCCEEDED", "elapsed_ms": 1348, "columns": ["n"], "rows": [["21932"]] }, { "sql": "SELECT nope FROM samples.nyctaxi.trips", "statement_id": "01f1e012-7052-197a-a61c-dedd88e64047", "state": "FAILED", "elapsed_ms": 1276, "error": { "message": "[UNRESOLVED_COLUMN.WITH_SUGGESTION] A column, variable, or function parameter with name `nope` cannot be resolved. Did you mean one of the following? [`dropoff_zip`, `pickup_zip`, `fare_amount`, `trip_distance`, `tpep_dropoff_datetime`]. SQLSTATE: 42703; line 1 pos 7", "error_code": "BAD_REQUEST" } } ] exit=1 Three things to notice:
- The good query still returns its result: One failure does not hide the other answers.
- The error carries the suggestion: An agent reads "Did you mean
pickup_zip" and fixes its own SQL on the next try. - The exit code is
1: Any failed query makes the whole command fail, so CI checks and scripts can trust it.
Also, notice the shape changed!
A single query returns a list of objects ({"trips": "1227"}), a batch returns columns and rows as arrays.
If you parse the output in a script, you must handle both shapes.
Command 3: discover-schema
Before an agent writes SQL against a table, it needs to know the columns.
Without this command, it runs DESCRIBE, then a sample SELECT, then maybe a null check... that's three round trips per table!
discover-schema does all of it in one command:
ait discover-schema samples.nyctaxi.trips COLUMNS: tpep_pickup_datetime: timestamp tpep_dropoff_datetime: timestamp trip_distance: double fare_amount: double pickup_zip: int dropoff_zip: int SAMPLE DATA: Row 1: tpep_pickup_datetime: 2016-02-13T21:47:53.000Z tpep_dropoff_datetime: 2016-02-13T21:57:15.000Z trip_distance: 1.4 fare_amount: 8.0 pickup_zip: 10103 dropoff_zip: 10110 ... NULL COUNTS: total_rows: 21932 tpep_pickup_datetime_nulls: 0 ... For each table you get the column names and types, five sample rows, the null count per column, and the total row count.
Pass many tables in one call, use the full CATALOG.SCHEMA.TABLE name:
ait discover-schema samples.tpch.orders samples.tpch.customer samples.tpch.lineitem All the probes share one budget of --concurrency statements (default 8), however many tables you pass.
The help text says that Ctrl+C cancels the running statements on the server.
A small warning: the sample rows are real data. If a table holds personal data, the agent now has five rows of it in its context. Point the agent at tables it is allowed to see.
Command 4: statement submit, status, get, cancel
query waits for the result, that is what you want most of the time.
Sometimes you do not want to wait, a large OPTIMIZE, a long backfill, or a heavy report can run for minutes.
An agent that blocks on it cannot do anything else.
The statement commands split the work into steps:
| Command | What it does | Blocks? |
|---|---|---|
statement submit | Sends the SQL and returns a statement_id at once | No |
statement status <id> | Reads the current state one time | No |
statement get <id> | Waits until the statement finishes, then prints the result | Yes |
statement cancel <id> | Asks the server to stop the statement | No |
Here is the full cycle:
ait statement submit " SELECT pickup_zip, sum(fare_amount) AS total FROM samples.nyctaxi.trips GROUP BY 1 ORDER BY 2 DESC LIMIT 2" { "statement_id": "01f1e012-7467-1656-b3f5-73910324e0d2", "state": "PENDING", "warehouse_id": "9810e746a2babb0b" } STATEMENT_ID=01f1e012-7467-1656-b3f5-73910324e0d2 ait statement status "$STATEMENT_ID" ait statement get "$STATEMENT_ID" { "statement_id": "01f1e012-7467-1656-b3f5-73910324e0d2", "state": "SUCCEEDED" } { "statement_id": "01f1e012-7467-1656-b3f5-73910324e0d2", "state": "SUCCEEDED", "columns": ["pickup_zip", "total"], "rows": [["11422", "19209.5"], ["11371", "14798.84"]] } To stop a statement, use cancel, I submitted a slow self-join on the same table, then cancelled it at once:
ait statement cancel "$STATEMENT_ID" ait statement status "$STATEMENT_ID" { "statement_id": "01f1e01e-4981-1152-92c5-79b495d9e88a", "state": "CANCELED" } { "statement_id": "01f1e01e-4981-1152-92c5-79b495d9e88a", "state": "CANCELED", "error": { "message": "statement reached terminal state CANCELED" } } The first CANCELED is optimistic, the API returns no body on cancel, so the Databricks CLI assumes it worked. The second one comes from the server.
If you need to be sure, always run status after cancel, a statement get on a cancelled statement prints the same error and exits with code 1.
One difference is easy to miss: The help text says that Ctrl+C on query cancels the statement on the server, but Ctrl+C on statement get only stops waiting. The statement keeps running, and the SQL warehouse keeps working.
If an agent stops waiting, it must also run statement cancel.
Command 5: aitools install
The tools commands are useful, but the agent must know they exist, that is the job of the other command group.
I noticed this the first time I listed my profiles, the Databricks CLI printed a hint before the list:
Databricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install databricks aitools install finds the coding agents on your machine and installs the Databricks skills for them.
For Claude Code, Codex, and GitHub Copilot, it installs a plugin through the agent's own CLI, for the others (Cursor, OpenCode, Antigravity, Pi, Gemini CLI, Goose, Kiro), it writes raw skill files.
To install for one agent, in the current project only:
databricks aitools install --agents claude-code --scope project Useful flags:
| Flag | What it does |
|---|---|
--agents claude-code,cursor | Act only on the named agents |
--scope project or global | Install for this repository, or for your user |
--skills name1,name2 | Install only the named skills (with --skills-only or --path) |
--experimental | Include experimental skills |
--path <dir> | Write the skill files to a folder, so you can read them first |
The group also has list, update, uninstall, and version.
I did not run install for this article, if you want to know what the skills tell your agent before you trust them, use --path and read the files.
The sharp edges
So far so good... right?
Mostly... here is what can hurt you.
It runs any SQL, not only SELECT, the same command that reads a table can drop a catalog:
ait query 'DROP CATALOG IF EXISTS `my_catalog` CASCADE' The Databricks CLI does not ask "are you sure?". The only guard is the permissions of the identity behind the profile.
If an agent uses your admin profile, the agent has admin rights.
So give the agent its own identity, such as a service principal (an identity for automation, not for a person), and its own profile.
Grant it only what it needs to read:
GRANT USE CATALOG ON CATALOG main TO `agent-readonly`; GRANT USE SCHEMA, SELECT ON SCHEMA main.reporting TO `agent-readonly`; It also needs CAN USE on one SQL warehouse, with these grants, a DROP fails on the server, however the agent writes it. Then keep the agent's own approval rules for anything that is not a read.
Quote carefully in zsh and bash: Backticks inside double quotes run a shell command. Use single quotes around SQL that has backtick identifiers, as above.
It can start a SQL warehouse: If the default SQL warehouse is stopped, the first query starts it. That costs money. Pin a small SQL warehouse with DATABRICKS_WAREHOUSE_ID for agent work.
Values are strings in JSON: Cast before you do maths in a script.
Batches need --output json: At least in v1.26.0.
It is experimental: Flags, output shapes, and command names can change in any Databricks CLI release. Do not build a production pipeline on the exact JSON shape.
Why you should pay attention
I see three reasons.
- Databricks now treats coding agents as real users of the Databricks CLI: Databricks wrote these commands for agents, said so in the help text, and ships the skills that teach agents to use them. I read that as a signal about where the tooling is going. That part is my opinion, not a fact from the docs.
- One command replaces a fragile chain: Find a SQL warehouse, submit, poll, parse, handle errors:
querydoes all of it. Fewer steps means fewer wrong guesses and fewer wasted tokens. - It is useful without an agent. Parameter binding, CSV export, parallel batches with a correct exit code, and asynchronous statements are good tools for shell scripts and CI checks too.
Positives
- One command for "run this SQL and give me the result", with structured output.
- Named, typed parameters instead of string building.
-
discover-schemagives columns, samples, and nulls for many tables in one call. - Parallel batches return per-query results and a non-zero exit code on any failure.
- Asynchronous
statementcommands for long work. -
aitools installputs the instructions where the agent can find them.
Negatives
- Experimental, no stability guarantee, and I already found one gap between the help text and the behaviour.
- Two output shapes, and every value is a string.
- No built-in guard against destructive SQL, safety depends on your identity and agent permissions.
- Auto-detection can pick, and start, a SQL warehouse you did not expect.
- Sample rows from
discover-schemaput real data into the agent's context.
My default recommendation: install the skills in one project, pin a small SQL warehouse, give the agent a read-only profile, and let it use query and discover-schema for a week.
Wrapping up
If your coding agent talks to Databricks SQL through databricks api post or a one-off Python script, it is working too hard.
databricks experimental aitools tools gives it one command to run SQL, one to understand tables, and a small set to handle long work.
databricks aitools install tells the agent those commands exist.
Treat it as experimental, because it is, give it a narrow identity, because it can run any SQL.
If you already use it in CI or with a coding agent, please tell me in the comments what worked and what broke.
Then try one command today:
databricks experimental aitools tools discover-schema samples.nyctaxi.trips 
