Skip to content

Custom Reports

Use defineReportKind for the normal extension path. It lets you resolve payloads and return a small presentation tree instead of building a custom renderer for each report.

import { z } from "zod";
import { createBuiltinRegistry, defineReportKind } from "mlform/engine";
const riskSummaryReport = defineReportKind({
kind: "risk-summary",
schema: z.object({
id: z.string().optional(),
kind: z.literal("risk-summary"),
label: z.string().optional(),
source: z.string().optional(),
}),
resolve: ({ report, result }) => result.reports[report.source],
render: {
summary: ({ payload }) => ({
title: payload.label ?? "Risk",
value: payload.score,
tone: payload.score > 0.8 ? "danger" : "neutral",
}),
content: ({ payload }) => [
{ type: "metric", label: "Score", value: payload.score },
{ type: "list", label: "Drivers", items: payload.drivers },
],
},
});
const registry = createBuiltinRegistry().registerReport(riskSummaryReport);

render.content can return text, metric, kv, list, table, badge, notice, or json nodes. The built-in declarative renderer handles the normal layout for you.

If resolve throws, MLForm marks only that report as error; the form submission can still complete for other reports.

Use defineReportDefinition plus a custom primitive renderer only when you need a fully custom visual contract.