mcpField
N
nocobase-dsl-reconciler

nocobase-dsl-reconciler

github.com/nocobaseDevelopment55242 installsUpdated 8w ago
Repository

About nocobase-dsl-reconciler

nocobase-dsl-reconciler is an opt-in skill for building NocoBase applications from committed YAML DSL files and deploying them with the CLI. It guides incremental builds from a starter, validates specifications before deployment, and distinguishes DSL work from live-UI changes.

Key features

  1. 01Scaffold from the starter template
  2. 02Define collections, routes, pages, and popups in YAML
  3. 03Push changes to NocoBase with the CLI
  4. 04Deploy targeted groups or pages incrementally
  5. 05Validate DSL specifications before deployment

Use cases

  1. 01Build a CRUD module as committed YAML
  2. 02Extend an app with collections and pages in stages
  3. 03Plan multi-collection structures before writing YAML
  4. 04Deploy DSL changes to selected pages

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/nocobase/skills --skill nocobase-dsl-reconciler

After installing, type/nocobase-dsl-reconcilerin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

exports/crm-opportunities/
structure.yaml
exports/crm-opportunities/js/
opportunities_filterForm_0_pipeline_stats_filter_block.js
exports/crm-v2/
enhance.yaml
exports/crm-v2/charts/
analytics_chart_1.json
analytics_chart_2.json
analytics_chart_3.json
analytics_chart_4.json
analytics_chart_5.json
exports/crm-v2/js/
analytics_filterForm_0_js_0.js
analytics_jsBlock_6.js
analytics_jsBlock_7.js
analytics_jsBlock_8.js
analytics_jsBlock_9.js
customers_filterForm_1_customer_stats_filter_block.js
customers_popup_name_tab0_contacts_contact_row_column_customer_360.js
customers_popup_name_tab0_details_0_customer_statistics_block.js
customers_popup_name_tab0_details_0_item_profile_customer_profile_header.js
customers_popup_name_tab0_details_1_ai_smart_profile_block.js
customers_popup_name_tab0_opportunities_opportunity_row_column_customer_360.js
customers_table_0_col_health_score.js
customers_table_0_col_last_activity.js
leads_filterForm_1_leads_stats_filter_block.js
leads_popup_name_tab0_details_0_lead_badges_row_component.js
leads_popup_name_tab0_details_0_lead_key_stats_block.js
leads_popup_name_tab0_details_5_lead_stage_flow_block.js
leads_popup_name_tab0_lead_insights_lead_ai_insights_block.js
leads_popup_name_tab0_lead_owner_lead_profile_header_block.js
leads_table_0_col_ai_score.js
leads_table_0_col_contact.js
leads_table_0_col_created.js

Details

From the project README

NocoBase Application Builder (DSL path)

Before you use this skill

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.

Golden rule

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.

When NOT to use this skill

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 wantednocobase-ui-builder
ACL / role permissions / route permissionsnocobase-acl-manage
Workflow create / update / revision / executionnocobase-workflow-manage (this skill only wires the trigger button; authoring the graph goes there)
Collection / field / relation authoring outside a DSL projectnocobase-data-modeling
Plugin development (.tsx components, server code)nocobase-plugin-development
Install / enable pluginnocobase-plugin-manage
Environment setup / app install / upgradenocobase-env-manage

Any change that should live as a committed YAML file under workspaces/<project>/ — stays here.

Environment

cd <skill-dir>/src
export [email protected] NB_PASSWORD=admin123 NB_URL=http://localhost:14000

Quick start — new build

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.

Customizing the starter (the agile loop)

