Skip to content
v0.1.0 · in developmentApache 2.0Independent — not a Databricks product

Governed data.Plain English.Visible SQL.


An independent open-source .NET client, terminal application and automation toolkit for Databricks Genie Agents. Genie lives in the Databricks web UI. This puts it in your terminal, your scripts and your C#.

Generated SQL can be wrong in ways that read as correct. LakeSpeak keeps the statement, its bound values and the message ids, so you can check the answer yourself.

Installdotnet 10
dotnet tool install --global LakeSpeak.Cli
dotnet add package LakeSpeak.Genie
6
output formats
10
stable exit codes
6
test projects
0
credentials stored

You need a Genie Agent that already exists in your workspace and is shared with you. LakeSpeak cannot create one.

A terminal that
keeps the receipts

Real output, captured from the CLI against a live Databricks workspace. All three frames are on the page, so switching works with scripting off.

Choose a command to see its output
Genie is analyzing your question…Waiting for a SQL warehouse…Preparing answer…The total revenue by region is as follows:- **Germany:** €4,500,000.00- **France:** €3,350,000.50- **Spain:** €1,780,000.25Germany generated the highest revenue among the listed regions.╭────────────────────────┬─────────────────────────────────────────────────────╮ region total_revenue_eur ├────────────────────────┼─────────────────────────────────────────────────────┤ Germany 4500000.00 France 3350000.50 Spain 1780000.25 ╰────────────────────────┴─────────────────────────────────────────────────────╯╭─Generated SQL─────────────────────────────────────────────╮ SELECT `region`, SUM(`revenue_eur`) AS total_revenue_eur FROM `sales`.`public`.`regional_revenue` WHERE `region` IS NOT NULL AND `revenue_eur` IS NOT NULL GROUP BY `region` ╰───────────────────────────────────────────────────────────╯
Captured against a live workspace on 2026-08-01, with that workspace's identifiers replaced by synthetic ones. The answer, the rows, and the SQL Genie generated to produce them.
LakeSpeak.NET — independent, not a Databricks productAgent: Sales IntelligenceType /help for commands, /exit to leave.You: What is the total revenue by region?Genie is analyzing your question…Waiting for a SQL warehouse…Preparing answer…The total revenue by region is as follows: **Germany** generated €4,500,000.00,**France** generated €3,350,000.50, and **Spain** generated €1,780,000.25. Amongthese regions, Germany had the highest total revenue, while Spain had thelowest.╭────────────────────────┬─────────────────────────────────────────────────────╮ region total_revenue_eur ├────────────────────────┼─────────────────────────────────────────────────────┤ Germany 4500000.00 France 3350000.50 Spain 1780000.25 ╰────────────────────────┴─────────────────────────────────────────────────────╯/sql /result /exportYou: and break that down by quarterGenie is analyzing your question…Waiting for a SQL warehouse…Genie is analyzing your question…Preparing answer…The total revenue by region is broken down by quarter as follows:- **France**: €1,650,000.00 in 2025-Q4 and €1,700,000.50 in 2026-Q1- **Germany**: €2,100,000.00 in 2025-Q4 and €2,400,000.00 in 2026-Q1- **Spain**: €880,000.00 in 2025-Q4 and €900,000.25 in 2026-Q1Revenue increased from Q4 2025 to Q1 2026 in all regions, with Germany showingthe largest growth.╭────────────────────┬──────────────────┬──────────────────────────────────────╮ region quarter total_revenue_eur ├────────────────────┼──────────────────┼──────────────────────────────────────┤ France 2025-Q4 1650000.00 France 2026-Q1 1700000.50 Germany 2025-Q4 2100000.00 Germany 2026-Q1 2400000.00 Spain 2025-Q4 880000.00 Spain 2026-Q1 900000.25 ╰────────────────────┴──────────────────┴──────────────────────────────────────╯/sql /result /exportYou: /sql╭─Generated SQL────────────────────────────────────────────────────────────────╮ SELECT `region`, `quarter`, SUM(`revenue_eur`) AS total_revenue_eur FROM `sales`.`public`.`regional_revenue` WHERE `region` IS NOT NULL AND `quarter` IS NOT NULL AND `revenue_eur` IS NOT NULL GROUP BY `region`, `quarter` ORDER BY `region`, `quarter` ╰──────────────────────────────────────────────────────────────────────────────╯You: /exit
A two-turn session ending in /sql, captured 2026-08-06. Reflowed from 152 columns to 80 — the wording, figures and command sequence are exactly as the session produced them, the line breaks are not.
Profiles in .databrickscfg: 0Requesting a token via DATABRICKS_TOKEN…OK — obtained a token (3331 characters, not shown).OK — the workspace answered; 1 Agent visible.
The token is obtained and its length reported. It is never printed, and never written to disk.

Six calls, four id types, one polling loop

Today · by hand
  1. 01databricks genie list-spaces
  2. 02databricks genie start-conversation <space-id> "question"
  3. 03databricks genie get-message <space-id> <conversation-id> <message-id>
  4. 04databricks genie get-message-attachment-query-result ...
  5. 05databricks genie generate-download-full-query-result ...
  6. 06databricks genie get-download-full-query-result ...
With LakeSpeak
lakespeak ask--agent sales "How did revenue change last month?"

This is a narrow tool. It is not a Databricks SDK for .NET, and Databricks already ships managed MCP endpoints for Genie. What you could not do before was hold a conversation from C#, or run a set of questions on a schedule and get a document back.

→ Read the decision records

Down goes the question

