@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,22 @@
1
+ ---
2
+ title: print-template-definition schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `print-template-definition` schema
8
+
9
+ Print Template Designer's content model (@erp/print-engine) — Report's band/element sibling with a physical mm page + merge-tag text. Dashboards have NO separate schema — use `page`.
10
+
11
+ Pull the full JSON Schema: `erp schema pull print-template-definition`  ·  MCP: `erp_get_schema {"name":"print-template-definition"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `name` | string | yes | |
18
+ | `description` | string | | |
19
+ | `metadata` | object | | |
20
+ | `modules` | array | | |
21
+ | `definition` | object | yes | |
22
+
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: provider-definition schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `provider-definition` schema
8
+
9
+ Provider Designer's content model (ProviderAuthoringController) — kind/connectionRef/basePath/supports.
10
+
11
+ Pull the full JSON Schema: `erp schema pull provider-definition`  ·  MCP: `erp_get_schema {"name":"provider-definition"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `name` | string | yes | |
18
+ | `description` | string | | |
19
+ | `metadata` | object | | |
20
+ | `modules` | array | | |
21
+ | `definition` | object | yes | |
22
+
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: report-definition schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `report-definition` schema
8
+
9
+ Report Designer's content model (@erp/report-engine) — band/element grid.
10
+
11
+ Pull the full JSON Schema: `erp schema pull report-definition`  ·  MCP: `erp_get_schema {"name":"report-definition"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `name` | string | yes | |
18
+ | `description` | string | | |
19
+ | `metadata` | object | | |
20
+ | `modules` | array | | |
21
+ | `definition` | object | yes | |
22
+
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: rule schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `rule` schema
8
+
9
+ Frontend rule-engine definition (page-level conditional visibility/computed-value rules).
10
+
11
+ Pull the full JSON Schema: `erp schema pull rule`  ·  MCP: `erp_get_schema {"name":"rule"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `id` | string | yes | |
18
+ | `name` | string | yes | Stable marketplace-facing name (Phase 21 §4c). |
19
+ | `description` | string | | |
20
+ | `event` | string | yes | Dotted event-name pattern: '*' matches one segment, trailing '**' matches any remainder. |
21
+ | `priority` | integer | yes | One of the band values 1/5/10/100 unless priorityOverride is true. Bands confer timing and veto rights (ADR 0008 §4): security(1) and validation(5) run synchronously pre-commit and can veto; business(10) sync; notification(100) async post-commit. |
22
+ | `priorityOverride` | boolean | | Explicit opt-out of the band constraint. Deliberate, visible, audited. |
23
+ | `enabled` | boolean | | |
24
+ | `scope` | object | | Match dimensions beyond event name and tenant. Unset means any. |
25
+ | `watchedFields` | array | | Fire only when one of these data fields changed between data.old and data.new (ADR 0008 §2). Fires when old/new are absent (creation). |
26
+ | `condition` | string | | Phase 1 expression over ${event.*} plus the ambient scopes. Boolean true fires; false/Null skips; non-Boolean/unresolvable is a distinct observable skip. |
27
+ | `suppressSelfTrigger` | boolean | | Don't re-fire from events this rule's own actions caused (ADR 0008 §5). |
28
+ | `actions` | array | yes | Phase 2 action chain(s), executed by the Action Engine with its frozen retry/timeout/error semantics. |
29
+ | `metadata` | object | | Marketplace-publishable shape (Phase 21 §4c): version required at publish time. |
30
+
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: workflow-definition schema
3
+ audience: tenant
4
+ generated: true
5
+ ---
6
+
7
+ # `workflow-definition` schema
8
+
9
+ A real multi-stage approval workflow (engine-workflow) — stagesJson/tasksJson/transitionsJson (each itself a JSON-encoded string) + a top-level approvalPermissions array.
10
+
11
+ Pull the full JSON Schema: `erp schema pull workflow-definition`  ·  MCP: `erp_get_schema {"name":"workflow-definition"}`
12
+
13
+ ## Top-level properties
14
+
15
+ | Property | Type | Required | Notes |
16
+ | --- | --- | --- | --- |
17
+ | `name` | string | yes | Unique per tenant; a domain module's REST layer starts instances by this exact name, e.g. "employee.transfer". |
18
+ | `description` | string | | |
19
+ | `stagesJson` | string | yes | |
20
+ | `tasksJson` | string | yes | |
21
+ | `transitionsJson` | string | yes | |
22
+ | `approversJson` | string | | |
23
+ | `slasJson` | string | | |
24
+ | `escalationsJson` | string | | |
25
+ | `notificationsJson` | string | | |
26
+ | `metadataJson` | string | | |
27
+ | `approvalPermissions` | array | | Which role (by its stable role.code, never a mutable display name) may decide which named approvalObject task. |
28
+
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: "\"My grid shows 0 rows\""
3
+ audience: tenant
4
+ ---
5
+
6
+ # "My grid shows 0 rows"
7
+
8
+ A `core.grid` renders empty and there is **no error message**. Work through this
9
+ in order.
10
+
11
+ ## 1. Is `metadata.dataSource` set, and on the page?
12
+
13
+ Open the page file. The **top level** (sibling of `definition`, not inside it,
14
+ not on any block) must have:
15
+
16
+ ```json
17
+ "metadata": { "dataSource": "oeq-equipment-provider" }
18
+ ```
19
+
20
+ `core.grid` has no data property. If `dataSource` is missing, on the grid block,
21
+ or inside `definition`, the grid gets nothing.
22
+
23
+ ## 2. Does the string match a provider `name` exactly?
24
+
25
+ There must be a file `spk-assembly/metadata/provider/*.json` whose `name` field
26
+ is **character-for-character** equal to the `dataSource` string. Check for a
27
+ typo, a plural, a stray dash.
28
+
29
+ ```bash
30
+ erp plugin test spk-assembly
31
+ ```
32
+
33
+ catches this as **PAGE-DS-001** and prints the mismatched name.
34
+
35
+ ## 3. Does the provider's `basePath` actually return rows?
36
+
37
+ For an entity you own it should be `/api/v1/entities/<entityName>/records`. Test
38
+ it directly:
39
+
40
+ ```bash
41
+ erp api get "/api/v1/entities/oeq_equipment/records/query?size=5"
42
+ ```
43
+
44
+ - Rows come back → the provider path is fine; the problem is wiring (steps 1–2)
45
+ or filtering (step 4).
46
+ - `{ "rows": [], "total": 0 }` → the table genuinely has no rows. Create one and
47
+ retry.
48
+ - 404 → wrong `basePath`.
49
+
50
+ ## 4. Is an `externalFilter` hiding everything?
51
+
52
+ If the grid binds `externalFilter` to a page object, check what's in that object
53
+ on mount. A stale `status: ""` or an un-cleared search string can filter out
54
+ every row. In the mount loader, initialise the filter object to `{}` or omit
55
+ problematic keys.
56
+
57
+ ## 5. Is `refreshTrigger` stuck?
58
+
59
+ If the grid only ever loads with the filter/refresh value it had at mount, make
60
+ sure `refreshTrigger` binds a page value that actually changes (you toggle
61
+ `page.gridRefreshTick` after writes), and that the mount loader sets it to a
62
+ defined initial value (`0`).
63
+
64
+ ## 6. Row-level permissions
65
+
66
+ If the grid is empty **only for some users**, that's row-level security doing its
67
+ job. Check the user's role and the entity's permission keys.
68
+
69
+ ## The fast check
70
+
71
+ ```bash
72
+ # does the raw endpoint the provider points at return data?
73
+ erp api get "/api/v1/entities/<entity>/records/query?size=5"
74
+ ```
75
+
76
+ Whatever that returns is what a correctly-wired grid shows. If it has rows and
77
+ the grid doesn't, the bug is in steps 1–5.
78
+
79
+ ## See also
80
+
81
+ - [Wire a page's data](../guides/wire-a-pages-data.md)
82
+ - [How does a page get its data?](../concepts/page-data-flow.md)
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: "\"My scheduled job never runs\""
3
+ audience: tenant
4
+ ---
5
+
6
+ # "My scheduled job never runs"
7
+
8
+ You seeded a job config row but nothing happens. Work through this in order.
9
+
10
+ ## 1. Is the job registered for your tenant?
11
+
12
+ ```bash
13
+ erp api get "/api/v1/jobs/engine-entity.status-date-sweep"
14
+ ```
15
+
16
+ - `{ "status": "ENABLED", ... }` → registered. Go to step 2.
17
+ - 404 → **not registered.** The job registers on the **first** config row via an
18
+ `AFTER_CREATE` register rule. Either:
19
+ - your plugin seeded no config row (check the install log for
20
+ `PluginDataSeedInstaller: ... -> entity entity_<x>_config`), or
21
+ - for the aggregation / cadence / compliance / cross-plugin / document jobs you
22
+ **must ship the register rule yourself** —
23
+ `metadata/rules/ensure_<job>_registered.json` with an unconditional
24
+ `AFTER_CREATE` `EXECUTE_SERVICE`. Only the **status-date-sweep** job's
25
+ register rule ships with the platform.
26
+
27
+ ## 2. Run it on demand and read the result
28
+
29
+ ```bash
30
+ erp api post "/api/v1/jobs/engine-entity.status-date-sweep/execute" --body "{}"
31
+ # → { "executionId": 534 }
32
+
33
+ erp api get "/api/v1/jobs/engine-entity.status-date-sweep/executions?size=1"
34
+ ```
35
+
36
+ The latest execution's `resultJson`:
37
+
38
+ ```json
39
+ { "swept": 0, "failed": 0, "rowsScanned": 92, "configsScanned": 28 }
40
+ ```
41
+
42
+ - **`configsScanned: 0`** → the job sees no config rows. Your seed didn't land,
43
+ or `active` is `false` on the rows.
44
+ - **`configsScanned` > 0 but `swept: 0`** → configs are seen but no record
45
+ matched. Go to step 3.
46
+ - **`failed` > 0** → a matching record's update threw. Go to step 4.
47
+
48
+ ## 3. `swept: 0` — why does no record match?
49
+
50
+ For each config row, a record is swept only if **all** of:
51
+
52
+ - its `status_field` value is **exactly** one of `when_status_in` (case
53
+ matters — match the enum);
54
+ - its `date_field` satisfies `compare_op` against `now` (± `day_offset` days);
55
+ - (aggregation/compliance) it passes any `when_status_in` filter.
56
+
57
+ Common causes:
58
+
59
+ - `when_status_in: "checked_out"` but the enum value is `CHECKED_OUT`.
60
+ - `day_offset` omitted, so "within 3 days" is really "on the exact day".
61
+ - `date_field` is null on the records (a null date never satisfies a comparison).
62
+
63
+ Set up a record that should obviously match and re-run.
64
+
65
+ ## 4. `failed` > 0 — the update threw
66
+
67
+ ```bash
68
+ erp logs tail --lines 200 --grep "status/date sweep: failed to update"
69
+ ```
70
+
71
+ ```
72
+ Entity status/date sweep: failed to update oeq_checkout #1 (tenantId=2):
73
+ PreparedStatementCallback; bad SQL grammar [update oeq_checkout set return_due_soon = ? ...]
74
+ ```
75
+
76
+ The most common cause: the config's `set_field` is a **boolean** column and the
77
+ job writes the value as a string. **Fix:** flip a status enum value
78
+ (`DUE_SOON`, `OVERDUE`) instead of a boolean flag.
79
+
80
+ ## 5. It works on demand but not on schedule
81
+
82
+ The cron is `0 5 0 * * *` (00:05 daily, per tenant). It won't fire mid-day.
83
+ Test with `/execute`; trust the schedule once `/execute` is clean.
84
+
85
+ ## See also
86
+
87
+ - [Add a scheduled reminder job](../guides/add-a-reminder-job.md)
88
+ - [Seed config and reference data on install](../guides/seed-data-on-install.md)
89
+ - Tutorial chapter 7: [the return-due reminder job](../tutorial/07-return-due-reminder-job.md)
@@ -0,0 +1,193 @@
1
+ ---
2
+ title: Known gotchas
3
+ audience: tenant
4
+ ---
5
+
6
+ # Known gotchas
7
+
8
+ The mistakes everyone makes once. Each entry: **symptom → cause → fix.** All real,
9
+ found in a live build (many while building the [tutorial module](../tutorial/index.md)).
10
+
11
+ ## Entities
12
+
13
+ ### `label` fails the install with `invalid input syntax for type json`
14
+
15
+ **Cause:** an entity's `label` or a field's `label` was a bare string.
16
+ **Fix:** the column is JSON, so the *string* must be valid JSON. Entity:
17
+ `"label": "{\"en\":\"Equipment\"}"`. Field: `"label": "\"Asset Tag\""`. Same for
18
+ `typeParams` (`"{\"enumValues\":[...]}"`) and a workflow's
19
+ `stagesJson`/`tasksJson`/`transitionsJson`.
20
+
21
+ ### A field is silently missing after install
22
+
23
+ **Cause:** `required: false` without `nullable: true` (or vice-versa) on a table
24
+ that already has rows — the `ALTER TABLE ADD COLUMN ... NOT NULL` fails but the
25
+ metadata row was inserted.
26
+ **Fix:** always pair them. `required:true` ⇒ `nullable:false`;
27
+ `required:false` ⇒ `nullable:true`. Then bump the version and re-publish.
28
+
29
+ ### A big blob of text is rejected / truncated
30
+
31
+ **Cause:** `dataType: "long"` — that's the 64-bit **integer** type.
32
+ **Fix:** use `dataType: "text"`.
33
+
34
+ ### The generic status-date-sweep job reports `"failed": N` on your rows
35
+
36
+ **Fixed 2026-09-10:** a sweep config whose `set_field` (or `status_field`) is a
37
+ **boolean**, **integer** or **numeric** column now works — `set_status_to:
38
+ "true"` / `"42"` is coerced to the column's real type before the write (in
39
+ `EntityRecordService`, so every generic sweep job — status-date, compliance,
40
+ cadence, aggregation, cross-plugin — and entity-rule `SET_VALUE` benefits). An
41
+ unparseable value (e.g. `"maybe"` into a boolean) still counts that one row as
42
+ `failed` without throwing. If you still see `failed: N`, the value genuinely
43
+ doesn't parse for the column type, or the row hit an entity-rule rejection —
44
+ check `erp logs tail`.
45
+
46
+ ## Pages and action chains
47
+
48
+ ### `committed` payload key is `event.new`
49
+
50
+ **Cause:** a handler read `event.value` / `event.record`.
51
+ **Fix:** the value the user entered or picked is `${event.new}`. For a
52
+ `core.lookup` it's the picked record's **id**.
53
+
54
+ ### `metadata.dataSource` is page-level, not grid-level
55
+
56
+ **Cause:** put `dataSource` / `entity` / `recordType` on the `core.grid` block —
57
+ it has none of those properties.
58
+ **Fix:** the page artifact's top-level `metadata.dataSource` must equal a
59
+ `metadata/provider/*.json` file's `name`, exactly. Miss or mistype it and the
60
+ grid renders 0 rows with no error. `erp plugin test` catches this as
61
+ **PAGE-DS-001**.
62
+
63
+ ### KPI card shows `undefined`
64
+
65
+ **Cause:** bound `${out.value}` against a `composite` Data Service.
66
+ **Fix:** `count` → `${out.value}`; `composite` → `${out.results.<step>.value}`.
67
+ `.total` is the unrelated list-pagination envelope.
68
+
69
+ ### A parameterized Data Service `/execute` returns 400
70
+
71
+ **Cause:** arguments passed as `{ "params": { "search": "x" } }`.
72
+ **Fix:** nest one level deeper: `{ "params": { "parameters": { "search": "x" } } }`.
73
+ A `core.lookup` does this for you.
74
+
75
+ ### `!= null` in an action-engine condition never matches
76
+
77
+ **Cause:** action-engine quirk.
78
+ **Fix:** use the `isNotNull` / `isNull` operators (rule engine: `is_not_null` /
79
+ `is_null`).
80
+
81
+ ### UI shows a raw i18n key like `office-equipment.x.y`
82
+
83
+ **Cause:** the key has no entry in `metadata/i18n/en.json`.
84
+ **Fix:** add it. `erp plugin test` lists missing keys as **I18N-001**.
85
+
86
+ ## Rules and workflows
87
+
88
+ ### A workflow-trigger rule fires on every save, not just the transition
89
+
90
+ **Cause:** the condition checks only `status == PENDING_APPROVAL`.
91
+ **Fix:** add `{ "field": "status__previous", "op": "neq", "value": "PENDING_APPROVAL" }`
92
+ inside an `all` clause.
93
+
94
+ ### Nobody can approve a workflow task
95
+
96
+ **Cause:** `approvalPermissions` references a role **display name**.
97
+ **Fix:** use `roleCode` — the stable `code`, never the mutable name.
98
+
99
+ ### Install rejects a workflow file
100
+
101
+ **Cause:** `stagesJson` / `tasksJson` / `transitionsJson` authored as nested
102
+ objects.
103
+ **Fix:** each is a JSON-**encoded string**.
104
+
105
+ ## Publishing
106
+
107
+ ### A same-version re-publish does nothing
108
+
109
+ **Cause:** the plugin is already installed at that version — the re-publish is a
110
+ silent no-op.
111
+ **Fix:** bump `plugin.json` `version` before every `build` + `publish`, even if
112
+ you're sure the last publish shipped the fix.
113
+
114
+ ### `Refusing to overwrite immutable artifact`
115
+
116
+ **Cause:** re-publishing a version whose `.spk` was already stored, with
117
+ different bytes (e.g. after a publish that failed later in the pipeline).
118
+ **Fix:** go to the next version number.
119
+
120
+ ### `There is an already loaded plugin ... with the same id`
121
+
122
+ **Cause:** you **uninstalled** the plugin and the background unload didn't
123
+ complete (it can stall on a transient DB error), so the old PF4J code is still
124
+ loaded and the next `publish` (install-new path) is refused.
125
+ **Fix:** to iterate, **bump the version and re-publish** — don't uninstall.
126
+ Recovery from a stuck state is a backend restart or publishing under a new
127
+ plugin id.
128
+
129
+ ### `plugin.json is missing mainClass` *(fixed 2026-09-10 — no longer occurs)*
130
+
131
+ A pure-JSON plugin (only declarative metadata artifacts — entities, pages,
132
+ providers, menus, seed-data, workflows, rules, sweep configs — and zero Java
133
+ classes) now publishes with `"mainClass": null` exactly as `erp plugin create`
134
+ scaffolds it. The platform loads it through a built-in generic no-op PF4J entry
135
+ point (`JsonOnlyPlugin`); full lifecycle (start/stop/enable/disable/upgrade),
136
+ artifact installation, migrations and data seeding all run normally. **You do
137
+ not compile or ship any jar.** Only declare a `mainClass` (with a jar in
138
+ `spk-assembly/lib/`) if your plugin genuinely ships Java extensions.
139
+
140
+ ### `A role named "X" already exists` / `Role "X" ... already exists owned by "Y"`
141
+
142
+ **Cause:** a `plugin.json` role whose `name` or `code` collides with another
143
+ installed plugin's role.
144
+ **Fix:** prefix your role names and codes.
145
+
146
+ ## Runtime / navigation
147
+
148
+ ### The browser URL is not `route.pattern`
149
+
150
+ **Cause:** treating the menu/page `route.pattern` string as the reachable URL.
151
+ **Fix:** the URL is `/app/<app-slug>/<module-slug>/<pluginId>/<page-name>`. The
152
+ `route.pattern` is an internal routing key.
153
+
154
+ ### A menu item 404s
155
+
156
+ **Cause:** `navigation.route` doesn't match the page's `route.pattern`.
157
+ **Fix:** copy it exactly.
158
+
159
+ ### A page isn't in the sidenav
160
+
161
+ **Cause:** the page's `modules` array doesn't name the module it should appear
162
+ under.
163
+ **Fix:** set `modules` to your module's `name`; align the menu file's `modules`
164
+ too.
165
+
166
+ ## Environment / tooling (Windows)
167
+
168
+ ### `erp` command turns `/api/v1/...` into `C:/Program Files/Git/api/v1/...`
169
+
170
+ **Cause:** Git Bash rewrites a leading `/` in an argument to a Windows path.
171
+ **Fix:** run the command from PowerShell, or set `MSYS_NO_PATHCONV=1`.
172
+
173
+ ### `--tenant 5` errors even though you're logged in
174
+
175
+ **Cause:** `--tenant` only overrides the `X-Tenant-Id` header; it doesn't
176
+ re-authenticate you.
177
+ **Fix:** your token must already be valid for that tenant.
178
+
179
+ ## Getting unstuck
180
+
181
+ - `erp logs tail --grep <X-Correlation-Id>` — a failed API response's
182
+ `X-Correlation-Id` header joins to the exact server-side stack trace. *(SDK
183
+ connected mode, offline: use the ERP admin log view.)*
184
+ - `erp platform catalog --query "<term>"` — before writing any "there's no X"
185
+ assumption, check whether the platform already has it.
186
+ - `erp plugin test <dir>` and `node developer-docs/examples/test-examples.mjs` —
187
+ run both before every publish.
188
+
189
+ ## See also
190
+
191
+ - [Publishing and install errors](./publishing-errors.md)
192
+ - [My grid shows 0 rows](./grid-shows-zero-rows.md)
193
+ - [My scheduled job never runs](./job-never-runs.md)
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: Publishing and install errors
3
+ audience: tenant
4
+ ---
5
+
6
+ # Publishing and install errors
7
+
8
+ Every error message `erp plugin publish` can return, what it means, and what to
9
+ do. The install pipeline is all-or-nothing and names the stage it failed at — see
10
+ [How does a plugin get installed?](../concepts/plugin-lifecycle.md).
11
+
12
+ ## Before upload
13
+
14
+ ### `plugin.json is missing mainClass` *(no longer raised, as of 2026-09-10)*
15
+
16
+ A pure-JSON plugin publishes fine with `"mainClass": null` — the platform loads
17
+ it via a built-in generic entry point, no jar required. Only set `mainClass`
18
+ if your plugin ships its own Java extensions.
19
+
20
+ ### `plugin.json is missing id/version`
21
+
22
+ Fill them in. `id` is lowercase kebab-case; `version` is semver.
23
+
24
+ ### `could not reach <url>` / `ERR_INVALID_URL`
25
+
26
+ Wrong env base URL (`erp env list`), the server is down, or — on Git Bash —
27
+ `MSYS_NO_PATHCONV=1` needed.
28
+
29
+ ## Stage VALIDATING
30
+
31
+ ### `Refusing to overwrite immutable artifact at "system/plugins/<id>/<version>/..."`
32
+
33
+ You already stored a `.spk` under this `<version>`, the new bytes differ, **and
34
+ that version genuinely shipped** (it reached `pf4jState: STARTED` on some
35
+ tenant). A shipped version is write-once — **bump to the next version.**
36
+
37
+ As of the 2026-09-10 publish-hardening pass this is *no longer* raised for a
38
+ version whose prior install **failed or never started** (a role clash caught
39
+ late, or a metadata-only plugin that isn't PF4J-loaded): re-publishing the same
40
+ version now replaces the stored `.spk` and retries. So the "edit a page,
41
+ re-publish the same version" loop works for JSON-only plugins without a bump.
42
+ You still bump for every change to a version that's live and `STARTED`.
43
+
44
+ ### `one or more page definitions failed block-property validation`
45
+
46
+ A page sets a property that the block type doesn't have, or uses a block type
47
+ that doesn't exist. Run `erp plugin validate spk-assembly` locally — it reports
48
+ which page and which property. Ground the block first with
49
+ `erp blocks list --type <name>`.
50
+
51
+ ### schema validation failure on an artifact
52
+
53
+ The named file doesn't match its schema. Run
54
+ `erp schema validate <file> --schema <name>` and
55
+ `node developer-docs/examples/test-examples.mjs`.
56
+
57
+ ## Stage LOADING
58
+
59
+ ### `There is an already loaded plugin ... with the same id (...)`
60
+
61
+ A previous version of this plugin id is still loaded in PF4J. The platform
62
+ cannot hold two versions of one id at once.
63
+
64
+ - **Normal upgrade / in-place re-publish**: handled — the publish path now
65
+ stops + unloads the currently-loaded same-id plugin (with a GC hint) *before*
66
+ loading the new bundle. `erp plugin publish` / `spark publish` also auto-retry
67
+ once with a force-unload if they still see this error.
68
+ - **Wedged load** (`state: uninstalled` in `erp plugin list` but `pf4jState:
69
+ STARTED`): a crash or a transient `Failed to obtain JDBC Connection` during
70
+ uninstall aborted the unload. Recover without a backend restart:
71
+
72
+ ```bash
73
+ erp plugin force-unload <plugin-id>
74
+ erp plugin publish <plugin>-<next>.spk --tenant 2
75
+ ```
76
+
77
+ `force-unload` best-effort stops, unloads and GC-hints the stuck classloader,
78
+ then reports `pf4jState` (`NOT_LOADED` on success). It's idempotent and does
79
+ not need an active install row. `erp plugin publish --force` runs it for you.
80
+
81
+ ### `plugin manager rejected the package (missing/invalid plugin.json or mainClass?)`
82
+
83
+ The `lib/*.jar` doesn't contain the class named by `mainClass`, or isn't a valid
84
+ plugin jar. Recompile the stub; confirm
85
+ `jar -tf spk-assembly/lib/*.jar` lists your `.class` file at the right package
86
+ path.
87
+
88
+ ## After code load (rolled back)
89
+
90
+ ### `A role named "X" already exists`
91
+
92
+ Another installed plugin owns a role with that display **name**. Rename yours.
93
+
94
+ ### `Role "X" (code "Y") already exists owned by "Z"`
95
+
96
+ Another plugin owns that role **code**. Change your code. (This survives a
97
+ plugin's own uninstall — a role created by `example-plugin` blocks
98
+ `office-equipment` from creating one with the same code.)
99
+
100
+ ### seed-data failure — entity not found
101
+
102
+ A `metadata/seed-data/*.json` targets an entity your plugin doesn't ship. Ship
103
+ the `metadata/entities/*.json` (or, for a shared config entity, ship the additive
104
+ version of it).
105
+
106
+ ## Verify a successful publish
107
+
108
+ ```bash
109
+ erp plugin list
110
+ ```
111
+
112
+ Your plugin: `"state": "installed"`, `"pf4jState": "STARTED"`,
113
+ `"migrationStatus": "INSTALLED"`. Then hit a page or entity endpoint to confirm
114
+ the new content is live.
115
+
116
+ ## See also
117
+
118
+ - [How does a plugin get installed?](../concepts/plugin-lifecycle.md)
119
+ - [Publish and upgrade a plugin](../guides/publish-and-upgrade.md)