Iterate one concern at a time, push between each:

  1. Rename identifiers to match the user's domain.
    • collections/nb_starter_projects.yaml → your collection name (match the nb_<module>_<entity> convention)
    • routes.yaml: change "Starter" / "Projects" titles
    • pages/starter/ directory name if you want (match routes.yaml key)
    • Find-replace nb_starter_projects across all files
  2. Adjust the field list in your collection YAML to match the user's entity. Update pages/<...>/layout.yaml fields: and the popup templates' field_layout: accordingly.
  3. Add a 2nd entity only when the first one works end-to-end. Create a new collections/*.yaml + pages/<module>/<entity>/ dir, copy pages/starter/projects/layout.yaml as a starting point.
  4. Extend incrementally: add a tab, a chart, a workflow trigger. Push after every change. See "Advanced patterns" below for which CRM file matches which pattern.

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.

Fast-track: when --copy helps

Pass --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.

Incremental edits — existing workspace

  • Add a block / field / action / popup → write the DSL → push.
  • Remove from DSL → push. The reconciler destroys the matching live model on the NB side and cleans state.yaml. Manual NB-UI authored elements (not tracked in state.yaml) are left alone.
  • Rename → not supported automatically. Delete + re-add.

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 push

For pure live-UI tweaks without a DSL commit, hand off to nocobase-ui-builder instead (see the routing table above).

Advanced workflow — when the starter isn't enough

Triggers for going beyond the agile starter loop:

  • More than ~3 collections with cross-relations (m2m, tree structures)
  • Dedicated workflow / approval / permission logic that the user wants designed up-front
  • Multi-tab pages, sub-tables, or cross-module navigation
  • Dashboard with bespoke KPIs mapped to the user's domain language

Progression: Round 0 design → Round 1 scaffold → Round 2 fill → Round 2' seed → Round 3 JS. Each round is a deployable state.

Round 0: System architecture — confirm with user

Write a DESIGN.md (markdown, not YAML) covering:

  1. Collections — every table, its fields, and its relations. See nocobase-data-modeling skill for field-interface reference.
  2. Page list — every page, one-line purpose each, grouped by menu.
  3. Navigation wiring — which m2o fields open which popup templates; which pages link to each other.

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)
Round 0.5: Sub-agent CWD (only when spawning)

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.

Round 1: Scaffold — still start from the starter

Even in the advanced path, don't hand-write routes.yaml + collections/*.yaml from scratch. Copy the starter, then grow:

StepWhat to do
Basecp -r templates/starter workspaces/<name> — push once
Add collectionWrite collections/<next_coll>.yaml (format matches starter's). Match nb_<module>_<entity> convention.
Add pagemkdir pages/<module>/<page>/ + layout.yaml; mirror starter's projects layout.
Update routesAdd entry under the existing group in routes.yaml.
Deploycli push <name> --force after each collection/page addition

CRM references (consult only when stuck on structure):

  • templates/crm/collections/nb_crm_leads.yaml — collection format
  • templates/crm/routes.yaml — multi-group routes (shape only)
Round 2: Fill content — blocks, popups, templates

For each page beyond the starter basics:

BuildingCRM reference
Main list table + filtertemplates/crm/pages/main/leads/layout.yaml
Multi-tab pagetemplates/crm/pages/main/customers/ (page.yaml + tab_*/layout.yaml)
Create-form with inline sub-table for o2m childrentemplates/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 templatetemplates/crm/templates/popup/activity_view.yaml
m2o auto-popup bindingstemplates/crm/defaults.yaml
Parent-detail + child-list popuptemplates/crm/pages/main/customers/tab_customers/popups/
addNew + field click-popup patterntemplates/crm/pages/main/leads/popups/

Rules for copying from CRM:

  • Copy 10–30 lines, adapt names. Never copy whole files.
  • Don't copy uid: / targetUid: / route_id: — deployer assigns fresh.
  • For every m2o field displayed in a table, either set clickToOpen: templates/popup/popup_detail_<target>.yaml OR add popups.<target>: ... in defaults.yaml. Validator errors otherwise.
Per-row actions (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:

  1. Is there a boolean / enum status field? → add 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).
  2. Does the record need a second detail/form view different from the default edit popup? → popup with templateRef.
  3. Tree/hierarchy collection? → add addChild.
  4. Need to navigate elsewhere with this record's id/filter? → link with url: /admin/...?filter={{ctx.record.id}}.
  5. Want to let the user clone a complex record? → duplicate.
  6. Need to start a workflow manually? → 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 typePurposeCRM reference
