---
name: nocobase-ui-builder
description: >-
  **DEFAULT entry point for any NocoBase UI authoring or tweak** — new
  pages, new blocks, new menu items, AND localized edits (move / reorder /
  reconfigure a single block, field, action, or reaction rule) on an
  already-running NocoBase app. Uses the skill-local
  `nb-flow-surfaces.mjs` wrapper against the live app; no DSL file
  commit required.

  Only hand off to `nocobase-dsl-reconciler` when the user **explicitly
  asks for** DSL / YAML / committed-to-git / `cli push` workflow (e.g. "use
  the DSL reconciler", "I want YAML files", "commit the spec"). The DSL
  reconciler is in active development and has rough edges; prefer this
  skill unless the user opts in.

  Does not handle ACL, data modeling, workflow orchestration, browser
  reproduction, page error postmortems, or non-Modern-page navigation.
---

# Goal

- Agent-facing front door is `node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs`.
- Internal backend transport contract remains `flow-surfaces`; agents use only the wrapper entry for that family.
- Use `nb` as the only public transport. If `nb` is missing or stale, report the blocked command/env state instead of switching transports.
- Keep routing intent-first: open one matching quick-route doc first, not the whole directory.
- When a quick route already matches, stay on it. Do not enumerate the skill directory just to rediscover docs.
- When the task is a partial-match or handoff-only request, answer from this skill's scope boundary directly. Do not inspect runtime, scripts, or helper docs just to justify the handoff.
- Treat one user request that spans several pages as ordered single-page runs.
- Agent orchestration rule: if multiple ordered page runs share the same menu group title, serialize the page runs yourself. On the first page, use `navigation.group.title` to create or resolve the group and capture the returned `routeId`; for all later pages, set `navigation.group` to `{ routeId }` and do not use title-only creation. Never start concurrent title-only group creates for the same shared group.
- Concurrent title-only shared-group creates are forbidden.

# Router

- whole-page authoring goes through `applyBlueprint`, `nb-flow-surfaces.mjs apply-blueprint`, and [whole-page-quick.md](./references/whole-page-quick.md)
- localized existing-surface edits go through `nb-flow-surfaces.mjs` subcommands such as `compose`, `configure`, `update-settings`, `add-*`, `move-*`, and `remove-*`, plus [local-edit-quick.md](./references/local-edit-quick.md)
- localized existing-surface reaction work starts with `get-reaction-meta`, `nb-flow-surfaces.mjs get-reaction-meta`, writes through `set*Rules`, and [reaction-quick.md](./references/reaction-quick.md); first-pass whole-page reactions stay in `reaction.items[]` with no live `get-reaction-meta`; artifact-only localized reaction drafts record the planned `get-reaction-meta` probe
- partial-match or boundary-only requests go through [boundary-quick.md](./references/boundary-quick.md) first
- After that route is clear, if template / reference / `copy` routing is truly in scope, read [template-quick.md](./references/template-quick.md) first and then [templates.md](./references/templates.md) for the full decision matrix.
- Do not open [tool-shapes.md](./references/tool-shapes.md) or [helper-contracts.md](./references/helper-contracts.md) until you are preparing a real nb body, running the `prepare-write` gate, or validating a prepared write body.
- If the task involves JS `code`, `renderer: "js"`, `jsBlock`, `jsColumn`, `jsItem`, JS actions, charts, or `ctx.*` API questions, read [js.md](./references/js.md) first, then [js-surfaces/index.md](./references/js-surfaces/index.md), then [js-snippets/index.md](./references/js-snippets/index.md), and only then [js-reference-index.md](./references/js-reference-index.md).
- Before using a `flow-surfaces` subcommand you have not used yet in the current task, run `node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs <subcommand> --help`.
- Local helper CLIs are skill-local, not PATH contracts. Invoke `nb-runjs`, `nb-template-decision`, and `nb-localized-write-preflight` through `node skills/nocobase-ui-builder/runtime/bin/<helper>.mjs` from this repo root, or through the equivalent absolute path to this skill. Do not probe bare helper names first.

# Hard Rules

1. For nb writes, use `node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs` as the agent-facing entry. When a write path runs whole-page `prepare-write` or localized preflight, the later backend nb raw body is the returned `result.cliBody`, not the original draft payload. Do not wrap that object again.
2. For a normal single-page request, default to exactly one real tab. Do not add empty tabs or placeholder `markdown` / note / banner blocks unless the user asked for them.
3. Default blueprint `fields[]` entries to simple strings. Upgrade a field entry to an object only when `popup`, `target`, `renderer`, or a field-specific `type` is required.
4. For page authoring, field truth comes from live collection metadata. Prefer `nb api data-modeling collections get --filter-by-tk <collection> --appends fields -j`; if that command family is unavailable, use `nb api resource list --resource collections --filter '{"name":"<collection>"}' --appends fields -j`. Do not use `data-modeling fields list` / `nb api data-modeling collections fields list` as the authoring truth. Any field used in blueprint `fields[]` must have a non-empty `interface`. For whole-page `applyBlueprint`, recompute the full involved collection set from live metadata on every draft and rebuild `defaults.collections` from scratch instead of reusing stale fragments. For every involved direct collection, always emit `popups.view` / `popups.addNew` / `popups.edit` as `{ name, description }`, and let any `table` block pull that collection into `addNew` threshold evaluation even when the blueprint did not spell out an `addNew` opener. Keep `fieldGroups` collection-only on the target collection, and add `defaults.collections.<collection>.fieldGroups` only when one of those fixed generated popup scenes should still have more than 10 effective fields after scene filtering; otherwise omit `fieldGroups`. After generating any defaults fieldGroups, run one compact self-review of semantic grouping, required-field coverage, group balance, and group title specificity; use a short structured verdict such as `approve` or `regenerate`, use the lowest practical reasoning effort / no-think mode, and do not ask for chain-of-thought. If the verdict is `regenerate`, regenerate those fieldGroups once from live metadata and stop after that single retry. For association fields, keep every involved relation scope on the same fixed `view` / `addNew` / `edit` trio under `defaults.collections.<sourceCollection>.popups.associations.<associationField>.<action>` with the same `{ name, description }` contract, keyed only by the first relation segment; prepare-write may normalize legacy deeper keys down to that first segment. Do not create per-association or relation-scoped `fieldGroups`. Never generate `defaults.blocks`, and never put `blocks`, `fields`, `fieldGroups`, or layout under `defaults.collections.*.popups`.
5. `layout` belongs only on `tabs[]` or inline `popup`, never on a block object. For `createForm`, `editForm`, `details`, and `filterForm`, use `fieldsLayout` when the blueprint must control the inner field grid directly. Omit page/popup `layout` only when that tab/popup has at most one non-filter block; otherwise explicit layout is required. When multiple non-filter blocks share the same tab/popup, each non-template-backed data block needs a title; template-backed blocks are exempt. A single non-filter block may omit its title unless the user explicitly asks for one. For low-level `set-layout`, do not reuse public `{ rows: [[{ key, span }]] }` syntax: runtime `rows` is `Record<string, string[][]>`, each cell array stacks live child `uid`s, `[[uidA], [uidB]]` means two columns, and `[[uidA, uidB]]` means one stacked column.
6. For `createForm`, `editForm`, and `details`, once the block contains more than 10 real fields, use explicit `fieldGroups` instead of one flat `fields[]` list. Do not treat manual `divider` items as a substitute, and do not combine `fieldGroups` with `fieldsLayout`.
7. If clicking a shown record or relation record should open details, prefer a field popup. Use a button or action column only when the request explicitly asks for one.
8. If visible same-title destination menu groups already exist, never create another same-title group just to avoid ambiguity and never choose one locally. In that multi-match case, require explicit `navigation.group.routeId` before write. Without `routeId`, only zero-match create and one-match title reuse may proceed; metadata on reused groups is ignored.
9. In `applyBlueprint create`, any newly created `navigation.group` and any top-level or second-level `navigation.item` must carry one valid Ant Design icon name. When `navigation.item` is attached under an explicit existing `navigation.group.routeId`, keep an icon by default but do not assume the local prepare-write gate can prove whether that live target is already third-level or deeper.
10. Page identity for duplicate-page prevention is `(navigation.group.routeId, page.title)`, after resolving a unique `navigation.group.title` to routeId. In `applyBlueprint create`, if the same group already has the same page title, the wrapper may upgrade the prepared body to `replace` with `target.pageSchemaUid`; if a different group has the same page title, do not merge, reuse, or auto-replace that page.
11. `navigation.group.routeId` and desktop-route `id` are navigation locators only. When follow-up localized work or explicit inspection is needed after create/init or successful whole-page `applyBlueprint`, normalize to `pageSchemaUid` for page-level `flow-surfaces get`, and only use live `uid` values returned by `get` / `describe-surface` / create responses for `catalog`, `context`, `get-reaction-meta`, `compose`, `configure`, `add*`, or `remove*`. Never pass a desktop-route `id` as `target.uid`. For artifact-only locator handoffs, keep direct machine-readable fields `navigation.routeId`, `page.pageSchemaUid`, and `liveTargets[].uid`; when no live uid exists yet, use a non-empty placeholder string instead of `null`.
12. Before the first real whole-page `applyBlueprint`, invoke `node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs apply-blueprint`. The wrapper must run the internal prepare-write gate and send only the prepared `result.cliBody` to the backend write. Keep helper-specific rules in [references/helper-contracts.md](./references/helper-contracts.md): metadata auto-resolve, hidden popup defaults, hard failure on missing large-popup `fieldGroups`, `settings.sort` / `heightMode` normalization, and strict local failure on invalid `calendar` / `kanban` semantic field bindings. `collectionMetadata` stays outside the blueprint root in the wrapper envelope/call options.
13. Update action field assignment uses only public `settings.assignValues`. `bulkUpdate` is a collection action under block `actions`; `updateRecord` is a record action under `recordActions`. `assignValues` must be one plain object keyed by fields from the host collection metadata; `{}` is valid and clears assignment values. Do not use `add-fields`, raw `flowModels`, `AssignFormGridModel`, or `AssignFormItemModel` to configure action assignment UI.
14. Treat the local prepare-write helper as the authority for normalized local write shape. If it auto-adds, rewrites, or normalizes fields in ways that are within the helper's expected contract, keep that result as-is. Whole-page work includes whole-page create / replace, one route-backed tab full build, complex multi-block pages, nested-popup pages, and pages with multiple reaction families. Pre-write reads, metadata fetch, and `prepare-write` are allowed, but the first mutating write in that route must be `applyBlueprint`; in agent execution, invoke that first write through the wrapper `apply-blueprint` path, which then forwards only `result.cliBody` through backend transport. Do not send the original draft blueprint to the backend write. For whole-page `applyBlueprint` create / replace and same-blueprint `reaction.items[]`, a successful `applyBlueprint` response is the default stop point. Run follow-up `get` only when follow-up localized work or explicit inspection needs live structure. Without that extra readback, report the write from the success response and request intent rather than as a normalized persisted subtree.
15. Treat default values, computed values, field/block/action state, and show/hide as reaction work first. Do not guess raw configure keys.
16. Any JS / RunJS write payload must preserve readable multiline source. For `value.source: "runjs"`, `jsBlock` / `jsItem` / JS actions, chart raw code, or any other `code` field, author non-trivial code with actual newline characters and 2-space indentation before validation or write. Do not compress multiple statements, local variable setup, conditional branches, or string assembly onto one physical line just because the surrounding payload is JSON; encode line breaks as `\n` in JSON strings. Only a single short return/expression with no setup or branching may stay one line. A validator pass does not waive this readability rule.
17. If live readback shows an existing template reference and the requested change touches template-owned content, default to the template source. Keep host/openView config edits local. Page-scoped wording is not local-only intent, so do not auto-detach to `copy`; clarify before writing when scope is unresolved. For decision artifacts, record the template-owned content route and the host/openView route separately as `templateOwnedContentRoute` and `hostOpenViewConfigRoute`.
18. In testing or multi-agent runs, do not perform destructive cleanup unless the user explicitly asked for deletion.
19. When you actually have persisted readback to summarize for the user or for local helper artifacts, prefer one stable public summary with normalized type labels such as `table`, `details`, `editForm`, `filterForm`, and `createForm`; do not rely on raw model names alone. For page-level create / replace, keep `page.pageSchemaUid`, `page.pageTitle`, and `page.menuGroupTitle` explicit in that summary. When a scenario spans multiple pages, use the same canonical page identity keys under `pages.*`, and use `type` for concrete summary nodes such as `tables.*`, `lists.*`, and `forms.*`; reserve `blockTypes` for aggregate arrays such as `root.blockTypes` or `popups.*.blockTypes`. Keep root actions under `root.actionTitles` instead of leaving `recordActionTitles` as the only proof.
20. For reaction work, pick the final block/action target only after `get-reaction-meta` proves the required source path is available in that scene. On targets that expose multiple capabilities, select the write slot by matching `kind` first and then reuse that exact capability fingerprint; do not copy a nearby fingerprint from another `kind`. If the current target cannot expose the needed path, move the target or restructure the page/popup first instead of writing a guessed rule to an unsupported host.
21. Resolve filter wording before choosing structure. For table / list / gridCard / calendar / kanban-like data surfaces, ambiguous “筛选 / filter” requests default to the same host's block-level `filter` action/button, not a separate filter block. Treat “搜索 / search” that way only when the request explicitly adds search to a table / list / gridCard / calendar / kanban / card-like host, including wording such as “支持搜索 / 带搜索 / 可搜索 / searchable”; page-noun wording such as “搜索页 / 搜索结果页 / 搜索门户 / 搜索列表页” should stay page intent, not filter intent. Route `树筛选 / 树状筛选 / tree filter / tree filter block / 树形筛选区块` directly to `TreeBlockModel`, not `FilterFormBlockModel`; read [references/blocks/tree.md](./references/blocks/tree.md) before writing it. Route `分析看板 / dashboard / trend / KPI / 概览` to chart/grid-card insight paths by default; route to `KanbanBlockModel` only when kanban cues such as `看板区块 / kanban / pipeline / status columns / 拖拽 / 泳道 / backlog` are present. Plain `看板` alone does not override analytics intent. Do not create a new `filterForm` by default. Read [references/aliases.md](./references/aliases.md) first. Open [references/blocks/filter-form.md](./references/blocks/filter-form.md) and keep a real `filterForm` in the first-pass blueprint only when the user explicitly asks for a filter/search block, form, or query area and the phrase is not a tree-filter request; then include stable filter items, `submit` / `reset` actions, and same-blueprint string `target` block keys instead of low-level `defaultTargetUid` or raw block settings payloads.
Direct non-template table / list / gridCard / calendar / kanban data surfaces must carry a non-empty block-level `defaultFilter`; keep filter/search intent on the same host's block-level `filter` action unless the user explicitly asks for a separate filter block.
22. When you author one localized `compose` / `add-block` / `add-blocks` / `configure` body locally, validate it first with the local localized preflight helper (`node skills/nocobase-ui-builder/runtime/bin/nb-localized-write-preflight.mjs --operation <compose|add-block|add-blocks|configure> --stdin-json`) or `runLocalizedWritePreflight(...)`. Treat it as an explicit local validator for skill-authored low-level payloads: caller-supplied `collectionMetadata` is still required for data-bound payloads, direct non-template public data-surface blocks still fail closed on missing or empty block-level `defaultFilter`, and known live `configure` targets resolved through `collectionMetadata.liveTopology.byUid[target.uid]` reuse the same host-aware validations described in [references/helper-contracts.md](./references/helper-contracts.md). The wrapper `node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs <compose|add-block|add-blocks|configure>` must send only `result.cliBody` in the later backend write. The backend runtime remains compatibility-tolerant; this stricter requirement belongs to the local skill-side validator contract.
23. If a first-pass whole-page write still leaves `filterForm` as an empty shell after a successful `applyBlueprint`, treat it as an explicit local/live gap and keep any low-level `addBlock` / `addAction` / `addField` repair narrowly scoped. If the first `applyBlueprint` fails with a verified `filterForm`-specific shape/runtime error, repair the blueprint from that error, rerun `prepare-write`, and retry blueprint-only up to 5 rounds. Do not switch to low-level writes during those pre-success retries. After 5 failed rounds, report the latest blueprint / error evidence.
24. If a create/edit form helper or reference depends on `formValues.*`, inspect catalog / `get-reaction-meta` before choosing the host. When that live scene exposes `fields` / `actions` / `node` but not `blocks`, model the helper as a `jsItem` or other field-like helper inside the same form scene, not as a standalone block; for current JSItem targets, implement hide/show by rendering `null` until the form value is present instead of assuming `setFieldState` can target the JSItem. When that render-null pattern is used successfully, treat it as a configured helper toggle in readback/evidence instead of marking the helper outcome false only because `fieldLinkage` cannot target the JSItem itself.
25. Treat pages with multiple work areas, filter/search blocks, nested popups, or multiple reaction families as complex whole-page requests, not as a separate router path. They still stay on [whole-page-quick.md](./references/whole-page-quick.md) and still prefer one `applyBlueprint` request.
26. For those complex whole-page requests, first-pass blueprint generation should include the structural blocks, inline popups, and top-level `reaction.items[]` together. Do not split the page into root-shell / popup / reaction phases just because the page is large.
27. Use low-level `get-reaction-meta` + `set*Rules` or `add*` repair only for localized edits on an existing live page, or after a successful whole-page `applyBlueprint` when an explicit local/live gap still needs narrowly scoped repair. Before one whole-page `applyBlueprint` succeeds, do not use `createMenu`, `createPage`, `compose`, `configure`, `update-settings`, `add*`, `move*`, `remove*`, or `set*Rules`. If a whole-page `applyBlueprint` fails before first success, repair the blueprint from the error, rerun `prepare-write`, and retry blueprint-only up to 5 rounds. Do not switch to low-level writes during those pre-success retries. After 5 failed rounds, report the latest blueprint / error evidence.
28. Stay env-neutral in the general skill contract. Use the current configured CLI env or explicit runtime flags instead of hard-coding local aliases or fixed URLs.

# Read Paths

- Route unclear: [references/index.md](./references/index.md)
- Whole-page draft/create/replace from business intent: [whole-page-quick.md](./references/whole-page-quick.md)
- Localized existing-surface edit: [local-edit-quick.md](./references/local-edit-quick.md)
- Whole-page or localized reaction change: [reaction-quick.md](./references/reaction-quick.md)
- Partial-match handling and narrow handoff reports: [boundary-quick.md](./references/boundary-quick.md)
- Reuse, template selection, or existing template reference edits: [template-quick.md](./references/template-quick.md)
- Write-time helper CLIs / prewrite gate / helper return shapes for real writes: [helper-contracts.md](./references/helper-contracts.md)
- JS or chart work: [js.md](./references/js.md) or [chart.md](./references/chart.md)

# Scope & Handoff

- Handle only Modern page (v2) menu/page/tab/popup/content surfaces and the block / field / action / layout / reaction work inside them.
- For partial-match or boundary-report tasks, keep the Modern-page slice narrow and write the handoff report directly from this boundary list. Do not inspect runtime or scripts unless the request is explicitly about those mechanics.
- Hand off ACL / route permissions / role permissions to `nocobase-acl-manage`.
- Hand off collection / field / relation authoring to `nocobase-data-modeling`.
- Hand off workflow create / update / revision / execution to `nocobase-workflow-manage`.
