There's an awkward moment in every agentic data project. The agent works. It writes decent SQL, it reasons about schema, it proposes a migration that looks right. And then somebody asks the question nobody wants to answer: what happens when it's wrong against production?

The usual answers are all bad.

Point it at production with read-only credentials, and you've capped the damage but also capped the agent — it can diagnose and never fix. Point it at a seeded staging database, and you get an agent that's confidently correct about data that stopped resembling production four months ago. Spin up a container per run, and you're maintaining fixture pipelines forever while paying for compute that sits idle between runs.

What you actually want is a full copy of production that appears in under a second, costs almost nothing while it exists, and vanishes without a trace when the agent is done. Which is to say: you want git checkout -b for your database.

Databricks Lakebase does this, and the mechanism is interesting enough to be worth understanding rather than just enabling.

How Lakebase gets there

Lakebase is Postgres — genuinely Postgres, not a wire-compatible reimplementation. What Databricks changed is the part underneath.

Databricks lakebase description

Standard Postgres is a monolith with compute and storage tightly coupled. Databricks decoupled the two and moved storage onto lake storage, which creates an immediate problem: object storage has high latency and no transactional consistency, and Postgres assumes neither. Two components patch the gap. Safekeepers, built on the Paxos consensus algorithm, handle low-latency durable writes. Page servers handle low-latency reads, materializing pages on demand from the lake.

Unity description

Once storage is decoupled and page-addressed, branching becomes almost free. It's copy-on-write: the data stays in one place on the lake, and Lakebase tracks only the deltas between branches. Writing to a branch records the change independently without touching the others.

The numbers that matter for agent work: a branch takes roughly 500 milliseconds, a new instance comes up in under 500 milliseconds, rollback to an earlier snapshot is equally fast, and idle branches scale to zero so you pay only cheap lake storage while they sit around. Databricks reports 12 million database launches per day in production, which is less a benchmark than an indication that ephemeral instances are the intended usage pattern rather than an edge case.

That last property is what makes this viable for agents specifically. As Ali Ghodsi put it on the summit keynote stage, agents "don't want to wait 10 minutes on a database to come up." An agent that has to wait ten minutes for an environment will simply be given production instead, because the alternative makes the loop unusable. Sub-second provisioning is what makes the safe option also the convenient one.

Isolation strategies, compared

Approach Provisioning time Data realism Idle cost Blast radius of a bad write Ongoing maintenance
Read-only prod credentials None Perfect None Zero — but agent can't act None
Full prod write access None Perfect None Catastrophic None
Seeded staging database Minutes to hours Degrades continuously Full instance cost Contained, but shared between runs High — fixture pipelines, refresh jobs
Container per run with dump restore Minutes, scales with data size Good at restore time None between runs Contained per run Medium — dump/restore tooling
Lakebase branch per run ~500 ms Identical to parent at branch time Near zero (storage deltas only) Contained per run, discardable Low

The column that usually decides it is provisioning time, because it governs whether developers actually use the isolated path or route around it under deadline pressure.

Building the loop

Here's the pattern end to end. The Databricks-specific surface is deliberately isolated into one thin layer; everything else is ordinary Postgres, because that's what it is.

Step one — get an instance. Lakebase is now included in Databricks Free Edition alongside Genie Code, serverless GPUs, Agent Bricks and LakeFlow Designer, so this costs nothing to follow along with.

# lakebase_control.py — the only Databricks-specific layer in this design.
# Endpoint shapes for branch operations are still settling; check current
# docs before shipping. Everything downstream of get_dsn() is plain Postgres.

import os
import time
import requests

