Getting started · build it with Claude Code, or by hand

One typed model → idiomatic code, in your language.

Scaffold once, then tell Claude Code what you want"build the REST API for tasks," "add a priority field and update everything." Because meta init installs the MetaObjects skills, it knows the model is the source of truth: it edits the model and regenerates the code — it never hand-writes the boilerplate. Every command and output below is real verified, run against @metaobjectsdev/cli.

Adopting into an existing codebase? Before you install anything, have your agent run the fit & migration assessment against your repo. It's read-only and propose-only — it reads your code, migrations, and git history, and writes a decision-grade report: per-pillar fit verdicts (including NOT A FIT), a drift ledger of the shapes you already declare twice — with the past commits where a fix patched one copy and missed the other — and, if the verdict is yes, a first-week wedge plan.

Fetch https://metaobjects.dev/assess.md and run the MetaObjects Fit & Migration Assessment against this repository.

Any agent that can fetch a URL works (Claude Code, Cursor, Windsurf, Copilot's #fetch, Gemini CLI). Locked-down agent? Save the prompt file into your workspace and tell the agent to read it — every agent reads workspace files. The report ends where this page begins: its wedge plan's first command is meta init, step 1 below. Greenfield project? Skip straight to step 1.

Set up the project and the agent context

Add the TypeScript CLI and scaffold the workspace. meta init also teaches your coding agent how to use MetaObjects:

npm i @metaobjectsdev/cli
npx meta init

That writes a small, legible tree — your models go in metaobjects/:

your-project/
├── metaobjects/                 # ← your models live here (.yaml or .json)
│   └── meta.common.json         #   starter file (shared abstracts)
├── metaobjects.config.ts        # ← targets: dialect + what to generate
├── CLAUDE.md                    # wired to auto-load the context below
├── .metaobjects/                # tool state + agent docs (AGENTS.md, CLAUDE.md)
└── .claude/skills/              # six skills your agent now has:
    ├── metaobjects-authoring/   #   write & edit models
    ├── metaobjects-codegen/     #   run codegen
    ├── metaobjects-runtime-ui/  #   wire up runtime + UI
    ├── metaobjects-prompts/     #   typed LLM prompts
    ├── metaobjects-verify/      #   drift checks
    └── metaobjects-audit/       #   check adoption depth

This is the part that feels like magic. You describe outcomes; Claude Code knows the MetaObjects way — change the model, regenerate — because the codegen skill it loaded says so ("pattern-derivable-from-metadata = regenerate"). You never ask it to hand-write a route or a table.

Author the model

Model a Task — a title (required, max 120), a status of open / active / done, and a due date.

→ Claude writes metaobjects/meta.tasks.yaml using the metaobjects-authoring skill. You review a few readable lines.

Build the API — later, on demand

Now expose a REST API for tasks.

→ It knows REST routes are derived from the model, so it runs meta gen. The entity, typed data-access, and /tasks endpoints are generated — not one route hand-written.

Change the model — the API follows

Add a priority field, and make each Task belong to a Project. Then update everything.

→ It edits the model and re-runs meta gen. The schema, validation, data-access, and API all regenerate to match — and your hand-written logic in *.extra.ts is preserved. If anything no longer fits, it breaks at compile time, pointing at the exact line.

The rest of this page is what's happening underneath — useful if you'd rather drive it by hand, or just want to see it.

The model is language-neutral — the same metaobjects/ drives every port. You pick the toolchain for the language you ship. This guide uses TypeScript; swap the row for yours:

LanguageInstallGenerate
TypeScript (this guide)npm i @metaobjectsdev/climeta gen
C# / .NETdotnet add package MetaObjectsdotnet meta gen
Java / Kotlincom.metaobjects (Maven Central)mvn metaobjects:generate
Pythonpip install metaobjectsmetaobjects gen

For TypeScript, metaobjects.config.ts chooses what you generate — the SQL dialect and which generators run:

// metaobjects.config.ts
export default defineConfig({
  outDir:  "src/generated",
  dialect: "sqlite",        // sqlite | postgres | d1
  generators: [
    entityFile(),               // Drizzle table + Zod schemas + typed constants
    queriesFile(),              // typed data-access (findById, list, create…)
    routesFile(),               // REST endpoints (Fastify)
    // formFile(),              // opt-in: React form components
    barrel(),
  ],
});

Whether the agent writes it or you do, a model is just a small file in metaobjects/. Here's the one from above — metaobjects/meta.tasks.yaml:

# metaobjects/meta.tasks.yaml — the source of truth (.json works too)
metadata:
  package: app
  children:
    - object.entity:
        name: Task
        children:
          - source.rdb:       { table: tasks }
          - field.long:       { name: id }
          - field.string:     { name: title, required: true, maxLength: 120 }
          - field.enum:       { name: status, values: [open, active, done] }
          - field.timestamp:  { name: createdAt, required: true }
          - identity.primary: { fields: [id], generation: increment }
$ npx meta gen
meta gen — sqlite, src/generated

  NEW        src/generated/Task.ts
  NEW        src/generated/Task.queries.ts
  NEW        src/generated/Task.routes.ts
  NEW        src/generated/index.ts

  4 written

That's a real run. You get three things per entity (plus a barrel):

// src/generated/Task.ts  (excerpt — real output)
export const tasks = sqliteTable("tasks", {
  id:        integer("id").primaryKey({ autoIncrement: true }),
  title:     text("title").notNull(),
  status:    text("status", { enum: ["open", "active", "done"] }),
  createdAt: text("created_at").notNull(),
}, (t) => [ check("chk_tasks_status", sql`status IN ('open','active','done')`) ]);

export const TaskInsertSchema = z.object({
  title:  z.string().min(1).max(120),
  status: z.enum(["open", "active", "done"]).optional(),
  createdAt: z.string(),
});

Import the generated code into your app. It runs with no MetaObjects dependency — Drizzle, Zod, and Fastify only:

import Fastify from "fastify";
import { taskRoutes } from "./generated/Task.routes";

const app = Fastify();
app.register(taskRoutes);   // GET/POST/PATCH/DELETE /tasks, validated
await app.listen({ port: 3000 });

The custom business logic that makes the app yours? You write it — against the generated, typed code. Each file points at a *.extra.ts seam for additional queries, auth, and handlers that codegen preserves on regen. Change the model, re-run meta gen, and anything that no longer fits breaks at compile time — pointing you at the exact line.

As the model evolves, meta verify fails the build the moment generated code, schema, or prompt templates drift from it:

npx meta verify              # --templates (default): prompt-template drift
npx meta verify --codegen    # generated code vs the model
npx meta verify --db         # live database schema vs the model

Using a different assistant than Claude Code? Point it at metaobjects.dev/llms.txt for version-pinned context.

The monorepo is the source of truth — full quickstarts, the spec, and the cross-language conformance fixtures live there. This page is the on-ramp.