editEdit popup (default shape)many
viewRead-only detail popuptemplates/crm/templates/popup/opportunity_view.yaml
deleteSingle-row delete with confirmmany
updateRecordAssign fields + optional linkageRules — the state-change workhorsetemplates/crm/pages/main/overview/layout.yaml (Done / Undone pair)
popupOpen a custom popup with templateRef (form/detail different from edit)templates/crm/pages/main/leads/popups/table.name.yaml
linkNavigate to another admin page, carrying record context in URLtemplates/crm/pages/lookup/layout.yaml
addChildTree collection: add a child node under this rowtemplates/crm/pages/main/products/tab_categories/layout.yaml
duplicateClone the record into a new form—
workflowTriggerManually trigger a workflow on this record(toolbar in templates/crm/pages/main/customers/tab_customers/layout.yaml — same shape works per-row)
historyExpand / historyCollapseRecord-history plugin inline expand—
aiAI 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.

Round 2': Test data (parallel with Round 2)

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.

Round 3: JS — where CRM uses it, you probably need it

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:

SpotCRM has JS?Your project likely needs JS if...
Field renderer / column (e.g. color-coded status tag, days-until-due badge)✅ in most list tablesAny 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 tipsA summary widget, inline chart, or "helper panel" that reads from multiple collections
Dashboard page (whole page of charts + KPIs)✅ analytics pageModule 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 layoutWhat to mirror
templates/crm/pages/main/overview/layout.yamlOverview: 1 jsBlock hero row, 2 small tables underneath. Use for a landing page with a few KPIs.
templates/crm/pages/main/analytics/layout.yamlFull 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:

  1. Open the reference layout.yaml. Note the block keys / types / widths.
  2. Write YOUR 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.
  3. For each block's leaf JS/SQL file, copy from the single-file table below. Copy files individually; do not cp -r the folder.
Leaf file to copyReference
KPI card jsBlocktemplates/crm/pages/main/overview/js/overview_jsBlock.js
Filtered summary jsBlocktemplates/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 filterFormtemplates/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:

  • Rename in place and retarget SQL/collection/field names.
  • Remove ns: 'nb_crm' i18n wrappers unless your module has i18n.
  • Simplify 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).

Core concepts

Two identifiers: key and title

key = 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: 工单
Two popup modes: 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.

Auto-created columns — do NOT declare them

NocoBase auto-creates these; declaring them causes silent filtering or type conflicts:

  • System columns: id, createdAt, updatedAt, createdBy, updatedBy
  • m2o / o2m FK columns: declaring owner: m2o → users auto-creates owner_id; don't add a second owner_id: integer row
  • m2m join tables: through: nb_x_y is auto-created; don't write a collection YAML for it
Table vs sub-table — two different things, don't confuse
TableSub-table
What it isFull CRUD block (filter + list + add/edit popups) for child records of a parentInline editable grid for child rows, lives INSIDE a parent form
DSLtype: table + resource_binding.sourceId + associationName{ field: tasks, type: subTable, columns: [...] } inside a createForm/editForm's fields:
Where usedDetail popup, tab page, standalone-list popupInside createForm / editForm
Use whenChildren 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:

  • Table (standalone CRUD block): templates/crm/pages/main/customers/tab_customers/popups/table.name.yaml
  • Sub-table (inline editable grid): templates/crm/templates/block/form_add_new_opportunities_quotations_quotations.yaml (items)
foreignKey flips meaning

On 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).

Command reference

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.

Common errors

ErrorFix
fields not in collectionField names don't match the collection YAML
titleField is missingSet titleField: <field> or add a name/title field
Only some pages deployedkey mismatch with a pages/<key>/ directory
string violation on createcreatedAt/updatedAt declared in YAML — remove
Chart SQL failedSeed data first; quote field names like "createdAt"
m2o link 400 in UIMissing defaults.yaml popups: binding for target collection
Collection X not found in data source mainassociationName 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+deleterecordActions 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.

Comments

Markdown supported