All case studies AI agent platform

Draagon AI: 17 runtime schema patches to zero

Python / TypeScript / React / MetaObjects

One ~5,000-line YAML spine now generates ~19,600 lines of drift-checked Python and TypeScript — 44 entities and 106 LLM prompts declared once, and any divergence between the engine and the platform fails the build instead of shipping.

draagon.com ↗

17 → 0
runtime ALTER-TABLE patches
schema patcher deleted, Jun 2026
471
generated files (~19.6k LOC)
committed generated/ dirs
~4:1
generated : metadata leverage
~5k YAML → ~19.6k generated
1
place to change a field — was ~7
1 YAML edit + meta gen
106
prompts as typed, drift-checked contracts
template.prompt declarations
5 wks
adoption → patcher deleted
97 commits, Jun–Jul 2026

Draagon AI is an AI agent platform — a Python engine and a TypeScript platform (API, admin UI, web). Until mid-2026, every data shape was hand-declared in three places that drifted, reconciled at runtime by 17 ALTER TABLE patches and, once, by a production bug. Five weeks after adopting MetaObjects, one YAML model generates ~19,600 lines across both languages — and drift fails the build instead of shipping.

The challenge: three copies of every shape — and the drift was executable

Every agent, telemetry record, and config shape lived in three hand-kept declarations: the TypeScript Drizzle schema, the Python persistence layer, and the shared Zod validators and TS types. The cross-language contract between the engine and the platform was maintained by hand and documented in a comment.

The drift wasn't theoretical — it was executable. The Python side carried 17 ALTER TABLE … ADD COLUMN IF NOT EXISTS statements run on every database connect: Python defensively re-patching the schema on startup because it ran ahead of the TypeScript migrations. The two languages reconciled their schema at runtime, at the first connection.

Then it shipped a bug. Hand-written validators duplicated the data model and silently diverged — marking optional config fields as required — and legitimate partial agent-config writes started returning 400 in production. Meanwhile the 100+ LLM prompts were string constants formatted inside a try/except that silently degraded when a payload key went missing, and a ~190-line hand-written mapper re-mapped every field one by one — primed to drop one whenever the model grew.

Hand-syncing the third copy of every shape, with a runtime patcher papering over the gap, was the decision point: the shapes themselves had to become data.

The solution: declare it once, generate every consumer

The fix wasn't a better sync process — it was making the model the artifact. 44 entities, 16 value objects, 13 projection views, and 106 prompt templates are now declared once in YAML, and every Python and TypeScript consumer is generated from that spine and drift-gated in CI. One field declaration becomes the table column and the validator that used to be hand-kept:

Declared once — YAML
# meta.platform.yaml
- field.int:
    name: maxTokens
    column: max_tokens
Generated — schema + validator, one source
// Drizzle table (TypeScript)
maxTokens: integer("max_tokens"),

// Zod validator — was hand-kept, now generated
maxTokens: z.number().int().optional(),

From that one model, meta gen produces 471 committed files (~19.6k lines): Python record models, value objects, and prompt payloads with output parsers on the engine side; Drizzle tables, typed query helpers, Zod/TS contracts, TanStack hooks, admin UI, and API routes on the platform side. Business logic and the orchestration engine stay hand-written — the generated layer is the plumbing that used to drift.

Four meta verify gates run in CI, plus a pre-commit hook and a dedicated drift workflow on every push — so a model change that isn't reflected in both languages fails the build, not the first INSERT. The migration took five weeks and 97 commits, alongside normal feature work.

Results

Before

Add a field to an agent config

Touch up to ~7 hand-written places across two languages — dataclass, Drizzle table, Zod validator, TS type, route validation, web hook, form — and hope they agree.

After

Add a field to an agent config

Edit one YAML declaration and run meta gen. The field round-trips through both languages automatically.

What became newly possible

  • A new admin grid now costs a YAML projection + regen. All five observability grids render from generated columns; a net-new LLM Calls tab was built fully metadata-driven — including a per-row LLM cost column served by a generated computed-column route.
  • A generated data dictionary of the whole platform — a 343-file HTML reference rendered straight from the spine, always in sync because it has no other source.
  • Presentation as an overlay — UI render hints live in a separate file, versioned independently of the data contract, regenerated byte-identically.
  • The adoption fed the standard. As MetaObjects' first production Python adopter, Draagon's gaps became upstream releases within days — and it ran in reverse too: a stricter upstream validator surfaced two latent metadata bugs in Draagon at verify time, not in production.

The schema patcher that ran 17 ALTER TABLEs on every connect wasn't refactored — it was deleted. The canonical model owns the shape now, in both languages.

— verified in-repo: schema patcher removed (−75 lines), June 2026

Draagon's source is private — for a clone-and-read version of the same pattern, run the 10-second drift demo in Wizards of Odd or read smon.

Where it is now

Since 2026-09-09 it has run MetaObjects 1.0.0, every package it consumes pinned to the stable release, with its drift gate enforced in CI.

What wasn't clean

All case studies

Considering MetaObjects for your next platform?

Doug works directly with teams evaluating MetaObjects. Tell us what you're building and we'll tell you, honestly, whether MetaObjects fits.

Email doug@dougmealing.com