WORKSPACE = os.environ["DATABRICKS_HOST"].rstrip("/")
TOKEN = os.environ["DATABRICKS_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}


def create_branch(parent: str, name: str) -> dict:
    """Branch an existing Lakebase instance. Copy-on-write; returns in ~500ms."""
    started = time.perf_counter()
    resp = requests.post(
        f"{WORKSPACE}/api/2.0/database/instances/{parent}/branches",
        headers=HEADERS,
        json={"name": name},
        timeout=30,
    )
    resp.raise_for_status()
    elapsed_ms = (time.perf_counter() - started) * 1000
    print(f"branch {name!r} ready in {elapsed_ms:.0f} ms")
    return resp.json()


def delete_branch(parent: str, name: str) -> None:
    requests.delete(
        f"{WORKSPACE}/api/2.0/database/instances/{parent}/branches/{name}",
        headers=HEADERS,
        timeout=30,
    ).raise_for_status()


def get_dsn(instance: str, branch: str) -> str:
    """Standard Postgres connection string for a branch."""
    resp = requests.get(
        f"{WORKSPACE}/api/2.0/database/instances/{instance}/branches/{branch}",
        headers=HEADERS,
        timeout=30,
    )
    resp.raise_for_status()
    host = resp.json()["read_write_dns"]
    return f"postgresql://{os.environ['LAKEBASE_USER']}:{TOKEN}@{host}:5432/databricks_postgres?sslmode=require"

Enter fullscreen mode Exit fullscreen mode

Step two — wrap it so the agent physically cannot see production. This is the important part, and it's boring on purpose. The agent never receives a DSN; it receives a session bound to a branch.

# agent_sandbox.py
import contextlib
import psycopg
from lakebase_control import create_branch, delete_branch, get_dsn

PROD_INSTANCE = "orders-prod"


@contextlib.contextmanager
def branched_session(run_id: str, keep_on_failure: bool = True):
    """
    Yield a Postgres connection on a throwaway branch of production.

    On success the branch is discarded. On failure it is retained so you can
    attach a psql session and see exactly what the agent did — which is the
    single most useful debugging affordance in this whole design.
    """
    branch = f"agent-{run_id}"
    create_branch(PROD_INSTANCE, branch)
    conn = None
    try:
        conn = psycopg.connect(get_dsn(PROD_INSTANCE, branch), autocommit=False)
        yield conn
        conn.commit()
        delete_branch(PROD_INSTANCE, branch)
    except Exception:
        if conn:
            conn.rollback()
        if not keep_on_failure:
            delete_branch(PROD_INSTANCE, branch)
        else:
            print(f"retained branch {branch!r} for inspection")
        raise
    finally:
        if conn:
            conn.close()

Enter fullscreen mode Exit fullscreen mode

Step three — verify before you believe anything. An agent reporting success is not evidence. Snapshot the invariants you care about before and after, and diff them.

# verify.py
from dataclasses import dataclass

INVARIANTS = {
    "order_count":        "SELECT count(*) FROM orders",
    "open_order_total":   "SELECT coalesce(sum(total_cents), 0) FROM orders WHERE status = 'open'",
    "orphaned_items":     "SELECT count(*) FROM order_items i "
                          "LEFT JOIN orders o ON o.id = i.order_id WHERE o.id IS NULL",
    "negative_totals":    "SELECT count(*) FROM orders WHERE total_cents < 0",
    "duplicate_skus":     "SELECT count(*) FROM ("
                          "  SELECT sku FROM products GROUP BY sku HAVING count(*) > 1"
                          ") d",
}


@dataclass
class Snapshot:
    values: dict

    @classmethod
    def take(cls, conn) -> "Snapshot":
        out = {}
        with conn.cursor() as cur:
            for name, sql in INVARIANTS.items():
                cur.execute(sql)
                out[name] = cur.fetchone()[0]
        return cls(out)

    def diff(self, other: "Snapshot") -> dict:
        return {
            k: (self.values[k], other.values[k])
            for k in self.values
            if self.values[k] != other.values[k]
        }


def assert_no_corruption(before: Snapshot, after: Snapshot) -> None:
    """Hard invariants must not move regardless of what the task was."""
    for guard in ("orphaned_items", "negative_totals", "duplicate_skus"):
        if after.values[guard] != before.values[guard]:
            raise AssertionError(
                f"invariant {guard} violated: "
                f"{before.values[guard]} -> {after.values[guard]}"
            )

Enter fullscreen mode Exit fullscreen mode

Step four — the run itself.

# run_agent_task.py
import uuid
from agent_sandbox import branched_session
from verify import Snapshot, assert_no_corruption

def run(task: str, agent) -> dict:
    run_id = uuid.uuid4().hex[:8]

    with branched_session(run_id) as conn:
        before = Snapshot.take(conn)

        # The agent gets a live connection to real-shaped data and full write
        # access — to a branch that exists only for this run.
        agent.execute(task, conn)

        after = Snapshot.take(conn)
        assert_no_corruption(before, after)

        return {
            "run_id": run_id,
            "changed": after.diff(before),
        }

Enter fullscreen mode Exit fullscreen mode

The full cycle

full cycle description

This isn't a pattern I invented for the article — it's the shape Databricks uses internally for Genie ZeroOps, its autonomous pipeline-remediation agent. ZeroOps investigates a failure, drafts a fix, then creates shallow clones of production data using the same branching mechanism as Lakebase, deploys the proposed fix to the clone, verifies row counts, and presents the result as a pull request. Nothing reaches production without explicit human approval. The verification-on-a-clone step is what makes autonomous remediation defensible rather than reckless, and it's available to you as a primitive.

What branching does not solve

Worth being direct about, because the isolation is narrower than it feels.

Side effects escape the branch. Your database is isolated. The payment API your agent calls is not. Neither is the email it sends, the Slack message it posts, or the Kafka topic it publishes to. A branch protects state you own in Postgres and nothing else — every external integration needs its own sandboxing, and this is where teams get burned after assuming the branch covered them.

Branch sprawl is real. Sub-second creation plus retain-on-failure produces a lot of branches. Enforce TTLs from day one, tag branches with the run that created them, and put a reaper on a schedule. Copy-on-write storage is cheap, not free, and a branch that diverges heavily from its parent stops being cheap.

Fresh at branch time, stale immediately after. A long-running agent task is reasoning about a snapshot. If it takes twenty minutes and the underlying question was time-sensitive, the conclusion may be wrong by the time it lands — not because the isolation failed but because you asked a real-time question of a point-in-time copy.

Invariant checks only catch what you thought to write down. The assert_no_corruption function above is a floor, not a ceiling. It catches structural damage. It won't catch an agent that correctly updated the wrong customer's record, because that's semantically valid and structurally clean. Human review of the diff remains load-bearing.

The broader shift

The reason this matters beyond Databricks is that it closes a gap that has quietly constrained every agentic data project: agents were either safe or useful, and picking both required infrastructure most teams weren't going to build.

Application code got this right decades ago. Nobody tests a refactor by editing production source — you branch, you break things freely, you merge what works. Databases never got that affordance because copying one was expensive enough that the ergonomics never arrived. Decoupling compute from storage is what finally made the copy cheap, and cheap copies are what make experimentation safe.

Give the agent a branch. Let it break things. Read the diff.


Lakebase branch APIs are evolving; the control-plane calls above are structurally accurate but verify endpoint and field names against current Databricks documentation before running in anger. Everything downstream of the DSN is standard Postgres and will behave exactly as you expect.

References

  1. "Lakebase — Serverless Postgres on the Lakehouse." Databricks — https://www.databricks.com/product/lakebase
  2. "Introducing Genie ZeroOps." Databricks Blog — https://www.databricks.com/blog/introducing-genie-zeroops
  3. Marattha, P. "Databricks Data + AI Summit 2026 recap: Genie One, LTAP, Lakehouse//RT and every major launch." Flexera Blog, June 30 2026 — https://www.flexera.com/blog/perspectives/databricks-data-ai-summit-2026/
  4. "Databricks Launches LTAP, the First Lake Transactional/Analytical Processing Architecture." Databricks Newsroom — https://www.databricks.com/company/newsroom/press-releases/databricks-launches-ltap-first-lake-transactionalanalytical
  5. "Databricks Sandbox — Serverless Compute Documentation." Databricks Docs — https://docs.databricks.com/aws/en/compute/serverless/sandbox
  6. "Introducing Genie One, Genie Ontology, and Genie Agents." Databricks Blog — https://www.databricks.com/blog/introducing-genie-one-genie-ontology-and-genie-agents