Schema-as-Code Deployment With CHKit for Agent-Managed Pipelines

CHKit brings schema-as-code discipline to analytics databases running autonomous agent pipelines.

· 13 min read
Cover illustration for “Schema-as-Code Deployment With CHKit for Agent-Managed Pipelines”
MCP & Databases · September 19, 2026 · 13 min read · 2,895 words

Schema-as-code deployment means treating database structure as version-controlled, testable code instead of hand-run SQL. CHKit brings that discipline to an analytics database engine, and the timing matters most for agent-managed pipelines, where an AI system, not a person, is proposing and applying structural changes on its own schedule.

Relational teams have had this figured out for years. Change a TypeScript schema file, a tool diffs it against the last known state, generates the SQL, applies it, and fails the build if something drifted. Tools like Drizzle, Prisma, and Flyway made that workflow boring, and boring is the point: boring means safe. Analytics database teams mostly skipped that step. Migrations live in a folder that may or may not match what's running in production. Diffs get eyeballed. Someone runs a manual ALTER during an incident and never commits it anywhere.

That gap isn't a quirk, it's a liability with a body count. A migration drops a column, nobody catches it in review, it ships on a Friday, and the data is gone before Monday's standup. There's no single source of truth either: to know what a table actually looks like, someone queries the database itself rather than reading the code that's supposed to describe it. That's backwards, and it was only tolerable because analytics workloads used to be a side project. They're not anymore. As columnar databases built for analytical workloads become the default for production analytics, the tooling gap with the relational world turns into the thing that loses someone their data.

What agent-managed pipelines add to the schema problem

Human schema workflows move at the speed of a pull request. Someone proposes a change, a reviewer reads it, maybe asks a question, then it merges. That friction is annoying, but it's also a safety mechanism, and it's the first thing that disappears when an agent takes over.

An agent can propose and apply a schema change in the time it takes a human to open their laptop, and it can do that across a dozen pipelines in parallel. The feedback loop that used to take hours now takes seconds. Is that a massive efficiency gain or a massive risk? It depends on what sits between the agent's intent and the database actually changing, and most analytics tooling has nothing sitting there.

Start with the interface itself. If a CLI command returns a raw string of DDL text, an agent has nothing structured to act on. It can't cleanly decide whether to retry, escalate, or proceed, because there's no machine-readable signal telling it what just happened or what's about to happen. Agents need responses they can parse and route through logic, not prose they have to interpret.

Accountability gets harder too. If an agent drops a column or restructures a materialized view while nobody's watching in real time, that action needs to trace back to something concrete: which migration ran, which agent invocation triggered it, what state the system was in when it acted. Without that trail, an autonomous change is just an unexplained one, and unexplained changes to production data get people fired.

Agents also tend to be aggressive users of any interface they're given. They probe, they validate, they might fire off several commands in parallel just to check state before acting. Tooling built for a human typing one command at a time, waiting for a prompt, doesn't hold up under that kind of load. It needs to be non-interactive and scriptable by default, not scriptable as an afterthought bolted onto an interactive tool.

Then there's an engine-specific trap that's easy to miss until it costs someone real data. Some table engines used for deduplication only actually deduplicate at merge time, not at insert time. If an agent wires a materialized view to read from that kind of source table, and the view fires on every insert (which is how materialized views work), it processes rows before the underlying merge has deduplicated anything. Duplicates get permanently baked into an aggregate, and there's no fixing it after the fact without reprocessing. That's a structural property of how the engine behaves. Schema validation ought to catch that pattern before it ever reaches "apply," not after the aggregate is already wrong.

One more wrinkle: managed cloud environments sometimes rewrite the engine someone declared into a different variant under the hood, while self-hosted replicated clusters behave differently again. An agent deploying the same schema definition across a local environment, a cloud environment, and a self-hosted cluster needs something handling that mismatch automatically. Otherwise every environment becomes a special case the agent has to memorize, which is precisely the kind of thing agents are bad at catching on their own.

CHKit's TypeScript schema DSL and what agents can do with it

CHKit is listed as beta on its GitHub repo, with the CLI surface and schema DSL described as stable and already running production workloads. Beta means the edges are still being sanded down while the core contract holds, not that the tool is experimental. It means the edges are still being sanded down while the core contract holds.

The central idea: tables, views, materialized views, and dictionaries are TypeScript values. Not SQL strings sitting in a file, not YAML, not HCL. Actual typed objects that get checked by the compiler before they go anywhere near a database.

A table definition in CHKit's DSL carries a lot of surface area:

  • Database and table name, engine choice, and the full column list with types, nullability, and defaults (including server-side function defaults, prefixed with fn:)
  • Primary key, ORDER BY, and PARTITION BY expressions, TTL rules, and settings like index granularity
  • Skip indexes with their expression, type, max rows, and granularity, plus compression codec chains defined as typed objects rather than strings (a codec chain might look like DoubleDelta followed by ZSTD at a given level, expressed as structured data, not a string someone has to get exactly right)

