Databricks GenAI Engineer: Building Agents with MLflow, Agent Framework and Genie
Agentic development on Databricks means authoring an agent in code with MLflow's ResponsesAgent interface, logging and deploying it with Mosaic AI Agent Framework, instrumenting it with MLflow Tracing, measuring it with offline evaluation before release and monitoring after release, and giving it governed access to structured data through Genie spaces. This lesson follows that life cycle: how to wrap and log an agent, how it authenticates to Databricks resources, what a deployment sets up, how evaluation differs from monitoring, and how a multi-agent system retrieves data through Genie or the Genie Conversation API.
On this page10 sections
- How do you author an agent with MLflow ResponsesAgent?
- How do you log an agent with Models from Code?
- How does a deployed agent authenticate to Databricks resources?
- How do you instrument an agent with MLflow Tracing?
- What does databricks.agents.deploy() set up?
- How do the evaluation and monitoring phases differ?
- Which judges need ground truth, and how do production traces become evaluation data?
- When should an agent use Genie instead of a retriever?
- How do you improve the answers a Genie space gives?
- How do you call the Genie Conversation API, and what permissions does it need?
- Wrap an existing agent in an MLflow ResponsesAgent, add streaming with predict_stream and pass per-request values through custom_inputs
- Log an agent with Models from Code (set_model, code_paths) and validate the logged artifact with mlflow.models.predict
- Choose between automatic authentication passthrough and on-behalf-of-user authorization, and declare every resource the agent needs
- Instrument agents with autologging, @mlflow.trace and mlflow.start_span, and explain what agents.deploy() sets up and how to roll out a new version
- Compare evaluation and monitoring, pick reference-free judges for monitoring, configure per-scorer sampling and turn production traces into evaluation datasets
- Decide when an agent should use a Genie space instead of a retriever, add it to a multi-agent system with GenieAgent and curate the space for accurate SQL
- Call the Genie Conversation API correctly (polling, follow-ups, sessions) and grant the permissions it needs
How do you log an agent with Models from Code?
Databricks recommends logging agents with Models from Code: put the agent in a Python file (for example agent.py) that ends with mlflow.models.set_model(agent), and pass that file path to log_model (python_model= for pyfunc, lc_model= for LangChain). MLflow stores the code, and when the model is loaded or served it restores the environment and re-executes the file.
This sidesteps pickling. Agents typically hold objects that cannot be serialised (a WorkspaceClient, a compiled LangGraph graph, open connections); logging an instantiated object fails on them, and workarounds such as cloudpickle tricks or serialising a graph to JSON are brittle. With Models from Code those objects are simply re-created when the file runs.
import mlflow
from mlflow.models.resources import DatabricksServingEndpoint
with mlflow.start_run():
info = mlflow.pyfunc.log_model(
name="agent",
python_model="agent.py", # a file path, not an object
code_paths=["helpers"], # local modules agent.py imports
resources=[DatabricksServingEndpoint(endpoint_name="my-llm-endpoint")],
pip_requirements=["mlflow", "databricks-langchain"],
)
log_model argument | Purpose |
|---|---|
python_model / lc_model | Path to the agent's code file (Models from Code) |
code_paths | Copies local files or packages the agent imports into the model artifacts and puts them on the Python path at load time |
pip_requirements | Published packages to install in the serving environment (not local folders) |
model_config | Settings the code reads at load time (prompt text, endpoint names), fixed for that version |
resources / auth_policy | Databricks resources and auth the deployed agent needs (next section) |
input_example | A sample request recorded with the model |
Only the code file is captured automatically. If agent.py imports a sibling package such as helpers, loading the logged model fails with ModuleNotFoundError until that folder is passed in code_paths.
Validate before you deploy
Calling the in-memory agent in a notebook proves only that the notebook works. mlflow.models.predict(model_uri, input_data=...) loads the logged artifact (optionally in a fresh environment built from its recorded requirements) and runs a sample request, which is exactly what serving will do. It catches missing modules, missing dependencies and load-time errors before registration and deployment.
How does a deployed agent authenticate to Databricks resources?
A deployed agent reaches other Databricks resources in one of two ways: automatic authentication passthrough, where the agent acts as a system identity with short-lived credentials for the resources you declared, or on-behalf-of-user (OBO) authorization, where it acts as the person who sent the request. Both avoid long-lived secrets; they differ in whose permissions apply.
| Automatic passthrough (system) | On-behalf-of-user | |
|---|---|---|
| Identity used | The serving endpoint's service principal | The requesting user |
| Declared as | Resources from mlflow.models.resources (or SystemAuthPolicy(resources=[...])) | UserAuthPolicy(api_scopes=[...]) |
| Every caller gets | The same access | Their own Unity Catalog permissions, row filters and audit trail |
| Client created | Anywhere | Inside predict / predict_stream, per request |
| Fits | Shared indexes, LLM endpoints, data users may not access directly | Per-user row filters, per-user audit requirements |
Automatic passthrough: declare every resource at log time
You list the resources the agent calls when you log it, for example DatabricksServingEndpoint, DatabricksVectorSearchIndex, DatabricksFunction (a Unity Catalog function tool), DatabricksGenieSpace, DatabricksTable and DatabricksSQLWarehouse. At deployment Databricks provisions short-lived credentials scoped to exactly those resources and rotates them. Two consequences follow:
- Undeclared means unreachable. A call to anything not on the list fails with a permission error, even if the LLM and retrieval calls in the same agent work. Granting rights to a person, or giving someone CAN MANAGE, does not change the credential's scope.
- The list belongs to the logged version. If you add a tool (say a UC function) after logging, redeploying the same version cannot help; log a new version that declares it and deploy that.
A Genie space is not self-contained: running its generated SQL needs the space's tables, the SQL warehouse and any UC functions it uses, so those must be declared alongside DatabricksGenieSpace.
On-behalf-of-user authorization
When results must respect each person's permissions (row filters, column masks, per-user audit), use OBO after a workspace admin has enabled it. The agent declares the REST API scopes it needs in a UserAuthPolicy and, inside predict, builds a WorkspaceClient with ModelServingUserCredentials (from databricks_ai_bridge). It must be created per request because the caller is known only then; a client built in __init__ has no user context and would otherwise reuse one identity for everyone.
The user's credential is restricted to the declared scopes, so a downstream call succeeds only if the scope is declared and that user can access the resource. If some users succeed and others fail through the same version, the scopes and code are fine; the failing users need access to the resource. Granting the service principal changes nothing for user-authorized calls.
Mixing both
One agent can do both: pass AuthPolicy(system_auth_policy=SystemAuthPolicy(resources=[...]), user_auth_policy=UserAuthPolicy(api_scopes=[...])) to log_model. Shared resources that users should not access directly (a documentation index, the LLM endpoint) go through system credentials; resources whose results must vary per person (a Genie space over row-filtered tables) go through user credentials.
How do you instrument an agent with MLflow Tracing?
You instrument an agent by enabling autologging for the frameworks it uses and decorating or wrapping the custom code that autologging cannot see. A trace is a tree of spans, each recording inputs, outputs, latency and exceptions for one step, and it is the primary tool for debugging wrong answers.
- Autologging (
mlflow.langchain.autolog(),mlflow.openai.autolog()and others) instruments every call into a supported library with one line: a span per LLM call, retriever and tool. Use the integration that matches the library actually called. @mlflow.traceturns a custom Python function into a span, nested in the same trace as the autologged spans. Aspan_typesuch asTOOLlabels what kind of step it is.with mlflow.start_span(...)creates a span for any block of code, such as a loop inside a larger function, where a decorator does not fit.
| Call | What it does | Creates a span? |
|---|---|---|
mlflow.<flavor>.autolog() | Instruments a supported library | Yes, for that library's calls only |
@mlflow.trace | Instruments one function | Yes |
mlflow.start_span() | Instruments a code block | Yes |
mlflow.update_current_trace() | Adds tags or metadata to the active trace | No |
mlflow.start_run(), log_metric, log_dict | Experiment tracking (runs, metrics, artifacts) | No |
Decorating only the top-level entry function gives a single span, which hides where time and errors occur. Calling autolog again inside a custom function adds nothing, because autologging never instruments non-library code.
What does databricks.agents.deploy() set up?
databricks.agents.deploy() takes a Unity Catalog model name and version and, in one call, sets up a scalable Model Serving endpoint with short-lived credentials for the declared resources, a Review App, real-time tracing to the agent's MLflow experiment, and AI Gateway inference tables (production monitoring hooks into the same traces). It does not create data assets such as Vector Search indexes or Genie spaces, and it does not fine-tune anything.
The Review App
The Review App is a web chat interface for stakeholders who do not use notebooks. They talk to the agent and give feedback, which is stored as assessments on the agent's traces in its MLflow experiment, and it also hosts labeling sessions where experts attach expected facts to records. AI Playground, the experiment UI and the endpoint's query page are tools for engineers, not structured feedback collection.
Rolling out a new version
To replace version 3 with version 4 on a live endpoint, call agents.deploy() again with the same UC model name and the new version. The existing endpoint is updated and traffic moves when the new version is ready, so the URL stays the same and there is no gap. The principle is that the endpoint is tied to the UC model name: keep the name and the version change happens in place, while anything that tears down or renames the endpoint interrupts clients. A UC alias such as champion is a pointer for people and pipelines; the endpoint serves the version it was deployed with until you deploy again.
Current-docs note: Databricks now also promotes building new agents with Agent Bricks and Databricks Apps; agents.deploy() to Model Serving remains documented and is the path the exam guide's "Agent Framework" refers to.
How do the evaluation and monitoring phases differ?
Evaluation answers "is this version good enough to ship?" and runs before release on a curated dataset; monitoring answers "is the live app still good?" and runs after release on sampled production traces. They use the same scorers, which is what makes their numbers comparable.
| Evaluation (offline) | Monitoring (production) | |
|---|---|---|
| When | Development, before each release; a release gate | Continuously after deployment |
| API | mlflow.genai.evaluate(data=..., predict_fn=..., scorers=[...]) | Scorers registered to the experiment with .register() and started with .start(sampling_config=...) |
| Inputs | A fixed evaluation dataset, often with expectations (expected facts or responses) | Whatever real users send; no expectations |
| Best for | Comparing versions fairly on identical inputs; ground-truth checks | Detecting drift, safety issues and new failure patterns on real traffic |
| Results | An evaluation run with per-row assessments | Assessments attached to each scored trace, rolled into dashboards |
Reuse your evaluation scorers in monitoring
Built-in judges, Guidelines scorers and custom scorers that ran in mlflow.genai.evaluate can be registered and started on the production experiment unchanged. The principle: a quality number is only comparable across phases if the same scorer definition produces it and it is applied to real traffic. Re-implementing a scorer elsewhere quietly changes what it measures, an offline dataset never shows how live users behave, and end-user feedback is a complementary signal rather than the same measurement.
Sampling and filtering
Each registered scorer is started with its own ScorerSamplingConfig:
sample_rate: the fraction of eligible traces scored. Use 1.0 for checks policy requires on every response (safety) and a small fraction (Databricks suggests roughly 0.05 to 0.2) for expensive LLM judges.filter_string: which traces are eligible, inmlflow.search_tracesfilter syntax (for example a tag such asenvironment = 'production'), so test traffic sharing the experiment is excluded.
Results appear as assessments on each evaluated trace in the experiment's Traces tab (initial processing can take a few minutes), not as new evaluation runs, inference-table columns or labeling tasks.
Comparing versions fairly
Monitoring scores whatever traffic each version receives. If version 6 got easier questions than version 5, its higher live score proves nothing. To decide between versions, run mlflow.genai.evaluate for both on the same labeled dataset with the scorer that measures the quality in question.
Which judges need ground truth, and how do production traces become evaluation data?
Some built-in judges compare an answer with expectations (expected facts, an expected response, per-row guidelines); others judge the trace on its own. Production traces carry no expectations, so only reference-free judges belong in monitoring, while ground-truth judges stay in offline evaluation over a labeled dataset.
| Judge | What it checks | Needs expectations? | Runs in monitoring? |
|---|---|---|---|
Correctness | Response vs expected facts or expected response | Yes | No (offline only) |
RetrievalSufficiency | Whether retrieved context contains the expected facts | Yes | No |
ExpectationsGuidelines | Response vs guidelines stored per row | Yes | No |
RelevanceToQuery | Response addresses the request | No | Yes |
Safety | Harmful content in the response | No | Yes |
RetrievalGroundedness | Response claims are supported by the retrieved context | No | Yes |
RetrievalRelevance | Retrieved chunks are relevant to the request | No | Yes |
Guidelines | Response vs rules fixed in the scorer | No | Yes |
Note the near-twins: groundedness compares the answer with the retrieved chunks (catches unsupported claims), relevance compares chunks or answer with the request, and sufficiency compares the context with expected facts. Using the answer as its own reference ("expected response = actual response") makes every answer pass and measures nothing.
Closing the loop: traces to evaluation datasets
Monitoring finds problems; evaluation datasets keep them fixed. MLflow evaluation datasets are Unity Catalog tables, created with mlflow.genai.datasets.create_dataset and governed and versioned like other UC assets. The loop is:
- Find problem traces with
mlflow.search_traces(filter by tag, assessment, time). - Add them with
dataset.merge_records(traces). This copies each trace's request (and response) into the dataset but adds no expectations unless the trace already carries expectation assessments (for example from an earlier labeling session); ordinary production traces have none. - Have domain experts add expected facts, typically in a labeling session in the Review App.
- Evaluate every future version against the dataset with
mlflow.genai.evaluate.
Until step 3 is done, Correctness cannot produce usable assessments for the new records, while reference-free judges such as Safety work immediately. The principle: a regression set is only useful if it is governed, versioned and re-run against every candidate version. Changing how much traffic monitoring scores, or keeping failing cases outside a managed dataset, does not give you that repeatable check.
When should an agent use Genie instead of a retriever?
An agent should use a Genie space for natural-language questions over governed, structured tables and a retriever (a Mosaic AI Vector Search index) for unstructured documents. Genie translates a question into SQL, runs it on a SQL warehouse under Unity Catalog permissions and returns exact results; similarity search returns the top few matching chunks.
| Need | Genie space | Vector Search retriever |
|---|---|---|
| Totals, counts, group-bys, filters | Exact: SQL over all rows | Wrong: the LLM sums a sample of retrieved rows |
| Ad hoc and follow-up questions | Generated SQL per question; conversation keeps context | Only finds similar text |
| Governance | Unity Catalog permissions and row filters apply | Index-level access |
| Policy documents, manuals, PDFs | Poor fit | Good fit |
The principle: aggregation needs every qualifying row, and similarity search by design returns only a few. Any variation that keeps retrieval in charge of structured data still asks an LLM to do arithmetic over a partial sample, or answers only the questions someone pre-computed. Unity Catalog SQL functions as tools work for known questions; a hand-built text-to-SQL tool is workable but you own its prompts, execution and conversation handling. For unpredictable questions with the least custom code, a Genie space is the fit. (Databricks' current docs also call Genie spaces "Genie agents"; the API paths still say spaces.)
Adding Genie to a code-based multi-agent system
In a LangGraph supervisor built in code, GenieAgent from databricks_langchain wraps a Genie space as a worker node. You create it with the space ID and a description, and it calls the Genie Conversation API for you (no polling code to maintain).
- Descriptions drive routing. The supervisor chooses workers by reading their descriptions; if none is given, the space's own description is used. With neither, data questions look no different from document questions and get misrouted. Say which data and question types the worker answers.
- One GenieAgent per space. A second space (say HR headcount, owned and permissioned by another team) becomes a second worker with its own description. Merging tables into one broad space breaks ownership and focus; Databricks recommends narrowly focused spaces.
Agent Bricks' no-code Multi-Agent Supervisor, which can also include Genie spaces, is covered with chains and tools in Section 1.
How do you improve the answers a Genie space gives?
You improve Genie's answers by curating the space itself: example SQL queries, SQL expressions for metrics, table and column descriptions with synonyms, and (as a last resort) text instructions. Genie generates SQL from the knowledge in the space, not from the prompt of the agent that calls it, so supervisor prompts, GenieAgent descriptions and post-processing in one agent cannot fix wrong SQL; they also leave every other team using the space with wrong answers.
| Problem | Curation that fixes it |
|---|---|
| Business term computed wrongly (net revenue treated as gross) | Define it in the space, ideally with example SQL or a SQL expression |
| Multi-step metric whose SQL varies between runs | Add the approved SQL as an example query; text instructions have proved insufficient |
Users' words do not match cryptic column names (qty_oh vs "stock on hand") | Add a column description and synonyms in the space's table settings; these are scoped to the space and do not overwrite Unity Catalog metadata |
| Questions misrouted to another worker | Not a space problem: fix the worker description used for routing |
Databricks' Genie best-practice guidance ranks the levers: complete example SQL for hard, multi-part questions; SQL expressions for reusable metric definitions; clear descriptions and synonyms; and plain-text instructions only when the structured options do not apply. Changing a shared UC column comment or adding a renaming view would work technically but alter shared metadata or add objects to maintain.
How do you call the Genie Conversation API, and what permissions does it need?
The Genie Conversation API is asynchronous: you start a conversation, poll the resulting message until it reaches a final status, then fetch the query result from its attachment.
POST /api/2.0/genie/spaces/{space_id}/start-conversationwith the question. The response holds a conversation ID and message ID; no result yet, because Genie is still generating and running SQL.GET .../conversations/{conversation_id}/messages/{message_id}repeatedly until the status is conclusive:COMPLETED,FAILEDorCANCELLED.- On
COMPLETED,GET .../messages/{message_id}/attachments/{attachment_id}/query-resultto read the rows. - Follow-ups ("and only for EMEA?") go to the same conversation with
POST .../conversations/{conversation_id}/messages, so Genie interprets them in context.
Fetching the result once straight after starting hits an unfinished message; re-sending the question starts new work each time.
Polling and conversation hygiene
- Poll every 1 to 5 seconds, back off exponentially up to one minute between polls for long queries, and stop after about 10 minutes with a timeout message. Short initial polls keep quick answers quick, backoff limits request volume and rate-limit errors, and the timeout bounds the user's wait.
- One conversation per user session. Starting a conversation for every message loses follow-up context; reusing one thread across days drags stale context along and can reduce accuracy (spaces also cap stored conversations). Start a new conversation when a new session begins.
Permissions: Genie runs SQL as the caller
Genie executes its generated SQL with the caller's identity on the space's SQL warehouse, so the caller (user or service principal) needs:
- CAN RUN (or higher) on the Genie space, and the Databricks SQL entitlement;
- CAN USE on the space's pro or serverless SQL warehouse;
- Unity Catalog
SELECTon the tables plusUSE CATALOGon the catalog andUSE SCHEMAon the schema (SELECT on a table alone is not enough; an error naming the catalog and schema points at the missing USE privileges).
CAN MANAGE on the space or warehouse, audit-table access and serving-endpoint permissions are not part of answering questions.
Because the caller's identity is what row filters evaluate, an app that sends every employee's question with one service principal (OAuth machine-to-machine) shows everyone that principal's rows. For per-person results and audit trails, authenticate each request with the employee's own OAuth user token (or, inside an agent, on-behalf-of-user authorization). Prompt text asking Genie to "filter by user" or per-department principals cannot enforce per-person access.
Tip. Expect scenario questions that describe an agent, a constraint and a symptom, then ask which change, configuration or call fits. Agent Framework items turn on how an agent is wrapped (ResponsesAgent, streaming, custom inputs), logged (Models from Code, code paths, validation), authenticated (declared resources versus on-behalf-of-user, including what a Genie space needs) and deployed (what one deploy call provides, rolling out a version without changing the URL). Life-cycle items contrast offline evaluation with production monitoring: which judges can run without expectations, how sampling and filtering are configured per scorer, and how production traces become labeled evaluation data. Genie items ask when structured data belongs in a Genie space rather than a retriever, how a supervisor routes to Genie workers, where a fix belongs (the space's curation versus the calling agent), and how the Conversation API is polled, continued and permissioned. Several options often work in principle; constraints such as no downtime, no long-lived credentials, per-user filters, budget or least code decide between them.
- ResponsesAgent wraps any framework's agent with an inferred signature and automatic tracing; streaming needs predict_stream yielding delta events and a final output_item.done event.
- Per-request extras go in custom_inputs; model_config is fixed when the logged version loads.
- Log agents with Models from Code (agent.py ending in set_model); local imports need code_paths; validate the logged URI with mlflow.models.predict.
- Automatic passthrough covers only resources declared at log time: a Genie space also needs its tables, SQL warehouse and functions, and adding a resource means logging a new version.
- On-behalf-of-user auth uses a UserAuthPolicy and a client created inside predict; it is the choice when row filters or audit must reflect each user.
- agents.deploy() creates the endpoint, credentials, Review App, tracing and inference tables; re-running it with the same model name and a new version updates the endpoint in place.
- Evaluation (mlflow.genai.evaluate on a curated dataset) gates releases; monitoring (registered scorers with per-scorer sample_rate and filter_string) watches live traces with the same scorers.
- Ground-truth judges (Correctness, RetrievalSufficiency, ExpectationsGuidelines) stay offline; production traces become regression data via search_traces, merge_records and expert labeling.
- Use Genie for exact answers over governed tables and a retriever for documents; GenieAgent descriptions drive supervisor routing, while the space's example SQL, descriptions and synonyms drive SQL quality.
- The Genie Conversation API is asynchronous and runs SQL as the caller, who needs CAN RUN, CAN USE on the warehouse and SELECT plus USE CATALOG and USE SCHEMA.
Frequently asked questions
What is the difference between ResponsesAgent and ChatAgent in MLflow?
Both are MLflow interfaces for wrapping agent code so Databricks can log, serve and trace it. ResponsesAgent is the current interface, built around a Responses-style request and output format with predict and predict_stream methods; ChatAgent is the older interface for the same purpose. New Databricks agents are authored as ResponsesAgent subclasses.
Why does Databricks recommend Models from Code for logging agents?
Agents usually hold objects that cannot be pickled, such as a WorkspaceClient or a compiled LangGraph graph. With Models from Code, the agent lives in a Python file ending in mlflow.models.set_model, and log_model receives the file path; MLflow re-executes the file when the model is loaded or served, so those objects are re-created instead of serialised.
When should a Databricks agent use on-behalf-of-user authorization instead of automatic passthrough?
Use on-behalf-of-user authorization when results must reflect each caller's own Unity Catalog permissions, such as row filters, or when audit logs must show the individual user. Automatic passthrough always acts as the serving endpoint's service principal with short-lived credentials for the declared resources, so every caller gets the same access. One agent can combine both in an AuthPolicy.
What is the difference between evaluation and monitoring for a generative AI application?
Evaluation runs before release: mlflow.genai.evaluate scores a candidate version over a curated, often labeled dataset so versions can be compared on the same inputs. Monitoring runs after release: scorers registered to the MLflow experiment score a sample of live production traces and attach assessments to them. Databricks recommends reusing the same scorers in both phases.
Can the Correctness judge be used for production monitoring?
Not meaningfully. Correctness compares a response with expected facts or an expected response, and live production traces carry no expectations. Monitoring uses reference-free judges such as Safety, RelevanceToQuery, RetrievalGroundedness and Guidelines, while Correctness runs offline on an evaluation dataset that experts have labeled, which can be grown with selected production traces.
When should an agent query a Genie space rather than a Vector Search index?
Use a Genie space for questions over structured, governed tables, especially aggregates, filters and ad hoc follow-ups: Genie generates SQL and runs it on a SQL warehouse under Unity Catalog permissions, so results are exact. Use a Vector Search retriever for unstructured content such as PDFs and manuals, where similarity search over text chunks is the right tool.
How does the Genie Conversation API return results?
Asynchronously. A start-conversation call returns conversation and message IDs; the client polls the message (every 1 to 5 seconds, backing off up to a minute and stopping after about 10 minutes) until its status is COMPLETED, FAILED or CANCELLED, then reads the rows from the attachment's query-result endpoint. Follow-up questions are posted to the same conversation.
What permissions does a service principal need to call the Genie Conversation API?
Genie runs its SQL as the caller, so the service principal needs CAN RUN on the Genie space, the Databricks SQL entitlement, CAN USE on the space's SQL warehouse, and Unity Catalog SELECT on the tables together with USE CATALOG and USE SCHEMA on their parents. If per-user row filters must apply, call the API with each user's own OAuth token instead of one service principal.
Source
This lesson covers the "Application Development" domain of the official Databricks GenAI exam guide. Vendors revise their guides — check the source for the current version.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.