[data-reveal]{opacity:1!important;transform:none!important}
Drafts and Preview
Interface reference

Drafts and Preview

Use PreviewLayout to inspect a page draft without writing files, then SetPageBlocks to save its complete block tree in one transaction. A successful preview does not mean the draft compiles: the write performs that check.

Warning: SetPageBlocks replaces the whole block tree. Blocks you omit are removed. For one small change to an existing page, use ConfigureBlock instead.

Preview one card

With an existing /dashboard page, this read-only example returns a serialized KPI card. An editor can render preview.data.components.total; no source file changes.

import { PreviewLayout } from "@antelopejs/interface-dms-builder";

const preview = await PreviewLayout("/dashboard", {
  blocks: [{ name: "total", type: "KpiCard", config: { title: "Total revenue" } }],
});
if (preview.ok) {
  console.log(preview.data.components.total);
  console.log(preview.data.degraded);
} else {
  console.error(preview.error);
}

The per-block operations each save immediately. A visual editor usually holds edits locally and calls SetPageBlocks once, so the block tree and its queries either pass one typecheck together or remain unchanged.

Describe a Draft

PageDraft is a page as you want it written.

FieldTypeDescription
pagePartial<EditablePageMeta>Metadata to patch alongside the blocks. Omit to leave it alone.
blocksBlockDraft[]The top-level blocks, in the order they should appear.
queriesAddQueryInput[]The queries the page should serve, written with the blocks. Omit to leave the page's queries alone.

A block and the data it reads are one edit, so they are one write: the blocks, the model methods and the routes land in a single transaction with a single typecheck, or none of them land.

Omitting queries is not the same as passing an empty array. Omitted leaves whatever the page already serves untouched — which is what a caller written before queries joined the draft keeps doing. An empty array says the page should serve none, and removes the generated ones.

Two things a draft never destroys silently:

  • A query edited past the grammar is left exactly as it is, whether the draft still lists it or not. The engine reports query_opaque and moves on, because a chain it cannot read is one a human owns.
  • A query the draft forgot is kept when a block on the page still reads its URL, reported as query_kept. A draft that dropped a source a block depends on is a draft that would break that block, and keeping it is the recoverable side of the mistake.
import { SetPageBlocks } from "@antelopejs/interface-dms-builder";

await SetPageBlocks("/shop/board", {
  blocks: [
    {
      name: "revenueChart",
      type: "ChartCard",
      config: {
        title: "Revenue",
        fetchUrl: "/dms/pages/shop/board/stats/revenue",
        chart: { $block: { type: "ChartArea", config: { xaxisType: "category" } } },
      },
    },
  ],
  queries: [
    {
      name: "revenue",
      resource: "order",
      template: "series",
      params: { op: "sum", field: "amount", groupBy: "createdAt", bucket: "month" },
      response: "card",
    },
  ],
});

BlockDraft is one block:

FieldTypeDescription
namestringStatic key (top-level) or .child() id (nested). Must be a valid identifier.
typestringA BlockTypeDescriptor.type from the catalog. Ignored when preserve is set.
configRecord<string, unknown>The factory's options.
slotstringThe slot the block sits in.
metaRecord<string, unknown>Child metadata emitted with slot — { colSpan: 2 } on a grid child.
controllerResourceRefRequired for a controller-leading type such as TableView.
childrenBlockDraft[]Nested blocks, in .child() order.
preservebooleanWrite the block back exactly as it stands on disk.

Carry an Opaque Block Through a Write

A whole-tree write has to answer for blocks the builder can read but not rewrite: a CustomComponent, a block behind a computed expression, anything GetPageStructure reported with editable: false. preserve is that answer — the block is matched by name among its siblings and its source text is written back untouched.

To change only the page title, preserve every top-level block, including its children. This avoids reconstructing editable blocks from only part of their configuration:

import {
  GetPageStructure,
  SetPageBlocks,
} from "@antelopejs/interface-dms-builder";

const structure = await GetPageStructure("/dashboard");
if (structure.ok) {
  const blocks = structure.data.blocks.map((block) => ({
    name: block.name,
    preserve: true,
  }));
  const result = await SetPageBlocks("/dashboard", {
    page: { displayName: "Revenue" },
    blocks,
  }, {
    expectedVersion: structure.data.version,
  });
  if (!result.ok) console.error(result.error);
}

When you do edit a block, supply its full draft, including its children, slot, meta and controller where applicable. Do not reduce a block tree to names and configuration objects: omitted children are deleted.

A preserve block whose name matches nothing on disk is rejected with invalid_config.

Write a Whole Page

import { GetPageStructure, SetPageBlocks } from "@antelopejs/interface-dms-builder";

const structure = await GetPageStructure("/dashboard");
if (!structure.ok) throw new Error(structure.error.code);