Materialized views and refreshable views are first-class citizens in the same system, including their target table references and refresh schedules. None of that is bolted on as a special case, which matters because bolted-on special cases are exactly where agents make mistakes.

Why does a typed source-of-truth matter specifically for agents, more than it would for a person? Because it means an agent editing schema is editing structured, type-checked code, not composing a SQL string from scratch and hoping the syntax holds. A typo or a structurally invalid definition gets caught when the code is checked, before it ever produces a migration file. Errors appear at definition time, which is a far cheaper place to catch a mistake than after a migration has already run against a live table.

For teams running agent toolchains built on another programming language, CHKit also ships as an installable package for that ecosystem, with the same CLI and the same schema semantics, just written in .py files instead of .ts files. Same contract, different syntax.

CHKit doesn't touch query execution, and it has no opinion about how data gets read once it's stored. Its job is schema, migrations, and the guardrails around both. Keeping that boundary clean is what lets it stay focused instead of trying to be a database client too.

Diff-based migration generation and its importance for agent deployments

The generate command is where the DSL turns into an actual plan. It loads the current TypeScript schema definitions, compares them against a stored snapshot of the last known state, and computes an ordered list of operations needed to get from one to the other. It writes that plan out as a migration SQL file along with an updated snapshot. If nothing changed, no file gets created. The migrations folder only holds real changes, not noise.

Every operation in that plan gets tagged with a risk level, and that risk level determines what happens next. Creating a table or adding a column is marked safe, because nothing gets destroyed. Changing a setting is marked caution. Dropping a table or a column is marked danger, because it's destructive and there's no undo.

Some changes, like adjusting a column, an index, a projection, a setting, or a TTL rule, can be applied in place with an ALTER. Others, like changing the engine, the primary key, the ORDER BY clause, or the PARTITION BY expression, are structural: they require dropping the table and recreating it. That second category is where a "small" schema tweak turns into rewriting an entire table's worth of data, and it's exactly the kind of thing raw hand-written DDL doesn't warn anyone about. CHKit surfaces which category a change falls into before it applies, not mid-migration when it's too late to reconsider.

Renames get handled deliberately rather than inferred. Mark a column or table with a renamedFrom property, or pass a rename flag on the command line, and CHKit emits an actual RENAME operation instead of interpreting the change as a drop followed by an add. Getting that distinction wrong means losing data instead of keeping it, so leaving it to guesswork isn't really an option.

Every migration that actually runs gets recorded in a journal table (_chkit_migrations) inside the database itself, along with a SHA-256 hash of the migration file's contents. If someone edits a migration file after it's already been applied, the next run checks that hash and can detect the mismatch. Migration history is enforced as immutable, not just assumed to be.

For an agent, the risk tags are the real payoff. They're a structured signal an approval process can act on directly: auto-apply everything tagged safe, and stop to ask a human before anything tagged danger goes through. An agent makes that call from a structured data field. And because every applied migration lives in that journal with its hash, whatever an agent does stays permanently attributable to a specific run.

The migrate command is designed to make destructive operations visible before they run, and applying changes requires deliberate action rather than happening silently: no override without saying so out loud. That gives an agent a clean two-step pattern: preview first as validation, then escalate to apply only once the plan checks out.

JSON output mode and the CI gate as the agent's structured interface

Every command in CHKit supports a JSON output flag, with a consistent, stable output structure across commands. That means results can be piped through something like jq, or read directly by whatever's parsing an agent's tool-call responses, without anyone writing a regex to scrape text.

The CLI also detects when it's running in a CI environment or without an interactive terminal attached, and it never blocks waiting for a prompt in that situation. An agent shelling out to run CHKit as a subprocess doesn't risk hanging forever because some interactive confirmation was expected and never came.

The check command fails a build with a non-zero exit code if there are pending migrations, schema drift, or a checksum mismatch. That's a single command a CI pipeline, or an agent standing in for one, can run before letting anything else proceed. The result comes back as a non-zero exit code on failure.

The drift command inspects the live database directly and compares it, object by object and column by column, against the expected snapshot. It reports objects that are missing, objects that exist live but aren't in the schema definition, columns that changed, and mismatches on settings, TTL, indexes, ORDER BY, PARTITION BY, primary keys, and projections. All of that comes back in JSON too.

The status command gives a lighter-weight check: migration counts and checksum status, useful as a quick poll before an agent decides what to do next.

