skills/nocobase-dsl-reconciler/SKILL.md
**Opt-in DSL path** for NocoBase app building. Use ONLY when the user explicitly asks for YAML / DSL / committed-to-git / `cli push` / spec files — e.g. "use the DSL reconciler", "I want YAML I can commit", "build this as a workspaces/ project". For any other UI authoring request (new page, new block, tweak an existing screen), default to `nocobase-ui-builder` instead — this reconciler is still in active development and has rough edges that the live-UI path avoids. When the user opts in: produces/changes files under `workspaces/<project>/`, supports new pages, menus, modules, whole systems, collections, tables, sub-tables, popups, dashboards, approval workflows, recordActions, and deploys them via `cli push`.
npx skillsauth add nocobase/skills nocobase-dsl-reconcilerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This is the opt-in DSL path. Default is nocobase-ui-builder.
Stay on this skill only when the user explicitly wants YAML files they
can commit + cli push. If you arrived here from a generic "build me a
NocoBase app" request without the user naming DSL/YAML/git, switch to
nocobase-ui-builder instead — it's the default entry point.
templates/crm/ is a read-only reference library. When you're
unsure how a specific field, block, or popup is shaped, open the
closest CRM example, read it, then write your own adapted version in
your workspace. Per-scenario pointers live in the workflow sections below.
Never copy CRM files wholesale into your workspace. Do not
cp -r templates/crm/... to get started, do not duplicate
collections/nb_crm_*.yaml, do not base your routes.yaml on CRM's.
Bulk-copying drags unrelated leads/opportunities/orders state and
workflows into your project — you then spend the whole session fighting
hundreds of irrelevant validator errors instead of building your module.
The pre-deploy spec validator catches most structural mistakes with a
clear error message. Trust the validator: when it errors, fix what
it says rather than guessing — don't grep through src/deploy/*.ts.
Hand off to the matching skill when the user's request is orthogonal:
| User asks for… | Skill |
|---|---|
| One-off live-UI tweak on an already-running page (move / reorder / reconfigure a single block, field, action) — no DSL commit wanted | nocobase-ui-builder |
| ACL / role permissions / route permissions | nocobase-acl-manage |
| Workflow create / update / revision / execution | nocobase-workflow-manage (this skill only wires the trigger button; authoring the graph goes there) |
| Collection / field / relation authoring outside a DSL project | nocobase-data-modeling |
| Plugin development (.tsx components, server code) | nocobase-plugin-development |
| Install / enable plugin | nocobase-plugin-manage |
| Environment setup / app install / upgrade | nocobase-env-manage |
Any change that should live as a committed YAML file under
workspaces/<project>/ — stays here.
cd <skill-dir>/src
export [email protected] NB_PASSWORD=admin123 NB_URL=http://localhost:14000
Default path for "build me a NocoBase app": copy the starter and modify it. Do not hand-write the skeleton; do not study CRM first.
cd <skill-dir>/src
cp -r ../templates/starter ../workspaces/<name>
# First push: --copy bypasses validation rules that only matter once
# popups exist (m2o popup binding, clickToOpen file presence). The
# starter ships with its own popups so the first push is actually fully
# valid — --copy is for your *extensions* before popups are wired.
npx tsx cli/cli.ts push <name> --force
The starter is a complete minimal CRUD — 1 collection (Projects), 1 Dashboard page (4 KPI tiles + 2 charts), 1 list page with filterForm / table / addNew popup / detail popup / 2 updateRecord recordActions. Push as-is → visible in NB → then edit.
Iterate one concern at a time, push between each:
collections/nb_starter_projects.yaml → your collection name
(match the nb_<module>_<entity> convention)routes.yaml: change "Starter" / "Projects" titlespages/starter/ directory name if you want (match routes.yaml
key)nb_starter_projects across all filespages/<...>/layout.yaml fields: and the
popup templates' field_layout: accordingly.collections/*.yaml + pages/<module>/<entity>/ dir,
copy pages/starter/projects/layout.yaml as a starting point.Never write the whole module in one shot. For customer-facing builds — land the skeleton, show the user, gather feedback, iterate. The starter push takes minutes; a hand-built module takes hours.
--copy helpsPass --copy when the workspace has no popup files yet (early
stage — validator would fire errors about "m2o field X has no popup
binding" that the user will fix in the next push). The reconciler
auto-bypasses spec errors in this state. Once any popups/*.yaml
exists, drop --copy and let validation run.
state.yaml. Manual NB-UI
authored elements (not tracked in state.yaml) are left alone.Targeted pushes:
--group <key> scopes to one menu subtree--page <key> scopes to one page--incremental skips pages whose DSL hasn't changed since last pushFor pure live-UI tweaks without a DSL commit, hand off to
nocobase-ui-builder instead (see the routing table above).
Triggers for going beyond the agile starter loop:
Progression: Round 0 design → Round 1 scaffold → Round 2 fill → Round 2' seed → Round 3 JS. Each round is a deployable state.
Write a DESIGN.md (markdown, not YAML) covering:
nocobase-data-modeling skill for field-interface reference.Wait for user confirmation before writing YAML. A single design pass saves 3× redesigns.
Skip Round 0 if the user's ask fits the starter shape (single entity, basic CRUD).
Example:
## Collections
- nb_lib_books (title, author, isbn, category, status, loans: o2m → nb_lib_loans)
- nb_lib_members (name, email, phone, join_date, loans: o2m → nb_lib_loans)
- nb_lib_loans (loan_no, book: m2o, member: m2o, borrowed_at, due_date, returned_at, status)
## Pages (under menu "Library")
- Books list — browse + search books, add new
- Members list — browse members, their loan history
- Loans list — active/overdue loans, return action
- Dashboard — KPIs + charts
## Navigation
- books.table.title → books detail popup (shared template)
- loans.table.book → books detail popup (shared via defaults.yaml)
- loans.table.member → members detail popup (shared via defaults.yaml)
If launching a sub-agent (kimi TUI, Claude Code subprocess), its CWD becomes the default write target. Set it before launch:
mkdir -p <user-workdir>
cd <user-workdir>
kimi --yolo # or claude, codex
Skip the cd and the agent writes to the parent project root.
Even in the advanced path, don't hand-write routes.yaml +
collections/*.yaml from scratch. Copy the starter, then grow:
| Step | What to do |
|---|---|
| Base | cp -r templates/starter workspaces/<name> — push once |
| Add collection | Write collections/<next_coll>.yaml (format matches starter's). Match nb_<module>_<entity> convention. |
| Add page | mkdir pages/<module>/<page>/ + layout.yaml; mirror starter's projects layout. |
| Update routes | Add entry under the existing group in routes.yaml. |
| Deploy | cli push <name> --force after each collection/page addition |
CRM references (consult only when stuck on structure):
templates/crm/collections/nb_crm_leads.yaml — collection formattemplates/crm/routes.yaml — multi-group routes (shape only)For each page beyond the starter basics:
| Building | CRM reference |
|---|---|
| Main list table + filter | templates/crm/pages/main/leads/layout.yaml |
| Multi-tab page | templates/crm/pages/main/customers/ (page.yaml + tab_*/layout.yaml) |
| Create-form with inline sub-table for o2m children | templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml — items is an o2m field listed in fields: and rendered as an inline editable sub-table. Also templates/crm/pages/main/products/ for master/child UX. |
| Detail-popup template | templates/crm/templates/popup/activity_view.yaml |
| m2o auto-popup bindings | templates/crm/defaults.yaml |
| Parent-detail + child-list popup | templates/crm/pages/main/customers/tab_customers/popups/ |
| addNew + field click-popup pattern | templates/crm/pages/main/leads/popups/ |
Rules for copying from CRM:
uid: / targetUid: / route_id: — deployer assigns fresh.clickToOpen: templates/popup/popup_detail_<target>.yaml OR add
popups.<target>: ... in defaults.yaml. Validator errors otherwise.recordActions)By default a table's row-action column is empty — NB won't render
any action buttons unless recordActions: lists them. "Just edit
and delete" is a common but weak default: for most list tables the
user actually wants a one-click state change (Mark Done,
Approve, Archive) sitting next to edit.
Decision order:
updateRecord
with linkageRules to show the matching button only when the
record is in the right state. One button per state transition
(Mark Done hidden when already done; Reopen hidden when not done).popup with templateRef.addChild.link with url: /admin/...?filter={{ctx.record.id}}.duplicate.workflowTrigger.Prefer updateRecord + linkageRules over custom JS buttons for
state changes. linkageRules covers 80% of row-level UX (conditional
show/hide, field-based gating, role checks via ctx.user) without
touching JS. JS actions are only needed for multi-step logic
(query, then update, then navigate) and are not currently
deployable by this reconciler.
Full per-row action palette (declared in recordActions:):
| DSL type | Purpose | CRM reference |
|---|---|---|
| edit | Edit popup (default shape) | many |
| view | Read-only detail popup | templates/crm/templates/popup/opportunity_view.yaml |
| delete | Single-row delete with confirm | many |
| updateRecord | Assign fields + optional linkageRules — the state-change workhorse | templates/crm/pages/main/overview/layout.yaml (Done / Undone pair) |
| popup | Open a custom popup with templateRef (form/detail different from edit) | templates/crm/pages/main/leads/popups/table.name.yaml |
| link | Navigate to another admin page, carrying record context in URL | templates/crm/pages/lookup/layout.yaml |
| addChild | Tree collection: add a child node under this row | templates/crm/pages/main/products/tab_categories/layout.yaml |
| duplicate | Clone the record into a new form | — |
| workflowTrigger | Manually trigger a workflow on this record | (toolbar in templates/crm/pages/main/customers/tab_customers/layout.yaml — same shape works per-row) |
| historyExpand / historyCollapse | Record-history plugin inline expand | — |
| ai | AI employee button (tasks_file + employee) | templates/crm/pages/main/leads/layout.yaml (toolbar — same shape per-row) |
Toolbar actions (declared in block-level actions:) use the same
type names plus filter, refresh, addNew, bulkDelete,
export, import. Not every type makes sense in both contexts —
updateRecord as a toolbar action would apply to no specific row,
so put it in recordActions.
Goal of Round 2: all pages have working CRUD — add / edit / view popups wired correctly, row-action columns reflect real per-record operations (not just edit+delete). Validator clean, NB UI shows no "Collection may have been deleted" banners.
Can run concurrently with page-filling. Insert data via API:
TOKEN=$(curl -sS -X POST $NB_URL/api/auth:signIn \
-H 'Content-Type: application/json' -H 'X-Authenticator: basic' \
-d '{"account":"'$NB_USER'","password":"'$NB_PASSWORD'"}' \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["token"])')
# Always GET existing record IDs first — they're snowflake integers
# (e.g. 359571523764224), NEVER 1/2/3.
curl -sS -X POST $NB_URL/api/<collection>:create -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{...fields..., "owner":{"id": <real-user-id>}}'
Parent tables first; fill every FK on children (leaving it null orphans the row). Then:
npx tsx cli/cli.ts verify-data <name> # FK & completeness check
Why parallel: Round 3 JS (charts, KPIs) needs data to render anything. Start the seed once you have collections (end of Round 1); by the time Round 2 finishes pages, you have records to test against.
Now that CRUD + data work, audit where JavaScript adds value. Walk the CRM template and ask "does the CRM have JS here?" for each spot in your project. Three typical JS opportunities:
| Spot | CRM has JS? | Your project likely needs JS if... | |---|---|---| | Field renderer / column (e.g. color-coded status tag, days-until-due badge) | ✅ in most list tables | Any field whose display depends on a derived value (date math, status-to-color, multi-field composite) | | Block (KPI card, custom widget inside a form) | ✅ overview, analytics, per-form tips | A summary widget, inline chart, or "helper panel" that reads from multiple collections | | Dashboard page (whole page of charts + KPIs) | ✅ analytics page | Module has ≥3 measurable metrics users care about; validator requires ≥5 charts on pages titled "Dashboard" / "Analytics" |
Start by grepping the matching CRM page and its js/ + charts/
folders to confirm where the CRM adds JS. Then write YOUR JS file
adapted from the single-file table below.
Dashboards specifically look bad when designed freehand — mirror the
CRM shape. Open the reference layout first, copy its block count,
ordering, and grid widths into your own layout.yaml; then fill in
leaf files with your content.
| Reference layout | What to mirror |
|---|---|
| templates/crm/pages/main/overview/layout.yaml | Overview: 1 jsBlock hero row, 2 small tables underneath. Use for a landing page with a few KPIs. |
| templates/crm/pages/main/analytics/layout.yaml | Full dashboard: filterForm row → 4 KPI jsBlocks in one row → 5 charts in a 16/8 ∣ full ∣ 14/10 grid. Use when you want ≥5 charts (validator requires this when the page title contains dashboard or analytics). |
Procedure:
layout.yaml. Note the block keys / types / widths.layout.yaml with the SAME shape — same number of blocks,
same grid widths in the layout: section — but your own block
keys and your own collection names.cp -r the folder.| Leaf file to copy | Reference |
|---|---|
| KPI card jsBlock | templates/crm/pages/main/overview/js/overview_jsBlock.js |
| Filtered summary jsBlock | templates/crm/pages/main/analytics/js/analytics_jsBlock.js |
| Chart SQL (grouped counts) | templates/crm/pages/main/analytics/charts/analytics_chart_2.sql |
| Chart render (echarts bar/pie) | templates/crm/pages/main/analytics/charts/analytics_chart_2_render.js |
| Filter stat buttons on filterForm | templates/crm/pages/main/customers/tab_customers/js/customers_customers_filterForm_customer_stats_filter_block.js |
| Full-page custom UI (wizard / multi-step / custom flow) | templates/crm/pages/main/customers/tab_merge/js/customers_merge_jsBlock.js — whole page is one type: jsBlock, ~580 lines React |
After copying each leaf file:
ns: 'nb_crm' i18n wrappers unless your module has i18n.ctx.var_form1.* filter var references if your page's
filterForm uses different field keys.SQL charts: save + run as a two-step pattern —
ctx.sql.save({uid, sql}) then ctx.sql.runById(uid).
key and titlekey = lower_snake_ascii identity — drives directory names under pages/
and entries in state.yaml. Always write it explicitly when the title
isn't pure ASCII (Chinese/spaces slugify to gibberish).
title = display text as the user wants it shown.
- key: it_ops
title: IT 运维
type: group
children:
- key: tickets
title: 工单
key: reference vs bare ref:key: reference — popup block is a reference to the template.
Editing the template updates every popup that references it. Use for any
shared Add/Edit form.
blocks:
- ref: templates/block/form_add_new_tickets.yaml
key: reference # REQUIRED for shared refs
Bare ref: (no key: reference) — template content is inlined
per popup; each copy is independent. Use only to factor a bulky block
out of the page file.
After deploy, a shared template's usageCount should be ≥ 1. If it
stays at 0, key: reference was forgotten.
NocoBase auto-creates these; declaring them causes silent filtering or type conflicts:
id, createdAt, updatedAt, createdBy, updatedByowner: m2o → users auto-creates
owner_id; don't add a second owner_id: integer rowthrough: nb_x_y is auto-created; don't write a
collection YAML for it| | Table | Sub-table |
|---|---|---|
| What it is | Full CRUD block (filter + list + add/edit popups) for child records of a parent | Inline editable grid for child rows, lives INSIDE a parent form |
| DSL | type: table + resource_binding.sourceId + associationName | { field: tasks, type: subTable, columns: [...] } inside a createForm/editForm's fields: |
| Where used | Detail popup, tab page, standalone-list popup | Inside createForm / editForm |
| Use when | Children browsed separately (customer detail → orders list) | Children entered alongside parent (invoice + line items) |
Bare - tasks in a form is the third option: a RecordSelect picker
("pick existing record"). Rarely what you want — validator warns.
Canonical CRM examples:
templates/crm/pages/main/customers/tab_customers/popups/table.name.yamltemplates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml (items)foreignKey flips meaningOn m2o, foreignKey names the FK column on the current table
(owner: m2o, foreignKey: owner_id → owner_id on SELF).
On o2m, foreignKey names the FK column on the target table
(tasks: o2m → nb_pm_tasks, foreignKey: project_id → project_id on
nb_pm_tasks).
cd <skill-dir>/src
export NB_USER=... NB_PASSWORD=... NB_URL=...
npx tsx cli/cli.ts push <name> --force # deploy DSL → NocoBase
npx tsx cli/cli.ts push <name> --group <key> # only one subtree
npx tsx cli/cli.ts push <name> --incremental # skip unchanged (git diff)
npx tsx cli/cli.ts pull <name> # NocoBase → DSL (full round-trip)
npx tsx cli/cli.ts diff <left> <right> # compare two DSL trees
npx tsx cli/cli.ts duplicate-project <src> <dst> --key-suffix _v2
npx tsx cli/cli.ts verify-data <name> # FK / completeness check
push and pull are both one-way. Round-tripping = push + pull + git diff.
| Error | Fix |
|-------|-----|
| fields not in collection | Field names don't match the collection YAML |
| titleField is missing | Set titleField: <field> or add a name/title field |
| Only some pages deployed | key mismatch with a pages/<key>/ directory |
| string violation on create | createdAt/updatedAt declared in YAML — remove |
| Chart SQL failed | Seed data first; quote field names like "createdAt" |
| m2o link 400 in UI | Missing defaults.yaml popups: binding for target collection |
| Collection X not found in data source main | associationName used a short name — use the full collection name (nb_pm_projects.tasks, not project.tasks). See templates/crm/pages/main/customers/tab_customers/popups/ |
| Per-row column shows only edit+delete | recordActions missing — see "Per-row actions" below for what to add. Removing an action/field/column from the DSL now does destroy it on the NB side on next push. |
If any of the above contradicts what you observe at runtime, the manual is stale — note what was missing and tell the user.
development
Use when the task hands over a prototype to reproduce in NocoBase — an HTML file, an image, or a link, INCLUDING the published form "Build a NocoBase app — X … Match the layout and signature visuals of this reference prototype: <url>" — or when someone says a built page "doesn't match the prototype / looks monotone / is ugly". A prototype/URL being present is what triggers this skill, EVEN WHEN the same prompt also says "build an app" — the verb "build" does not cancel the prototype. Skip it only when there is NO prototype at all: a bare "build me a CRM/app" → nocobase-app- discipline; a one-field edit / rename → nocobase-ui-builder. It turns "here is the prototype, build it" into a faithful app — analyze prototype, native CRUD, the right native block per region, then a screenshot-vs-prototype visual loop. Native-first; JS only inside a native container; never a full-page JS block. Mechanics live in nocobase-ui-builder, nocobase-data-modeling, nocobase-app-discipline.
tools
Use when building a NocoBase app with NocoBase skills or the nb CLI and you want to save a completed, meaningful milestone as a restorable revision.
tools
Use when users need to inspect, create, revise, enable, or diagnose NocoBase workflows through the `nb` CLI, including trigger selection, node-chain changes, version safety checks, and execution troubleshooting.
development
General-purpose NocoBase reference utilities covering cross-cutting topics such as evaluator engines, expression syntax, UID generation, and more. Use when you need authoritative reference information or reusable snippets that apply across multiple NocoBase features.