Best practices
Three rules keep a DMS page maintainable, permissionable and editable by the page builder and by agents. The dms-best-practices skill, shipped with @antelopejs/dms, makes coding agents such as Claude Code and Codex, and the dms-ai assistant, apply them on every page they build.
1. A page is always described as blocks
Describe every page on the backend as a tree of DMS blocks: a toolbar, a grid of cards, a table, a form. Never as one block that draws the whole screen.
Each block is a line in the roles editor, a target for .watch() reactivity, a widget a PeriodSelector can drive, and a node the page builder can move. One block for the whole screen gives up all of that at once.
@RegisterPage()
export class OrdersPage extends PageController("orders", { displayName: "$shop.orders.title", category: shopCategory }) {
static period = PeriodSelector({ id: "orders" });
static kpis = Grid().child(
"row",
GridRow()
.child("revenue", KpiCard({ title: "$shop.orders.revenue", fetchUrl: "/api/orders/revenue", periodScope: "orders" }))
.child("count", KpiCard({ title: "$shop.orders.count", fetchUrl: "/api/orders/count", periodScope: "orders" })),
);
static table = TableView(OrdersDataAPI, { caption: "$shop.orders.list" });
}
@RegisterPage()
export class OrdersPage extends PageController("orders", { displayName: "Orders", category: shopCategory }) {
// One Vue component draws the period picker, the KPIs and the table:
// one permission, no period scope, nothing the page builder can edit.
static screen = CustomComponent("ShopOrdersScreen");
}
2. Build only the missing block
When a part of the screen has no DMS equivalent — a flow canvas, a seat map — build that block only as a custom component, and keep everything around it as DMS blocks described on the backend. Permissions stay granular, and the rest of the page keeps the theme, the period, the live state and the editor for free.
3. Every custom block carries .meta()
Every custom block declares its name, description and icon with .meta(), as i18n keys, so the roles editor shows a readable line for it in every language. Without it, the permission is titled with the raw component name.
static graph = CustomComponent("ApiRouteGraph")
.options({ fetchUrl: "/api/routes/graph", periodScope: "api" })
.meta({
name: "$api.routes.graph.name",
description: "$api.routes.graph.description",
icon: "i-ph-graph",
});
// The roles editor shows "ApiRouteGraph", untranslated, with no icon.
static graph = CustomComponent("ApiRouteGraph").options({ fetchUrl: "/api/routes/graph" });
Add the keys to every locale file of your frontend layer. Use $ keys for every other text too: page names, titles, labels, button labels.
When to leave DMS blocks
A custom block is a deviation from the DMS, so an agent proposes it and lets you decide. It proposes one only in the cases on the left:
| Leave DMS blocks when | Stay on DMS blocks when |
|---|---|
| No block covers the need, even through its options or a composition of blocks. | The difference is spacing, color, radius, font or a minor layout that the theme or the block's options handle. |
| The interaction model differs: a drag-and-drop canvas, real-time collaborative editing. | The DMS block only looks slightly different from the mockup. |
| You asked for pixel perfect on this element. | Another element of the page is custom: a custom block never pulls its neighbours along. |
| A measured constraint, such as performance or data volume, is beyond what the block can meet. | The constraint is a guess rather than a measurement. |
Before any custom block
An agent follows the same path before it writes any custom block, one element at a time:
Search the catalog
The agent looks for a block that fits: in dms-ai with @antelopejs/dms-builder loaded, through the Builder catalog; otherwise in the DMS block catalog, the components pages or the exports of @antelopejs/interface-dms/base. It tries the block's options first, then a composition of blocks, such as a Card holding a KeyValueList.
Compare
In the dms-ai chat, the agent previews the closest DMS option next to your mockup, with a sketch of the custom block it would build as another tab. Elsewhere, it writes a short comparison table.
Ask
The agent asks one closed question: keep the DMS block, adjust it through its options, or build a custom block.
Build
Only when you choose the custom block does the agent build it, alone and with .meta(); the rest of the page stays DMS blocks.
In safe mode, the dms-ai assistant cannot write custom code at all. It tells you so, offers the closest DMS composition, and asks whether to switch to Code mode for that one block.
When a design is involved
When you hand an agent a mockup or a screenshot, it follows the design-fidelity protocol: one framing question, then this path for each component that differs visibly.
Next steps
In the dms-ai assistant
The dms-ai module runs a coding agent next to your dashboard that loads the skills of every loaded module and previews DMS blocks in the chat.
Design fidelity
What an agent does with your mockup — one framing question, then component by component, a preview of the DMS block next to a sketch of the custom one, and the choice between keeping, adjusting or replacing the block.