| 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
nbas the only public transport. Ifnbis 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.titleto create or resolve the group and capture the returnedrouteId; for all later pages, setnavigation.groupto{ 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 - localized existing-surface edits go through
nb-flow-surfaces.mjssubcommands such ascompose,configure,update-settings,add-*,move-*, andremove-*, plus local-edit-quick.md - localized existing-surface reaction work starts with
get-reaction-meta,nb-flow-surfaces.mjs get-reaction-meta, writes throughset*Rules, and reaction-quick.md; first-pass whole-page reactions stay inreaction.items[]with no liveget-reaction-meta; artifact-only localized reaction drafts record the plannedget-reaction-metaprobe - partial-match or boundary-only requests go through boundary-quick.md first
- After that route is clear, if template / reference /
copyrouting is truly in scope, read template-quick.md first and then templates.md for the full decision matrix. - Do not open tool-shapes.md or helper-contracts.md until you are preparing a real nb body, running the
prepare-writegate, or validating a prepared write body. - If the task involves JS
code,renderer: "js",jsBlock,jsColumn,jsItem, JS actions, charts, orctx.*API questions, read js.md first, then js-surfaces/index.md, then js-snippets/index.md, and only then js-reference-index.md. - Before using a
flow-surfacessubcommand you have not used yet in the current task, runnode 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, andnb-localized-write-preflightthroughnode skills/nocobase-ui-builder/runtime/bin/<helper>.mjsfrom this repo root, or through the equivalent absolute path to this skill. Do not probe bare helper names first.
Hard Rules
- For nb writes, use
node skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjsas the agent-facing entry. When a write path runs whole-pageprepare-writeor localized preflight, the later backend nb raw body is the returnedresult.cliBody, not the original draft payload. Do not wrap that object again. - 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. - Default blueprint
fields[]entries to simple strings. Upgrade a field entry to an object only whenpopup,target,renderer, or a field-specifictypeis required. - 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, usenb api resource list --resource collections --filter '{"name":"<collection>"}' --appends fields -j. Do not usedata-modeling fields list/nb api data-modeling collections fields listas the authoring truth. Any field used in blueprintfields[]must have a non-emptyinterface. For whole-pageapplyBlueprint, recompute the full involved collection set from live metadata on every draft and rebuilddefaults.collectionsfrom scratch instead of reusing stale fragments. For every involved direct collection, always emitpopups.view/popups.addNew/popups.editas{ name, description }, and let anytableblock pull that collection intoaddNewthreshold evaluation even when the blueprint did not spell out anaddNewopener. KeepfieldGroupscollection-only on the target collection, and adddefaults.collections.<collection>.fieldGroupsonly when one of those fixed generated popup scenes should still have more than 10 effective fields after scene filtering; otherwise omitfieldGroups. 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 asapproveorregenerate, use the lowest practical reasoning effort / no-think mode, and do not ask for chain-of-thought. If the verdict isregenerate, regenerate those fieldGroups once from live metadata and stop after that single retry. For association fields, keep every involved relation scope on the same fixedview/addNew/edittrio underdefaults.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-scopedfieldGroups. Never generatedefaults.blocks, and never putblocks,fields,fieldGroups, or layout underdefaults.collections.*.popups. layoutbelongs only ontabs[]or inlinepopup, never on a block object. ForcreateForm,editForm,details, andfilterForm, usefieldsLayoutwhen the blueprint must control the inner field grid directly. Omit page/popuplayoutonly 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-levelset-layout, do not reuse public{ rows: [[{ key, span }]] }syntax: runtimerowsisRecord<string, string[][]>, each cell array stacks live childuids,[[uidA], [uidB]]means two columns, and[[uidA, uidB]]means one stacked column.- For
createForm,editForm, anddetails, once the block contains more than 10 real fields, use explicitfieldGroupsinstead of one flatfields[]list. Do not treat manualdivideritems as a substitute, and do not combinefieldGroupswithfieldsLayout. - 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.
- 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.routeIdbefore write. WithoutrouteId, only zero-match create and one-match title reuse may proceed; metadata on reused groups is ignored. - In
applyBlueprint create, any newly creatednavigation.groupand any top-level or second-levelnavigation.itemmust carry one valid Ant Design icon name. Whennavigation.itemis attached under an explicit existingnavigation.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. - Page identity for duplicate-page prevention is
(navigation.group.routeId, page.title), after resolving a uniquenavigation.group.titleto routeId. InapplyBlueprint create, if the same group already has the same page title, the wrapper may upgrade the prepared body toreplacewithtarget.pageSchemaUid; if a different group has the same page title, do not merge, reuse, or auto-replace that page. navigation.group.routeIdand desktop-routeidare navigation locators only. When follow-up localized work or explicit inspection is needed after create/init or successful whole-pageapplyBlueprint, normalize topageSchemaUidfor page-levelflow-surfaces get, and only use liveuidvalues returned byget/describe-surface/ create responses forcatalog,context,get-reaction-meta,compose,configure,add*, orremove*. Never pass a desktop-routeidastarget.uid. For artifact-only locator handoffs, keep direct machine-readable fieldsnavigation.routeId,page.pageSchemaUid, andliveTargets[].uid; when no live uid exists yet, use a non-empty placeholder string instead ofnull.- Before the first real whole-page
applyBlueprint, invokenode 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 preparedresult.cliBodyto the backend write. Keep helper-specific rules in references/helper-contracts.md: metadata auto-resolve, hidden popup defaults, hard failure on missing large-popupfieldGroups,settings.sort/heightModenormalization, and strict local failure on invalidcalendar/kanbansemantic field bindings.collectionMetadatastays outside the blueprint root in the wrapper envelope/call options. - Update action field assignment uses only public
settings.assignValues.bulkUpdateis a collection action under blockactions;updateRecordis a record action underrecordActions.assignValuesmust be one plain object keyed by fields from the host collection metadata;{}is valid and clears assignment values. Do not useadd-fields, rawflowModels,AssignFormGridModel, orAssignFormItemModelto configure action assignment UI. - 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-writeare allowed, but the first mutating write in that route must beapplyBlueprint; in agent execution, invoke that first write through the wrapperapply-blueprintpath, which then forwards onlyresult.cliBodythrough backend transport. Do not send the original draft blueprint to the backend write. For whole-pageapplyBlueprintcreate / replace and same-blueprintreaction.items[], a successfulapplyBlueprintresponse is the default stop point. Run follow-upgetonly 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. - Treat default values, computed values, field/block/action state, and show/hide as reaction work first. Do not guess raw configure keys.
- Any JS / RunJS write payload must preserve readable multiline source. For
value.source: "runjs",jsBlock/jsItem/ JS actions, chart raw code, or any othercodefield, 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\nin 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. - 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 astemplateOwnedContentRouteandhostOpenViewConfigRoute. - In testing or multi-agent runs, do not perform destructive cleanup unless the user explicitly asked for deletion.
- 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, andcreateForm; do not rely on raw model names alone. For page-level create / replace, keeppage.pageSchemaUid,page.pageTitle, andpage.menuGroupTitleexplicit in that summary. When a scenario spans multiple pages, use the same canonical page identity keys underpages.*, and usetypefor concrete summary nodes such astables.*,lists.*, andforms.*; reserveblockTypesfor aggregate arrays such asroot.blockTypesorpopups.*.blockTypes. Keep root actions underroot.actionTitlesinstead of leavingrecordActionTitlesas the only proof. - For reaction work, pick the final block/action target only after
get-reaction-metaproves the required source path is available in that scene. On targets that expose multiple capabilities, select the write slot by matchingkindfirst and then reuse that exact capability fingerprint; do not copy a nearby fingerprint from anotherkind. 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. - 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
filteraction/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 toTreeBlockModel, notFilterFormBlockModel; read references/blocks/tree.md before writing it. Route分析看板 / dashboard / trend / KPI / 概览to chart/grid-card insight paths by default; route toKanbanBlockModelonly when kanban cues such as看板区块 / kanban / pipeline / status columns / 拖拽 / 泳道 / backlogare present. Plain看板alone does not override analytics intent. Do not create a newfilterFormby default. Read references/aliases.md first. Open references/blocks/filter-form.md and keep a realfilterFormin 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/resetactions, and same-blueprint stringtargetblock keys instead of low-leveldefaultTargetUidor raw block settings payloads. Direct non-template table / list / gridCard / calendar / kanban data surfaces must carry a non-empty block-leveldefaultFilter; keep filter/search intent on the same host's block-levelfilteraction unless the user explicitly asks for a separate filter block. - When you author one localized
compose/add-block/add-blocks/configurebody 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) orrunLocalizedWritePreflight(...). Treat it as an explicit local validator for skill-authored low-level payloads: caller-suppliedcollectionMetadatais still required for data-bound payloads, direct non-template public data-surface blocks still fail closed on missing or empty block-leveldefaultFilter, and known liveconfiguretargets resolved throughcollectionMetadata.liveTopology.byUid[target.uid]reuse the same host-aware validations described in references/helper-contracts.md. The wrappernode skills/nocobase-ui-builder/runtime/bin/nb-flow-surfaces.mjs <compose|add-block|add-blocks|configure>must send onlyresult.cliBodyin the later backend write. The backend runtime remains compatibility-tolerant; this stricter requirement belongs to the local skill-side validator contract. - If a first-pass whole-page write still leaves
filterFormas an empty shell after a successfulapplyBlueprint, treat it as an explicit local/live gap and keep any low-leveladdBlock/addAction/addFieldrepair narrowly scoped. If the firstapplyBlueprintfails with a verifiedfilterForm-specific shape/runtime error, repair the blueprint from that error, rerunprepare-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. - If a create/edit form helper or reference depends on
formValues.*, inspect catalog /get-reaction-metabefore choosing the host. When that live scene exposesfields/actions/nodebut notblocks, model the helper as ajsItemor other field-like helper inside the same form scene, not as a standalone block; for current JSItem targets, implement hide/show by renderingnulluntil the form value is present instead of assumingsetFieldStatecan 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 becausefieldLinkagecannot target the JSItem itself. - 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 and still prefer one
applyBlueprintrequest. - 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. - Use low-level
get-reaction-meta+set*Rulesoradd*repair only for localized edits on an existing live page, or after a successful whole-pageapplyBlueprintwhen an explicit local/live gap still needs narrowly scoped repair. Before one whole-pageapplyBlueprintsucceeds, do not usecreateMenu,createPage,compose,configure,update-settings,add*,move*,remove*, orset*Rules. If a whole-pageapplyBlueprintfails before first success, repair the blueprint from the error, rerunprepare-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. - 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
- Whole-page draft/create/replace from business intent: whole-page-quick.md
- Localized existing-surface edit: local-edit-quick.md
- Whole-page or localized reaction change: reaction-quick.md
- Partial-match handling and narrow handoff reports: boundary-quick.md
- Reuse, template selection, or existing template reference edits: template-quick.md
- Write-time helper CLIs / prewrite gate / helper return shapes for real writes: helper-contracts.md
- JS or chart work: js.md or 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.