ZeitMind now has platform-wide branching, so our agent builds whole applications in one run
The Tech Behind ZeitMind

ZeitMind now has platform-wide branching, so our agent builds whole applications in one run

Marvin Bornstein
August 10, 2026
9 min read

Everything on a data platform builds on something else: the table backs the chart, the chart sits in an app, a filter feeds a query. If a human has to approve every individual change along that chain, you do not get an AI-enabled workflow, you get a slower human one. So we branched the entire platform: metadata, application data, and customer data, copied lazily on write. The agent now works in strides of hundreds of steps, and the user reviews one diff at the end, rendered as tables and charts rather than code.

Every change propagates through the layers

Data applications come with a long chain of dependencies: data enters through a connection layer, gets shaped in an integration layer (staging, derived, and incremental tables), and is consumed by two layers side by side, analytics (queries, charts, insights) and applications (apps, filters, actions).

Figure 1: the four platform layers, connection, integration, analytics and application
Figure 1: a new column at the source is five dependent writes, each one waiting on the one before it.

Pull one new column from a source system into a dashboard and the chain is: source extraction → staging table → derived table → chart → app. Five dependent writes, in order, and each is only verifiable after the previous one is applied: the derived table's new query does not even parse until the staging table has the column.

Gate each write behind a human approval and the agent either stalls the chain on five approvals in the right order, or writes the rest blind. We ran that approval-gated version until the end of 2025. For exploration, read-only work, it was fine. For modifications, the verify-as-you-go loop that makes agents useful was unavailable for exactly the task that needed it most.

Everything on the platform has to branch

The fix is conceptually obvious, and every software engineer already knows it: a branch. Apply everything, verify everything, review once, merge. The hard part is that a data platform is not a git repository. It is two very different kinds of state, and both have to branch.

Soft branching: the metadata is a diff, not a copy

Table definitions, visualizations, inputs, applications, tabs: every resource type on the platform. A branch does not copy any of it. It stores a diff against production in which every touched resource is either created, modified, or deleted (a tombstone); everything else falls through to production. The frontend applies this diff as an overlay inside the data-loading hooks, so components are branch-unaware. The backend holds the same diff in a branch session fed by realtime subscriptions; frontend and backend never coordinate beyond agreeing which branch is active. The work is not in the overlay, it is in the coverage: every resource type, every mutation path, every realtime subscription has to respect the diff, or the branch silently leaks into production.

Hard branching: customer data is copy-on-write

The metadata describes materialized Postgres tables, gigabytes of them, with dashboards reading them continuously. Duplicating the whole warehouse per branch is not feasible, so a branch only copies what it actually writes. Each branch gets its own Postgres schema, empty at first. Nothing is materialized until the first write, and untouched tables are read from production. Query resolution happens inside the database: queries on a branch resolve table names against the branch schema first and fall back to production, so the same query text runs unchanged on any branch.

Figure 2: written resources resolve against the branch schema, untouched ones fall through to production
Figure 2: read what you touched from the branch, everything else from production.

Copy-on-write gets harder as the table types get more stateful. Materialized query tables are the easy case: rebuild them into the branch schema. Incremental tables carry watermarking state that must be isolated per branch and reset when the query template changes. And transactional tables that users edit in place cannot be handled by rebuilding at all: the branch takes a copy on first write, records every structural change made on the branch, and at merge time replays those changes against production as migrations, so live usage is never disrupted and rows written to production while the branch was open survive.

One review at the end instead of thirty along the way

On a branch, the agent walks the whole chain: edit the staging table, rebuild it, verify the derived table's query against the branched result, continue up through chart and app, executing every step to check it. We removed the per-write approval and uncapped the loop. Typical modification runs land between 100 and 300 iterations, and the agent has the full toolset a forward-deployed engineer has on the platform, 45 branch-scoped tools, from creating tables to assembling applications. In fact, every conversation with the agent runs on its own branch: whatever happens in a conversation stays in that conversation unless the user decides to keep it.

This changed how we work with customers, not just how the agent works:

  • We build example applications live in a call. A workflow the customer assembles manually from Excel every month becomes an automated pipeline plus app in about fifteen minutes, and they watch it run on their own data before committing to keep any of it.
  • Sometimes the stride starts in the taxi to the meeting: we send the agent off with nothing but a summary of the meeting notes, and review the result when we arrive.
  • Users break things on purpose. A branch is a free sandbox on real data: undo is "delete the branch", so people let the agent attempt long shots they would never risk in production.

The review at the end is not a code review. Because the platform knows what every resource is, the diff is rendered in the user's own concepts: a table shows before and after side by side, a chart shows the old rendering above the new one. A controller can judge "quantities are now formatted, prices carry currency, one value corrected" from the diff directly; nobody has to read SQL to approve a merge.

Diff pair A: before and after of the pos_sales_order_item table
Figure 3: the table diff. Quantities are formatted, prices carry currency, one value corrected.
Diff pair B: before and after of the weekly work duration chart
Figure 4: the visualization diff. Absolute stacked bars become share of total.

The clearest trace of the unlock is our token bill. We released branching with the full toolset in early 2026, after running the approval-gated flow until the end of 2025. Monthly LLM input tokens have grown roughly 30x since the release, because the agent now does long independent work instead of waiting at approval gates. The sharp step in July is not all branching: it also reflects our move from GPT to Claude (Opus and Fable), which uses noticeably more input tokens per task. Model improvements are in that curve too, but we could not have captured them at all with the agent gated behind per-step approvals.

Figure 5: monthly LLM input tokens from November 2025 to August 2026
Figure 5: monthly LLM input tokens, November 2025 to August 2026. Branching shipped in early 2026, and the July step also reflects the move to Claude.

The merge is where the complexity sits

The core branching machinery (spec types, branch session, overlay, frontend context) is about 1,500 lines. The merge is roughly the same size again, and it is where everything we got wrong surfaced.

Merging is not "copy the branch over". Production kept moving while the branch was open, so branch-materialized data may be stale, and promoting it directly would silently serve old rows. The merge instead commits the metadata diff, moves or replays the data changes, and rebuilds the affected pipeline from production data:

merge(branch → production):
  1. take over conflicting build jobs            // see below
  2. apply deletions, move branch-built tables into production
  3. commit the metadata diff in dependency order
  4. replay recorded migrations for tables edited in place
  5. rebuild the affected downstream subgraph from production data

Step 1 exists because early merges could be painfully slow. Moving a table into production requires an exclusive lock, and a running pipeline build holds a shared lock on its inputs for its entire duration, so a merge could queue behind a build for the better part of an hour, and the waiting exclusive lock in turn holds up new readers of that table. The merge now cancels conflicting builds first, queues the rebuild under its own job, and carries a 60-second lock timeout as a backstop, so an uncancellable lock holder produces a clean error instead of a hung merge.

The other learning is where the effort goes. Branch creation is instant and boring. Everything difficult lives at the boundaries: the moment a branch first writes to a stateful table, and the moment it merges back into a moving production system.

Conflict detection, branch retention and merge governance are next in line

Merges do not detect conflicts yet: if production and a branch change the same table, last write wins, and the diff review is the safety net. Abandoned branches still accumulate schemas we clean up manually. And branching created a new permissions question we did not have before: users who were never meant to be builders now happily build on their own branches, and they should be able to, but who gets to merge, and under which guarantees that a merge cannot break anything downstream, is a product problem we are still working out.

Marvin Bornstein
August 10, 2026
9 min read