---
title: Adding a Metadata Type
description: How to register a new metadata type so it shows up in the Setup app's Metadata Admin with full CRUD, overlay diffing, and (optionally) a custom editor.
---

# Adding a Metadata Type

The **Metadata Admin** engine (Setup app → *All Metadata Types*) automatically
renders a directory tile, list page, schema-driven form, layered diff view,
quick-find palette, and version history for **every** type registered in the
metadata plugin registry. To plug in a new type you usually only need two
files; a third file is required only if you want a bespoke editor.

## TL;DR

```
1. Add an entry to DEFAULT_METADATA_TYPE_REGISTRY    (packages/spec)
2. Define a Zod schema for the type                  (packages/spec/src/<domain>/)
3. (Optional) Register a custom editor               (objectui/.../builtinComponents.tsx)
```

That's it — no UI code required for the 80% case. The engine introspects the
registry entry to pick an icon, a domain group, whether to allow runtime
overrides, and the JSON schema used to generate the form.

---

## 1. Register the type

The single source of truth is the `DEFAULT_METADATA_TYPE_REGISTRY` in
`packages/spec/src/kernel/metadata-plugin.zod.ts`. Each entry follows
`MetadataTypeEntrySchema`:

```ts
{
  type: 'my_widget',          // canonical metadata type name (singular, snake_case)
  label: 'My Widget',         // display label (English)
  domain: 'ui',               // one of: data | ui | automation | ai | api | identity | security | system | platform | other
  icon: 'Box',                // lucide-react icon name
  description: 'Widgets shown on the home dashboard.',
  schema: MyWidgetJsonSchema, // JSON Schema generated from your Zod (see step 2)
  allowOrgOverride: true,     // false = compile-time only, true = runtime overlays allowed
  allowRuntimeCreate: true,   // allow admin to create new instances at runtime
  supportsOverlay: true,      // false = no 3-state diff in the editor
}
```

> **Naming rule (Prime Directive #3):** the `type` field is **singular**
> (`widget`, not `widgets`). REST endpoints below add the plural automatically
> (`/api/v1/meta/widgets`).

The `allowOrgOverride` flag is the **only** place that controls whether the
overlay store accepts writes for this type. The runtime env-var
`OBJECTSTACK_METADATA_WRITABLE=foo,bar` flips the flag on at boot for the
listed types — useful for opt-in writable behaviour in production.

## 2. Define the Zod schema

Place the schema under the appropriate domain folder, mirroring the
namespace it belongs to (e.g. `packages/spec/src/ui/my-widget.zod.ts`):

```ts
import { z } from 'zod';

export const MyWidgetSchema = z.object({
  /** Machine name — snake_case, used as the persisted id */
  name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Machine name'),
  /** Human-readable label shown in the directory */
  label: z.string().describe('Display label'),
  /** Optional description shown under the title */
  description: z.string().optional(),
  /** Layout column count */
  columns: z.number().int().min(1).max(12).default(3),
});

export type MyWidget = z.infer<typeof MyWidgetSchema>;
```

Then export the JSON Schema for the registry:

```ts
import { zodToJsonSchema } from 'zod-to-json-schema';
export const MyWidgetJsonSchema = zodToJsonSchema(MyWidgetSchema, { name: 'MyWidget' });
```

The Metadata Admin **SchemaForm** consumes this JSON Schema and produces:

- A field per top-level property
- Inline validation matching the Zod constraints (min/max/regex/enum)
- Default values from `.default(…)`
- TSDoc descriptions surface as field help text

You get a fully functional create/edit form for free.

## 3. (Optional) Plug in a custom editor

For most types the generic SchemaForm is enough. But for types with
complex shapes (a matrix, a canvas, a drag-and-drop layout, …) you can
register a bespoke React component.

In `objectui/packages/app-shell/src/services/builtinComponents.tsx`:

```tsx
import { DesignerEditorWrapper } from '../views/metadata-admin/DesignerEditorWrapper';
import { MyWidgetDesigner } from '@object-ui/plugin-designer';

// In registerBuiltinComponents():
registry.registerEditPage('my_widget', (params) => (
  <DesignerEditorWrapper
    type="my_widget"
    name={params.name}
    renderDesigner={(value, onChange, readOnly) => (
      <MyWidgetDesigner value={value} onChange={onChange} readOnly={readOnly} />
    )}
  />
));
```

`DesignerEditorWrapper` handles:

- Loading `?layers=code,overlay,effective` from the REST API
- Dirty tracking (`JSON.stringify` diff)
- Save with destructive-change confirmation dialog
- Reset overlay / Refresh / History buttons
- Read-only fallback when `allowOrgOverride` is false

You only have to render the value-bound part.

Common designer prop shapes already supported:

| Designer | Prop shape |
| --- | --- |
| `ObjectViewConfigurator` | `{ config, onChange, readOnly }` |
| `DashboardEditor` | `{ schema, onChange, readOnly }` |
| `PageCanvasEditor` | `{ schema, onChange, readOnly }` |
| `PermissionMatrixEditor` | full custom (does not use DesignerEditorWrapper) |

## Routing

No routing changes are required. The Setup app already includes:

```ts
// setup.app.ts
nav: [
  { type: 'component', componentRef: 'metadata:directory', label: 'Data Model' },
]
```

The directory page enumerates every registered type. The
`metadata:resource` route handles list/edit/create/history for whatever
`?type=<name>` is selected.

## i18n

Add display strings to the engine's translation bundle at
`objectui/packages/app-shell/src/views/metadata-admin/i18n.ts`:

```ts
const TYPE_LABELS_EN = { …, my_widget: 'My Widget' };
const TYPE_LABELS_ZH = { …, my_widget: '我的小部件' };
```

If you omit them, the directory falls back to the registry `label`.

## Checklist

- [ ] Registry entry added (`type`, `label`, `domain`, `icon`, `schema`, flags)
- [ ] Zod schema authored under `packages/spec/src/<domain>/<name>.zod.ts`
- [ ] JSON Schema exported and referenced from the registry entry
- [ ] (Optional) Custom editor registered in `builtinComponents.tsx`
- [ ] (Optional) i18n labels added to `metadata-admin/i18n.ts`
- [ ] `pnpm test` passes (framework) and `pnpm --filter @object-ui/app-shell build` passes (objectui)

## Related

- [`/api/v1/meta/types`](./api-reference.mdx) — the REST endpoint that powers the directory
- [Object & Field design](./data-modeling.mdx) — the canonical example of a writable metadata type
- [Plugin lifecycle](./plugin-development.mdx) — how third-party packages contribute types
