@sparkerp/plugin-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/bundle/blocks.json +12229 -0
  4. package/bundle/catalog.json +32475 -0
  5. package/bundle/docs/applications/hcm/employee-profile.md +144 -0
  6. package/bundle/docs/concepts/artifact-types.md +80 -0
  7. package/bundle/docs/concepts/glossary.md +124 -0
  8. package/bundle/docs/concepts/overview.md +88 -0
  9. package/bundle/docs/concepts/page-data-flow.md +84 -0
  10. package/bundle/docs/concepts/plugin-lifecycle.md +81 -0
  11. package/bundle/docs/concepts/sdk-modes.md +71 -0
  12. package/bundle/docs/docs.json +109 -0
  13. package/bundle/docs/guides/add-a-cadence-job.md +134 -0
  14. package/bundle/docs/guides/add-a-compliance-job.md +139 -0
  15. package/bundle/docs/guides/add-a-create-edit-form.md +167 -0
  16. package/bundle/docs/guides/add-a-custom-block.md +225 -0
  17. package/bundle/docs/guides/add-a-data-provider.md +216 -0
  18. package/bundle/docs/guides/add-a-kpi.md +154 -0
  19. package/bundle/docs/guides/add-a-reminder-job.md +181 -0
  20. package/bundle/docs/guides/add-an-aggregation-job.md +153 -0
  21. package/bundle/docs/guides/add-an-approval-workflow.md +222 -0
  22. package/bundle/docs/guides/add-an-entity.md +170 -0
  23. package/bundle/docs/guides/add-business-rules.md +147 -0
  24. package/bundle/docs/guides/add-i18n.md +121 -0
  25. package/bundle/docs/guides/add-menus.md +149 -0
  26. package/bundle/docs/guides/build-a-code-plugin.md +141 -0
  27. package/bundle/docs/guides/build-a-page.md +213 -0
  28. package/bundle/docs/guides/build-a-polyglot-service-plugin.md +201 -0
  29. package/bundle/docs/guides/build-a-tenant-extension-service.md +149 -0
  30. package/bundle/docs/guides/create-a-plugin.md +172 -0
  31. package/bundle/docs/guides/expose-a-plugin-operation-as-an-ai-tool.md +114 -0
  32. package/bundle/docs/guides/index.md +53 -0
  33. package/bundle/docs/guides/publish-and-upgrade.md +152 -0
  34. package/bundle/docs/guides/responsive-plugin.md +117 -0
  35. package/bundle/docs/guides/seed-data-on-install.md +127 -0
  36. package/bundle/docs/guides/set-up-the-sdk.md +120 -0
  37. package/bundle/docs/guides/use-the-mcp-server.md +104 -0
  38. package/bundle/docs/guides/validate-and-test.md +119 -0
  39. package/bundle/docs/guides/wire-a-pages-data.md +153 -0
  40. package/bundle/docs/quickstart/index.md +118 -0
  41. package/bundle/docs/recipes/condition-based-compliance-flag.md +70 -0
  42. package/bundle/docs/recipes/cross-plugin-offboarding-fanout.md +68 -0
  43. package/bundle/docs/recipes/cross-row-aggregation.md +67 -0
  44. package/bundle/docs/recipes/entity-with-approval-workflow.md +72 -0
  45. package/bundle/docs/recipes/index.md +40 -0
  46. package/bundle/docs/recipes/install-lifecycle-data-seeding.md +76 -0
  47. package/bundle/docs/recipes/kpi-dashboard-page.md +92 -0
  48. package/bundle/docs/recipes/master-detail-page.md +84 -0
  49. package/bundle/docs/recipes/n-days-before-reminder.md +72 -0
  50. package/bundle/docs/recipes/per-row-document-generator.md +65 -0
  51. package/bundle/docs/recipes/related-record-lookup-field.md +74 -0
  52. package/bundle/docs/recipes/resolved-reference-grid-column.md +63 -0
  53. package/bundle/docs/recipes/scheduled-record-generation.md +68 -0
  54. package/bundle/docs/reference/action.md +18 -0
  55. package/bundle/docs/reference/block-definition.md +35 -0
  56. package/bundle/docs/reference/block-instance.md +33 -0
  57. package/bundle/docs/reference/composite-pack-manifest.md +33 -0
  58. package/bundle/docs/reference/connector-definition.md +36 -0
  59. package/bundle/docs/reference/data-service-definition.md +22 -0
  60. package/bundle/docs/reference/data-view-definition.md +26 -0
  61. package/bundle/docs/reference/entity-aggregation-config.md +38 -0
  62. package/bundle/docs/reference/entity-cadence-config.md +33 -0
  63. package/bundle/docs/reference/entity-compliance-config.md +37 -0
  64. package/bundle/docs/reference/entity-cross-plugin-action-config.md +37 -0
  65. package/bundle/docs/reference/entity-definition.md +18 -0
  66. package/bundle/docs/reference/entity-document-generator-config.md +35 -0
  67. package/bundle/docs/reference/entity-rule-definition.md +29 -0
  68. package/bundle/docs/reference/entity-status-date-sweep-config.md +27 -0
  69. package/bundle/docs/reference/form.md +35 -0
  70. package/bundle/docs/reference/index.md +37 -0
  71. package/bundle/docs/reference/menu.md +26 -0
  72. package/bundle/docs/reference/mobile-nav.md +22 -0
  73. package/bundle/docs/reference/page-template.md +29 -0
  74. package/bundle/docs/reference/page.md +44 -0
  75. package/bundle/docs/reference/plugin-manifest.md +54 -0
  76. package/bundle/docs/reference/plugin-seed-data.md +21 -0
  77. package/bundle/docs/reference/print-template-definition.md +22 -0
  78. package/bundle/docs/reference/provider-definition.md +22 -0
  79. package/bundle/docs/reference/report-definition.md +22 -0
  80. package/bundle/docs/reference/rule.md +30 -0
  81. package/bundle/docs/reference/workflow-definition.md +28 -0
  82. package/bundle/docs/troubleshooting/grid-shows-zero-rows.md +82 -0
  83. package/bundle/docs/troubleshooting/job-never-runs.md +89 -0
  84. package/bundle/docs/troubleshooting/known-gotchas.md +193 -0
  85. package/bundle/docs/troubleshooting/publishing-errors.md +119 -0
  86. package/bundle/docs/tutorial/01-create-the-plugin.md +157 -0
  87. package/bundle/docs/tutorial/02-category-entity-and-seed.md +94 -0
  88. package/bundle/docs/tutorial/03-equipment-entity.md +121 -0
  89. package/bundle/docs/tutorial/04-equipment-register-page.md +153 -0
  90. package/bundle/docs/tutorial/05-checkout-entity-and-page.md +116 -0
  91. package/bundle/docs/tutorial/06-checkout-approval-workflow.md +139 -0
  92. package/bundle/docs/tutorial/07-return-due-reminder-job.md +129 -0
  93. package/bundle/docs/tutorial/08-menus-i18n-publish.md +123 -0
  94. package/bundle/docs/tutorial/index.md +72 -0
  95. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-aggregation-configs.json +18 -0
  96. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cadence-configs.json +9 -0
  97. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-compliance-configs.json +74 -0
  98. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cross-plugin-configs.json +9 -0
  99. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-document-configs.json +9 -0
  100. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-sweep-configs.json +260 -0
  101. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/entities/hcm_salary_planning_entry.json +41 -0
  102. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/page/salary-planning.json +1 -0
  103. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/page/promotions.json +10944 -0
  104. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.movement.json +44 -0
  105. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.promotion-acknowledge.json +19 -0
  106. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.resignation.json +29 -0
  107. package/bundle/examples/example-patterns.mjs +94 -0
  108. package/bundle/manifest.json +14 -0
  109. package/bundle/schemas/action.schema.json +102 -0
  110. package/bundle/schemas/block-definition.schema.json +222 -0
  111. package/bundle/schemas/block-instance.schema.json +139 -0
  112. package/bundle/schemas/composite-pack-manifest.schema.json +160 -0
  113. package/bundle/schemas/connector-definition.schema.json +29 -0
  114. package/bundle/schemas/data-service-definition.schema.json +64 -0
  115. package/bundle/schemas/data-view-definition.schema.json +148 -0
  116. package/bundle/schemas/entity-aggregation-config.schema.json +32 -0
  117. package/bundle/schemas/entity-cadence-config.schema.json +27 -0
  118. package/bundle/schemas/entity-compliance-config.schema.json +31 -0
  119. package/bundle/schemas/entity-cross-plugin-action-config.schema.json +31 -0
  120. package/bundle/schemas/entity-definition.schema.json +126 -0
  121. package/bundle/schemas/entity-document-generator-config.schema.json +29 -0
  122. package/bundle/schemas/entity-rule-definition.schema.json +75 -0
  123. package/bundle/schemas/entity-status-date-sweep-config.schema.json +21 -0
  124. package/bundle/schemas/form.schema.json +281 -0
  125. package/bundle/schemas/menu.schema.json +106 -0
  126. package/bundle/schemas/mobile-nav.schema.json +53 -0
  127. package/bundle/schemas/page-template.schema.json +46 -0
  128. package/bundle/schemas/page.schema.json +292 -0
  129. package/bundle/schemas/plugin-manifest.schema.json +139 -0
  130. package/bundle/schemas/plugin-seed-data.schema.json +24 -0
  131. package/bundle/schemas/print-template-definition.schema.json +90 -0
  132. package/bundle/schemas/provider-definition.schema.json +27 -0
  133. package/bundle/schemas/report-definition.schema.json +96 -0
  134. package/bundle/schemas/rule.schema.json +82 -0
  135. package/bundle/schemas/workflow-definition.schema.json +42 -0
  136. package/bundle/validators/block-engine.mjs +23222 -0
  137. package/bundle/validators/page-engine.mjs +21645 -0
  138. package/erp-cli/authoring-root.mjs +344 -0
  139. package/erp-cli/erp.mjs +4065 -0
  140. package/erp-cli/example-patterns.mjs +94 -0
  141. package/erp-mcp-server/index.mjs +818 -0
  142. package/package.json +27 -0
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: A KPI dashboard page
3
+ audience: tenant
4
+ ---
5
+
6
+ # A KPI dashboard page
7
+
8
+ ## When you need this
9
+
10
+ A page (or the top of a list page) showing several headline numbers computed
11
+ server-side, fetched in **one** call, refreshed after writes.
12
+
13
+ ## The pieces
14
+
15
+ ### One `count` Data Service per metric
16
+
17
+ ```json
18
+ {
19
+ "name": "oeq-equipment-count-available",
20
+ "definition": {
21
+ "operation": "count",
22
+ "source": { "kind": "entity", "entityName": "oeq_equipment" },
23
+ "filters": [ { "field": "status", "operator": "eq", "value": "AVAILABLE" } ],
24
+ "parameters": []
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### One `composite` Data Service wrapping them
30
+
31
+ `metadata/data_service/oeq-equipment-kpis.json`
32
+ ([real file](../tutorial/example-plugin/spk-assembly/metadata/data_service/oeq-equipment-kpis.json)):
33
+
34
+ ```json
35
+ {
36
+ "name": "oeq-equipment-kpis",
37
+ "definition": {
38
+ "operation": "composite",
39
+ "source": { "kind": "entity", "entityName": "oeq_equipment" },
40
+ "steps": [
41
+ { "as": "total", "service": "oeq-equipment-count-total", "parameters": {} },
42
+ { "as": "available", "service": "oeq-equipment-count-available", "parameters": {} },
43
+ { "as": "assigned", "service": "oeq-equipment-count-assigned", "parameters": {} }
44
+ ]
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### The page mount loader
50
+
51
+ ```json
52
+ "events": { "mounted": { "source": "action-chain", "actions": [
53
+ { "id": "a0", "order": 0, "type": "callApi",
54
+ "config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-equipment-kpis/execute", "httpMethod": "POST", "params": {} }, "output": "kpi" },
55
+ { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.kpiTotal", "value": "${kpi.results.total.value}" } },
56
+ { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.kpiAvailable", "value": "${kpi.results.available.value}" } },
57
+ { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.kpiAssigned", "value": "${kpi.results.assigned.value}" } }
58
+ ] } }
59
+ ```
60
+
61
+ ### The cards
62
+
63
+ ```json
64
+ {
65
+ "blockType": "core.kpi-card",
66
+ "properties": {
67
+ "value": { "source": "binding", "binding": { "scope": "page", "key": "kpiTotal" } },
68
+ "labelKey": { "source": "static", "value": "office-equipment.equipment-register.kpi.total" },
69
+ "format": { "source": "static", "value": "number" },
70
+ "colorToken": { "source": "static", "value": "primary" }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## The rule that bites
76
+
77
+ `count` → `${out.value}`. `composite` → **`${out.results.<step>.value}`**. Never
78
+ `.total` (that's the unrelated list-pagination envelope).
79
+
80
+ Real response from the tutorial:
81
+
82
+ ```json
83
+ { "results": { "total": { "value": 0 }, "available": { "value": 0 }, "assigned": { "value": 0 } } }
84
+ ```
85
+
86
+ To refresh after a write, append the same `callApi` + `setValue` steps to the
87
+ Save action chain.
88
+
89
+ ## Related
90
+
91
+ - [Add a KPI or aggregation](../guides/add-a-kpi.md)
92
+ - [A cross-row aggregation into a summary entity](./cross-row-aggregation.md) — for numbers stored *back* into a table
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: A master-detail page with a child grid
3
+ audience: tenant
4
+ ---
5
+
6
+ # A master-detail page with a child grid
7
+
8
+ ## When you need this
9
+
10
+ A list where clicking a row opens a panel showing that record **plus** a list of
11
+ its children — a category and its equipment types, a maintenance record and its
12
+ line items, a checkout and its history.
13
+
14
+ ## The shape
15
+
16
+ 1. **The master grid** — a normal `core.grid` wired via `metadata.dataSource`.
17
+ 2. **`rowClicked`** on the grid runs an action chain that:
18
+ - fetches the master record → `page.detailRecord`;
19
+ - calls a **parameterized Data Service** for the children, passing the master
20
+ id → `page.childItems`;
21
+ - opens `page.detailDialogOpen`.
22
+ 3. **A `core.dialog`** bound to `page.detailDialogOpen` containing an overview
23
+ card bound to `page.detailRecord.*` and a `core.list` (or nested `core.grid`)
24
+ bound to `page.childItems`.
25
+
26
+ ## The `rowClicked` chain
27
+
28
+ ```json
29
+ "events": { "rowClicked": { "source": "action-chain", "actions": [
30
+ { "id": "a0", "order": 0, "type": "callApi",
31
+ "config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_category/records/${event.id}", "httpMethod": "GET", "params": {} },
32
+ "output": "detail" },
33
+ { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.detailRecord", "value": "${detail}" } },
34
+ { "id": "a2", "order": 2, "type": "callApi",
35
+ "config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-type-list/execute", "httpMethod": "POST",
36
+ "params": { "parameters": { "parentId": "${event.id}" } } },
37
+ "output": "children" },
38
+ { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.childItems", "value": "${children.rows}" } },
39
+ { "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.detailDialogOpen", "value": true } }
40
+ ] } }
41
+ ```
42
+
43
+ ## The child Data Service
44
+
45
+ ```json
46
+ {
47
+ "name": "oeq-type-list",
48
+ "definition": {
49
+ "operation": "search",
50
+ "source": { "kind": "entity", "entityName": "oeq_type" },
51
+ "filters": [ { "field": "category_id", "operator": "eq", "value": "${param.parentId}" } ],
52
+ "parameters": [ { "name": "parentId", "type": "string" } ]
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## The child list block
58
+
59
+ ```json
60
+ {
61
+ "blockType": "core.list",
62
+ "properties": {
63
+ "items": { "source": "binding", "binding": { "scope": "page", "key": "childItems" } },
64
+ "primaryPath": { "source": "static", "value": "name" },
65
+ "secondaryPath": { "source": "static", "value": "code" }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## The rules that bite
71
+
72
+ - **`${event.id}`** is the clicked master row's id — available in `rowClicked`.
73
+ - A parameterized Data Service is called with args nested under
74
+ **`parameters`**: `{ "params": { "parameters": { "parentId": "..." } } }`.
75
+ - Bind the child list to `${children.rows}` (or `.items` / `.records` — check
76
+ the service's response envelope).
77
+ - To let the panel **add** a child, put a small form + "Add" button in the same
78
+ dialog whose chain `POST`s to the child entity then re-runs the child Data
79
+ Service. The tutorial's Equipment Catalog page does exactly this.
80
+
81
+ ## Related
82
+
83
+ - [Build a page](../guides/build-a-page.md)
84
+ - [Add a data provider, data view, or data service](../guides/add-a-data-provider.md)
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: An N-days-before reminder job
3
+ audience: tenant
4
+ ---
5
+
6
+ # An N-days-before reminder job
7
+
8
+ ## When you need this
9
+
10
+ "Remind me N days before `<date>`." Warranty expiring in 30 days, an audit due in
11
+ 7, a checkout due back in 3. Zero Java — one `entity_status_date_sweep_config`
12
+ row with `day_offset` set, shipped as seed data.
13
+
14
+ ## The file
15
+
16
+ `metadata/seed-data/office-equipment-sweep-configs.json`
17
+ ([real file](../tutorial/example-plugin/spk-assembly/metadata/seed-data/office-equipment-sweep-configs.json)):
18
+
19
+ ```json
20
+ {
21
+ "entity": "entity_status_date_sweep_config",
22
+ "keyFields": ["entity_name", "date_field", "set_status_to", "day_offset"],
23
+ "source": "office-equipment",
24
+ "rows": [
25
+ {
26
+ "entity_name": "oeq_checkout",
27
+ "status_field": "status",
28
+ "when_status_in": "CHECKED_OUT",
29
+ "date_field": "due_date",
30
+ "compare_op": "lte",
31
+ "day_offset": 3,
32
+ "set_status_to": "DUE_SOON",
33
+ "active": true,
34
+ "seeded_by": "office-equipment"
35
+ }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ ## How to read it
41
+
42
+ `compare_op: "lte"` + `day_offset: 3` → the job fires for a `CHECKED_OUT`
43
+ checkout whose `due_date` is **on or before `now + 3 days`** — i.e. "within 3
44
+ days of due". `day_offset: 30` = "within 30 days". `day_offset: 0` (the default)
45
+ = "the date has already passed".
46
+
47
+ Use a **distinct target status per window** if you want 30/14/7-day reminders
48
+ that are independently queryable — e.g. `DUE_IN_30`, `DUE_IN_14`, `DUE_SOON`.
49
+
50
+ ## The rules that bite
51
+
52
+ - **Flip a status value, not a boolean flag.** The generic job passes
53
+ `set_status_to` as a string; writing `"true"` to a boolean column fails.
54
+ - Include `day_offset` in `keyFields` so a 30-day and a 7-day rule for the same
55
+ date column are distinct on re-install.
56
+ - Also ship `metadata/entities/entity_status_date_sweep_config.json` (additive
57
+ fields only). The job auto-registers on the first seeded row via a
58
+ platform-shipped rule — you don't ship a register rule for *this* job.
59
+
60
+ ## Verified
61
+
62
+ ```bash
63
+ $ erp api post "/api/v1/jobs/engine-entity.status-date-sweep/execute" --body "{}"
64
+ { "executionId": 534 }
65
+ $ erp api get "/api/v1/entities/oeq_checkout/records/2"
66
+ { "checkout_number": "CO-100", "status": "OVERDUE" } # was CHECKED_OUT, due_date in the past
67
+ ```
68
+
69
+ ## Related
70
+
71
+ - [Add a scheduled reminder job](../guides/add-a-reminder-job.md)
72
+ - Tutorial chapter 7: [the return-due reminder job](../tutorial/07-return-due-reminder-job.md)
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: A per-row document artifact generator
3
+ audience: tenant
4
+ ---
5
+
6
+ # A per-row document artifact generator
7
+
8
+ ## When you need this
9
+
10
+ "Generate a document per completed record and stash the file" — an audit report
11
+ per finished audit, a handover certificate per completed checkout. Zero Java —
12
+ one `entity_document_generator_config` row run by
13
+ `engine-entity.document-generator`.
14
+
15
+ ## The file
16
+
17
+ `metadata/seed-data/office-equipment-document-configs.json`:
18
+
19
+ ```json
20
+ {
21
+ "entity": "entity_document_generator_config",
22
+ "keyFields": ["document_code"],
23
+ "source": "office-equipment",
24
+ "rows": [
25
+ {
26
+ "document_code": "equipment-audit-report",
27
+ "source_entity": "oeq_audit",
28
+ "when_status_in": "COMPLETED",
29
+ "target_file_field": "report_file_ref_id",
30
+ "format": "json",
31
+ "cabinet_id": 42,
32
+ "child_entity": "oeq_audit_item",
33
+ "child_match_field": "audit_id",
34
+ "file_name_template": "equipment-audit-${id}-${date}.json",
35
+ "active": true
36
+ }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ## How to read it
42
+
43
+ For each `oeq_audit` in status `COMPLETED` that doesn't already have a file
44
+ reference in `report_file_ref_id` (a **text** column — its non-blank state is the
45
+ dedupe marker), the job builds a structured document from the audit row plus its
46
+ `oeq_audit_item` children, stores it in DMS cabinet `42`, and writes the file id
47
+ back into `report_file_ref_id`.
48
+
49
+ ## The limitation to know
50
+
51
+ The platform's PDF render path only renders synthetic sample data via the Studio
52
+ app — it can't be driven headless against a real record. So this job emits a
53
+ **structured-data artifact** (`json` or `csv`), not a rendered PDF.
54
+ `template_ref` is reserved for when real-record binding ships.
55
+
56
+ ## Also ship
57
+
58
+ - `cabinet_id` must be a real DMS cabinet id in your tenant.
59
+ - The shared `entity_document_generator_config.json` (additive) and an
60
+ `AFTER_CREATE` register rule calling
61
+ `ensureEntityDocumentGeneratorJobRegistered`.
62
+
63
+ ## Related
64
+
65
+ - `erp examples patterns --kind jobs` → `per-row-document-artifact-generation`
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: A searchable related-record lookup field
3
+ audience: tenant
4
+ ---
5
+
6
+ # A searchable related-record lookup field
7
+
8
+ ## When you need this
9
+
10
+ Any form field whose value is another entity's id — `category_id`,
11
+ `equipment_id`, `employee_id`. Use a `core.lookup` block, never a plain text or
12
+ number input that makes the user type a raw id. (An entire module built by AI
13
+ agents once shipped every FK as a bare number input because this pattern was
14
+ undiscoverable.)
15
+
16
+ ## The three files
17
+
18
+ ### 1. A Data View producing `{ value, label }`
19
+
20
+ `metadata/data_view/oeq-category-select-options-view.json`
21
+ ([real file](../tutorial/example-plugin/spk-assembly/metadata/data_view/oeq-category-select-options-view.json)) —
22
+ selects `t.id AS value` and a concatenated label. The output columns **must** be
23
+ named `value` and `label`.
24
+
25
+ ### 2. A `search` Data Service over that view
26
+
27
+ `metadata/data_service/oeq-category-search.json`
28
+ ([real file](../tutorial/example-plugin/spk-assembly/metadata/data_service/oeq-category-search.json)):
29
+
30
+ ```json
31
+ {
32
+ "name": "oeq-category-search",
33
+ "definition": {
34
+ "operation": "search",
35
+ "source": { "kind": "dataView", "dataViewName": "oeq-category-select-options-view" },
36
+ "filters": [ { "field": "t.category_name", "operator": "contains", "value": "${param.search}" } ],
37
+ "parameters": [ { "name": "search", "type": "string" } ]
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### 3. The `core.lookup` block on the page
43
+
44
+ ```json
45
+ {
46
+ "blockType": "core.lookup",
47
+ "properties": {
48
+ "value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
49
+ "recordType": { "source": "static", "value": "OeqCategory" },
50
+ "optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
51
+ },
52
+ "events": { "committed": { "source": "action-chain", "actions": [
53
+ { "id": "a0", "order": 0, "type": "setValue",
54
+ "config": { "field": "page.formCategoryId", "value": "${event.new}" } }
55
+ ] } }
56
+ }
57
+ ```
58
+
59
+ ## The rules
60
+
61
+ - `optionsSourceKey` = the `name` of the `search` Data Service. This is the real
62
+ wiring.
63
+ - `recordType` = a documentation-only label; it doesn't route anything.
64
+ - `committed` fires with **`event.new`** = the picked record's **id**. Not
65
+ `event.value`, not the label.
66
+ - If the target entity has no search-shaped Data Service yet, build files 1 and 2
67
+ first — don't fall back to a text input.
68
+ - For a small, static option set, `core.select` with inline `options` is simpler
69
+ and needs none of this.
70
+
71
+ ## Related
72
+
73
+ - [Add a create / edit form](../guides/add-a-create-edit-form.md)
74
+ - [A resolved reference column in a grid](./resolved-reference-grid-column.md)
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: A resolved reference column in a grid
3
+ audience: tenant
4
+ ---
5
+
6
+ # A resolved reference column in a grid
7
+
8
+ ## When you need this
9
+
10
+ A grid column (or a detail-view field) that should show a person's or record's
11
+ **name**, but the entity only stores the **id**. Don't bind the raw `*_id` — bind
12
+ the automatically-resolved `*_id_label` sibling.
13
+
14
+ ## The one line on the entity field
15
+
16
+ ```json
17
+ {
18
+ "fieldName": "employee_id",
19
+ "label": "\"Employee Id\"",
20
+ "dataType": "long",
21
+ "flags": "{\"reference\":{\"entityType\":\"Employee\"}}"
22
+ }
23
+ ```
24
+
25
+ That `flags` string turns on id → display-name resolution platform-wide. From
26
+ then on, **every** read of the entity —
27
+ `GET .../records/query` (the grid) and `GET .../records/{id}` (single record) —
28
+ gains a sibling key `employee_id_label`, resolved in one batched call per page
29
+ load, not per row.
30
+
31
+ **Verified live** in the tutorial:
32
+
33
+ ```bash
34
+ $ erp api post "/api/v1/entities/oeq_checkout/records" --body '{"...":"...","employee_id":1,"...":"..."}'
35
+ { "id": 1, "employee_id": 1, "employee_id_label": "Aria Chen", ... }
36
+ ```
37
+
38
+ ## The grid column
39
+
40
+ ```json
41
+ { "name": "employee_id_label", "type": "string",
42
+ "headerKey": "office-equipment.equipment-checkout.column.employee" }
43
+ ```
44
+
45
+ Bind `employee_id_label`, not `employee_id`.
46
+
47
+ ## The rules
48
+
49
+ - `entityType` must be a **registered** provider — one that exposes a
50
+ `batch-names` endpoint. `"Employee"` (backed by the HCM employee plugin) is the
51
+ only one shipped today. An unregistered `entityType` simply produces no
52
+ `_label` key — no error.
53
+ - For a foreign key to **your own** entity (no registered provider), denormalize
54
+ instead: store `category_name` next to `category_id` and write both on create.
55
+ That's what the tutorial's `oeq_equipment` does.
56
+ - You can add the flag to a field after the fact via
57
+ `PUT /api/v1/entities/{id}/fields/{fieldId}` — the `_label` key appears on the
58
+ next read.
59
+
60
+ ## Related
61
+
62
+ - [Add an entity](../guides/add-an-entity.md)
63
+ - [A searchable related-record lookup field](./related-record-lookup-field.md)
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: A scheduled record-generation cadence
3
+ audience: tenant
4
+ ---
5
+
6
+ # A scheduled record-generation cadence
7
+
8
+ ## When you need this
9
+
10
+ "Create new records on a schedule." A quarterly audit, a monthly review, a
11
+ follow-up task per due row. Zero Java — one `entity_cadence_config` row run by
12
+ `engine-entity.cadence-generator`.
13
+
14
+ ## Standalone mode — one record per interval
15
+
16
+ `metadata/seed-data/office-equipment-cadence-configs.json`
17
+ ([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json)):
18
+
19
+ ```json
20
+ {
21
+ "entity": "entity_cadence_config",
22
+ "keyFields": ["cadence_code"],
23
+ "source": "office-equipment",
24
+ "rows": [
25
+ {
26
+ "cadence_code": "equipment-audit-schedule",
27
+ "target_entity": "oeq_audit",
28
+ "interval_days": 90,
29
+ "dedupe_target_field": "cadence_marker",
30
+ "template_json": {
31
+ "audit_number": "AUDIT-${date}-${ts}",
32
+ "audit_name": "Scheduled Equipment Audit ${date}",
33
+ "status": "DRAFT"
34
+ },
35
+ "active": true
36
+ }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ Every 90 days (tracked via `last_generated_at`) one `oeq_audit` row is created
42
+ from `template_json`. String substitutions: `${date}`, `${ts}`, `${uuid}`.
43
+
44
+ ## Source-driven mode — one record per due source row
45
+
46
+ Add:
47
+
48
+ ```json
49
+ "source_entity": "oeq_equipment",
50
+ "source_date_field": "warranty_expiry_date",
51
+ "due_within_days": 30,
52
+ "field_map_json": { "equipment_id": "id", "equipment_tag": "asset_tag" },
53
+ "dedupe_target_field": "equipment_id"
54
+ ```
55
+
56
+ → for every `oeq_equipment` whose warranty expires within 30 days and which
57
+ doesn't already have a generated row (deduped on `equipment_id`), create one,
58
+ copying the mapped fields. `${sourceId}` is available in `template_json`.
59
+
60
+ ## Also ship
61
+
62
+ - the target entity, the shared `entity_cadence_config.json` (additive), and
63
+ - `metadata/rules/ensure_cadence_generator_job_registered.json`
64
+ (`AFTER_CREATE` → `ensureEntityCadenceGeneratorJobRegistered`).
65
+
66
+ ## Related
67
+
68
+ - [Add a cadence-generator job](../guides/add-a-cadence-job.md)
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: action schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `action` schema
8
+
9
+ A page-JSON/UI action-chain definition (frontend action engine — distinct from the server-side Action Engine, see ai/patterns/action-engine-server-side.md).
10
+
11
+ Pull the full JSON Schema: `erp schema pull action` &nbsp;·&nbsp; MCP: `erp_get_schema {"name":"action"}`
12
+
13
+ _This schema's root is not a plain object (see the full schema for its shape)._
14
+
15
+ ## Named sub-definitions
16
+
17
+ `actionDefinition`, `retryPolicy`, `errorStrategy`
18
+
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: block-definition schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `block-definition` schema
8
+
9
+ A block TYPE's own declared property/event contract (what core.grid, core.container, etc. accept).
10
+
11
+ Pull the full JSON Schema: `erp schema pull block-definition` &nbsp;·&nbsp; MCP: `erp_get_schema {"name":"block-definition"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `contractVersion` | — | yes | |
18
+ | `type` | string | yes | Globally unique block-type id, dot-namespaced (e.g. core.text-input, acme-hr.org-chart). |
19
+ | `version` | string | yes | Semver. Instances pin the version they were authored against; minor/patch upgrades apply automatically, major upgrades require an explicit migration (W1-05). |
20
+ | `publisher` | string | yes | The Phase 21 marketplace publisher id. |
21
+ | `inputClass` | boolean | | True for input-class blocks: the block inherits the validation-message surface (invalid state + message slot + aria-invalid/aria-describedby) from the base contract, which Phase 6's validation pipeline populates (round 2). |
22
+ | `properties` | array | yes | |
23
+ | `inputs` | array | | |
24
+ | `outputs` | array | | |
25
+ | `events` | array | yes | |
26
+ | `permissions` | object | yes | Which permission hook points the block honors (W2-05). Enforcement lands in Phase 11; the hooks are in the render path from day one. |
27
+ | `a11y` | object | yes | |
28
+ | `designer` | object | yes | Block-level designer metadata: what Phase 10's palette and property panel render for a block they have never seen (gap audit). |
29
+ | `slots` | array | | Container-class blocks only: named child slots and what they may contain. |
30
+ | `composition` | array | | 2026-07-18: present only on tenant-authored composite Block definitions (Studio's Block Designer) - the block's own fixed internal content, an array of frozen @erp/block-engine BlockInstanceConfig, validated against the live BlockRegistry at mount time exactly like a page item's or a container's own children. undefined for every core/plugin block. |
31
+
32
+ ## Named sub-definitions
33
+
34
+ `identifier`, `i18nKey`, `propertyType`, `valueSourceKind`, `selectOption`, `propertySpec`, `portSpec`, `eventSpec`
35
+
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: block-instance schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `block-instance` schema
8
+
9
+ One block INSTANCE inside a page (instanceId/blockType/properties/events) — same shape validate-page-properties.mjs checks.
10
+
11
+ Pull the full JSON Schema: `erp schema pull block-instance` &nbsp;·&nbsp; MCP: `erp_get_schema {"name":"block-instance"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `contractVersion` | — | yes | |
18
+ | `instanceId` | string | yes | The block's instance identity - also its Phase 4 block-scope scopeKey. |
19
+ | `blockType` | string | yes | |
20
+ | `blockVersion` | string | yes | The version this instance was authored against (the pin, W1-05). |
21
+ | `properties` | object | yes | |
22
+ | `events` | object | | |
23
+ | `outputs` | object | | Optional output redirection: where each output port writes. Defaults to the block's own block-scope entry named after the port. |
24
+ | `children` | object | | Container-class blocks only: slot name to nested instance configs. |
25
+ | `a11yLabelKey` | string | | W10 (additive, 2026-07-16): the authored label's translation key for this placed instance (a Phase 6 field's labelKey), resolved through the runtime i18n resolver into the node's accessible name; takes precedence over the block definition's labelFrom default. |
26
+ | `name` | string | | 2026-07-18: an author-facing display name (e.g. "Header block"), never read at render/runtime — a Studio authoring aid only. |
27
+ | `className` | string | | 2026-07-17: a framework-specific class name applied to the rendered root, the universal escape hatch alongside style. |
28
+ | `style` | object | | 2026-07-17: raw inline style properties applied to the rendered root, the universal escape hatch alongside className. |
29
+
30
+ ## Named sub-definitions
31
+
32
+ `boundKey`, `propertyValue`, `eventWiring`
33
+
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: composite-pack-manifest schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `composite-pack-manifest` schema
8
+
9
+ The newer composite-pack / Country-or-Industry-Pack / Code-Plugin-style manifest contract (contractVersion/publisher/distribution/12 extension-point contributions) — a genuinely different artifact type from an ordinary .spk plugin.json (use 'plugin-manifest' for that).
10
+
11
+ Pull the full JSON Schema: `erp schema pull composite-pack-manifest` &nbsp;·&nbsp; MCP: `erp_get_schema {"name":"composite-pack-manifest"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `contractVersion` | — | yes | |
18
+ | `id` | `#/$defs/identifier` | yes | |
19
+ | `version` | `#/$defs/semver` | yes | |
20
+ | `publisher` | string | yes | |
21
+ | `displayNameKey` | string | yes | |
22
+ | `descriptionKey` | string | yes | |
23
+ | `distribution` | enum | yes | one of: `locked`, `editable` |
24
+ | `dependencies` | array | | |
25
+ | `contributions` | `#/$defs/contributions` | yes | |
26
+ | `hasUpgradeMigration` | boolean | | |
27
+ | `hasDataErasureHook` | boolean | | |
28
+ | `frontendBundle` | object | | 2026-09-03, closes the dynamic-frontend-plugin-loading gap (ai/domains/ai-native-developer-platform-gap-analysis.md): a compiled @erp/* frontend bundle a runtime loader fetches from the real, generic engine-file content endpoint (GET /api/v1/files/{fileId}/content) and dynamically import()s, instead of the code shipping inside a host application's own build. Optional and additive - absent on every manifest that predates this field (every real plugin in the fleet today except this pass's own proof plugin), which a frontend loader must treat as a cheap, silent no-op. |
29
+
30
+ ## Named sub-definitions
31
+
32
+ `identifier`, `semver`, `versionRange`, `dependency`, `argKind`, `actionTypeContribution`, `functionContribution`, `idDescription`, `networkScopedContribution`, `scheduledJobContribution`, `themeContribution`, `menuItemContribution`, `localeBundleContribution`, `contributions`
33
+