Putting those pieces together lets an agent run a genuinely useful loop:

  • Run generate with JSON output, parse the plan, and check what operations are included
  • If everything's tagged safe, call the apply command with JSON output and proceed without waiting on anyone
  • If anything destructive appears, stop and surface the structured plan to a human reviewer instead of guessing
  • After applying, run drift with JSON output to confirm the live schema actually matches what was expected

That loop stays autonomous where it's safe to be autonomous, and gated where it isn't, and every step leaves a record in the migrations journal. Nobody has to reconstruct what happened after the fact from messages in a chat tool.

The mismatch between local and cloud-hosted environments deserves a straight answer too, because pretending it doesn't exist is how agents end up deploying the wrong engine somewhere. Local development typically runs a base table engine directly. Cloud-hosted environments sometimes rewrite that engine into a different variant automatically when the table gets created, one built for replication over shared storage rather than local disk. Self-hosted replicated clusters, using a database engine with its own replication model, behave differently again, and read patterns against these clusters carry their own constraints. An agent deploying one schema definition across all three shouldn't need a different engine declaration for each. That's what a plugin-based architecture is for: handling the engine-suffix matching automatically, so the same TypeScript definition works everywhere without an agent needing special-case logic per environment.

That shared-storage engine family is built to support a large number of replicas per table without needing every replica to hold a full copy of the metadata as things scale. The consequence for anything reading from these tables: certain read-time query patterns matter for getting correct results out of deduplicating or collapsing engines, and consistent engine targeting is what makes sure an agent is always querying the variant that's actually live, not the one it assumed was live.

Teams self-hosting have the same door open. CHKit is open source under MIT, works against any instance, and the plugin system is exactly the extension point for anyone who needs to write the equivalent engine-handling logic for their own cluster setup.

Diagram: How an Agent Decides: The Four-Step Migration Loop. Visualizes: Visualize the four-step autonomous agent loop described in the article.

Schema drift detection as a continuous safety signal for autonomous pipelines

Drift is close to the default state in agent-managed environments. It's close to the default state. Agents experiment, retry, fail partway through an apply, or operate against a branch that diverges from what's live. A schema that looked correct the last time anyone checked can quietly stop matching reality between one pipeline run and the next.

The drift command's comparison is granular by design: live database state against the expected snapshot, object by object, column by column. It surfaces objects the schema defines that don't exist live, objects that exist live but aren't defined anywhere, columns that changed shape, and mismatches on settings, TTL, indexes, sort keys, partition expressions, primary keys, and projections.

For an agent, running that check on a schedule, or as a pre-flight step before every pipeline execution, gives it a current picture of reality before it issues a query that assumes a particular schema shape. And because the result comes back in JSON, it's something the agent can act on directly rather than a warning meant for a person to read and shrug at.

The check command is where this becomes enforceable rather than just informative. It returns a non-zero exit code on pending migrations, on drift, or on a checksum mismatch: a single gate a pipeline, or an agent standing in for one, can run before letting any downstream step touch the database.

That checksum piece is a second, independent layer of protection. Even in the rare case where drift detection somehow misses a structural change, editing an already-applied migration file changes its hash, and the next run catches that on its own and aborts. Two separate mechanisms, checking two different things, both landing on the same outcome: stop before something breaks.

Set that against how a hand-written DDL shop usually finds drift: by accident. A query fails, a column that should be there isn't, and someone spends an afternoon figuring out why. Or worse, nobody notices until it appears in a customer-facing number weeks later. Making drift a named, queryable, CI-gateable state turns a latent unknown into something a pipeline checks on every single run, instead of something a person stumbles into after the damage is done.

Sandboxed schema experimentation for agents that need to test against

Agents proposing schema changes need somewhere to test those changes that isn't the production database. That's true of human engineers too, but it's sharper for an agent, which might generate and discard several candidate schema versions in the course of solving one problem, and has no business doing that trial-and-error against live data.

The mechanics CHKit uses for its own sandboxing, and how deep an isolated testing environment goes for a given deployment, aren't detailed here in a way that's safe to describe precisely. What's clear from everything else in the toolchain is the shape of the need: an agent should be able to run generate, inspect the resulting plan, and even run a preview migrate against something disposable, all before any of it touches a real table. The same structured-output, non-interactive design that makes the CI gate and drift detection usable by an agent is what makes a sandbox usable by one, too. Structured output, no blocking prompts, a clear record of what happened.

Whatever the specific implementation looks like, it has to clear that bar. Anything less turns the sandbox into a convenience for a human running commands by hand, not a real safety net for a pipeline that runs without one.

Sources

  1. GitHub - obsessiondb/chkit: ClickHouse schema and migration CLI for TypeScript projects
  2. The era of OLAP deserves the tooling relational has had for years
  3. @chkit/core CDN by jsDelivr - A CDN for npm and GitHub
Filed underMCP & Databases

More in MCP & Databases