const result = await SetPageBlocks(
  "/dashboard",
  {
    page: { displayName: "Revenue" },
    blocks: [
      {
        name: "kpis",
        type: "Grid",
        config: { gap: "1rem" },
        children: [
          {
            name: "row",
            type: "GridRow",
            children: [
              {
                name: "revenue",
                type: "KpiCard",
                config: { title: "Revenue", valueFormat: "currency" },
                meta: { colSpan: 2 },
              },
            ],
          },
        ],
      },
    ],
  },
  { expectedVersion: structure.data.version },
);
if (!result.ok) throw new Error(result.error.code);

Read the version immediately before preparing the draft. If the write returns stale, read the page again and review the other editor's changes before retrying.

On success the result carries the staged changes and the page's new version, ready to be passed as the next expectedVersion.

Blocks absent from the draft are removed. Static members that are not blocks are left where they are, and named imports nothing references any more are dropped, so a page shrinks cleanly rather than accumulating dead imports.

$expr is refused anywhere in a draft: a whole-tree write stays structured, and arbitrary code would defeat the guarantee. $dataType, $ref and $block are all accepted — see Value Encodings.

Read the Failures

SetPageBlocks validates the whole draft before touching anything, so one call reports every problem at once, each addressed by a JSON pointer into the draft:

PointerCause
/blocks/1/nameNot a valid identifier, or a duplicate among its siblings.
/blocks/2/typeUnknown block type, or a child a container does not accept.
/blocks/0/controllerA controller-leading type without one, or a controller on a type that takes none.
/blocks/3/config/title$expr inside a config value.

Beyond invalid_config, the write fails the same way the per-block operations do: stale when expectedVersion no longer matches, typecheck_failed when the emitted source does not compile — and then nothing is written. See Results and Errors.

Preview a Draft

PreviewLayout serializes a draft the way the runtime would, by calling the same dms-base factories the emitted source would call. Nothing is typechecked and nothing reaches disk, so it is cheap enough to run on every edit.

const preview = await PreviewLayout("/dashboard", draft);
if (preview.ok) {
  const grid = preview.data.components.kpis; // ComponentPreview
}

PageLayoutPreview holds:

FieldTypeDescription
componentsRecord<string, ComponentPreview>The serialized tree, keyed by top-level block name — the shape the DMS frontend renders.
degradedBlockPath[]Blocks the preview could not build faithfully.

A block lands in degraded when the preview cannot reach what the running page holds: a controller-leading block such as TableView (the DataAPI class lives in the app, not in the builder), a $ref, an $expr, a preserve block (which the draft does not describe), or a factory that threw. Render the last known-good layout for those rather than trusting the preview.

Preview a Query

PreviewLayout says what the page will look like; PreviewQuery says what its blocks will be showing. It runs a query the caller has not saved and answers what its route would, so a source can be configured against real data before anything is written.

import { PreviewQuery } from "@antelopejs/interface-dms-builder";

const result = await PreviewQuery({
  query: {
    name: "revenue",
    resource: "order",
    template: "series",
    params: {
      op: "sum", field: "amount", groupBy: "createdAt", bucket: "month",
      where: [
        { field: "createdAt", op: "ge", value: { $param: { name: "from" } } },
        { field: "createdAt", op: "lt", value: { $param: { name: "to" } } },
      ],
    },
  },
  args: { from: "2026-01-01T00:00:00.000Z", to: "2026-09-01T00:00:00.000Z" },
});
if (result.ok) {
  console.log(result.data.body); // { series: [{ x: 202601, y: 1250 }, …] }
  console.log(result.data.truncated); // whether it stopped reading early
}
FieldTypeDescription
queryAddQueryInputWhat to run. Validated the same way AddQuery validates it.
argsRecord<string, unknown>Values the route would receive, by the name it exposes them under. A bound value with nothing supplied is refused rather than compared against nothing.
tenantstringWhose rows to read, for a resource in a per-tenant schema.

What comes back is the route's own response body, not a rendering of it: body is built by the same helper the generated route would call, and output and response report it the way GetPageStructure reports the saved query. Ask for response: "card" and the preview answers a ChartCardData — the headline figure included — so a block can be handed it directly. response is set only when a helper arranged the body, which is the same condition read-back reports it under; a plain { value } or { series } leaves it absent. An arrangement the installed DMS does not publish is refused as unsupported rather than answered as bare points, because saving that query would fail its typecheck on the very same helper.

Read-only by construction: a query is filters, a grouping and a measure, and there is no way to spell a write in one. It stops at 500 points and sets truncated, because a chart that quietly lost its tail is worse than one that reports it. A database that refuses comes back as a failed OpResult, not as a rejection.

Next Steps