LakeSpeak starts a conversation, polls until Genie reaches a terminal state, then collects the answer, the generated SQL, the bound parameter values and the result rows.
It never stores a credential, and cannot see anything your own Databricks identity cannot.
ask "revenue by region?"
A conversation starts against the Agent you named, resolved from an alias, an id, or an exact title.
start-conversation
LakeSpeak holds the Agent, conversation, message and attachment ids so you never have to.
generate SQL, run on a warehouse
Genie writes SQL against the curated tables behind the Agent, and executes it.
poll until terminal
Progress is reported in product terms: “Waiting for a SQL warehouse”, not FETCHING_METADATA.
answer + query + rows
Cells arrive as strings exactly as Databricks returned them. Nothing is reparsed on the way out.
stdout, plus stderr diagnostics
Results on stdout, progress and warnings on stderr, which is what makes --format json pipeable.
Message statesSubmittedThinkingPendingWarehouseExecutingQueryCompletedFailedCancelledQueryResultExpiredUnknownA closed enum with an Unknown arm, treated as non-terminal: a status Databricks adds next month must not crash a released client.

What it actually does

Eight things, each of which the repository can be opened to check.
01

Stateful conversations

chat keeps context, so “and break that down by quarter” works. Slash commands cover /sql, /result and /export.

02

Six output formats

text, table, markdown, json, jsonl and csv. Results on stdout, progress and errors on stderr.

03

The SQL, preserved

The statement, the values bound into it and the message ids are all kept, so a person can check an answer instead of trusting it.

04

No credential ever stored

Short-lived OAuth tokens are brokered through the Databricks CLI and held in memory for the life of the process.

05

Question Packs

A YAML pack turns a set of business questions into a version-controlled Markdown report, runnable on a schedule.

06

Contractual exit codes

0 success, 1 unexpected, 2 bad input, 3 auth, 4 permission, 5 not found, 6 Genie could not answer, 7 timeout, 8 partial pack failure, 9 malformed response.

07

Typed failures in C#

Every error surfaces as GenieException with a GenieFailureKind you can branch on.

08

Unity Catalog, untouched

You see exactly what your Databricks identity is permitted to see. LakeSpeak has no way to widen that.

Questions under version control

A Question Pack turns a set of business questions into a reviewable report. Declare them once, run the pack on a schedule, then read the Markdown in a pull request.

validate reports every problem at once, so you fix a pack in one pass.

lakespeak pack init my-brief.yaml
lakespeak pack validate my-brief.yaml
lakespeak pack run my-brief.yaml

→ Schema and failure semantics

daily-platform-brief.yaml
        apiVersion: lakespeak.dev/v1alpha1
kind: QuestionPack

metadata:
  name: daily-platform-brief
  description: Daily summary of Databricks platform health

spec:
  agent: platform-operations

  questions:
    - id: failed-jobs
      title: Failed production jobs
      ask: >
        Which production jobs failed during the
        last 24 hours? Include job name, failure
        time, and latest error category.

    - id: expensive-queries
      title: Most expensive queries
      ask: Which queries consumed the most compute yesterday?

  output:
    format: markdown
    path: reports/daily-platform-brief.md

  behavior:
    continueOnQuestionFailure: true
    includeGeneratedSql: false
    includeTimings: true
      

The keys and values are those of the real example file inexamples/question-packs/, wrapped to fit this column.

Failures you can branch on

The client validates its configuration when you first resolve it, not on the first call. Failures arrive as a GenieException carrying aGenieFailureKind you can switch on.

Cells reach you as strings, exactly as Databricks returned them. A DECIMAL never goes through a double on the way.

  • Authentication
  • Authorization
  • AgentNotFound
  • ConversationNotFound
  • MessageFailed
  • MessageCancelled
  • PollingTimeout
  • RateLimited
  • QueryResultExpired
  • QueryExecutionFailed
  • MalformedResponse
  • UnsupportedResult
  • Network
  • Unexpected

→ Runnable quickstart, built in CI so it cannot drift

Program.cs
        using LakeSpeak.Genie;
using Microsoft.Extensions.DependencyInjection;

services.AddLakeSpeak(options => options.Profile = "production");

var genie = provider.GetRequiredService<IGenieClient>();

try
{
    var response = await genie.AskAsync(
        agentId: salesAgentId,
        question: "Which customers had the largest revenue decline?",
        cancellationToken: cancellationToken);

    Console.WriteLine(response.Text);

    // The SQL, so a human can check the answer.
    if (response.Query is { Sql: { } sql })
    {
        Console.WriteLine(sql);
    }
}
catch (GenieException ex) when (ex.Kind == GenieFailureKind.Authorization)
{
    // Fixed with a Databricks grant, not in code.
}
      

Compiled against LakeSpeak.Genie from nuget.org on every pull request, so this cannot quietly stop being real code.

What it does not do

Four things this will not do for you.
Governance

It does not bypass Unity Catalog. You see exactly what your Databricks identity is permitted to see, and LakeSpeak has no way to widen that.

Correctness

It does not guarantee a Genie answer is correct. Generated SQL can be wrong in ways that read as plausible, which is why the SQL, the source metadata and the message ids are preserved.

Write access

It does not execute arbitrary SQL, edit generated SQL, or modify Genie Agent definitions.

Retention

It does not store your questions, answers or query results anywhere except files you explicitly export.

Documentation index

Nothing to a working answer in ten minutes

Install, log in with the Databricks CLI, find your Agent, ask something. Longer if someone else still has to create the Agent for you.