@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,109 @@
1
+ {
2
+ "title": "ERP Plugin Developer Docs",
3
+ "audience": ["plugin-developer", "application-user"],
4
+ "audienceNote": "Historically this field was the single string \"tenant\" (per-page frontmatter `audience: tenant|all` is the field that actually gates bundle/build inclusion — see BUNDLE_ALLOWLIST in tools/erp-cli/erp.mjs — this top-level field is informational only, read by no code path). It's now an array because this site genuinely serves two different readers as of the Applications section: plugin-developer (Quickstart/Concepts/Guides/Reference/Tutorial/Recipes/Troubleshooting — build plugins on the platform) and application-user (Applications — use a shipped business-application screen like HCM's Employee Profile). Every individual page still declares its own `audience: tenant` frontmatter regardless of which readership it's written for; \"tenant\" there means \"the customer org\", which is accurate for both readers.",
5
+ "diataxis": ["quickstart", "concepts", "guides", "reference", "tutorial", "recipes", "troubleshooting", "applications"],
6
+ "nav": [
7
+ {
8
+ "section": "Quickstart",
9
+ "items": [
10
+ { "title": "Build and publish a one-page plugin", "path": "quickstart/index.md" }
11
+ ]
12
+ },
13
+ {
14
+ "section": "Concepts",
15
+ "items": [
16
+ { "title": "Platform overview", "path": "concepts/overview.md" },
17
+ { "title": "What are the artifact types?", "path": "concepts/artifact-types.md" },
18
+ { "title": "How does a plugin get installed?", "path": "concepts/plugin-lifecycle.md" },
19
+ { "title": "How do the SDK modes work (monorepo vs connected)?", "path": "concepts/sdk-modes.md" },
20
+ { "title": "How does a page get its data?", "path": "concepts/page-data-flow.md" },
21
+ { "title": "Glossary", "path": "concepts/glossary.md" }
22
+ ]
23
+ },
24
+ {
25
+ "section": "Guides",
26
+ "items": [
27
+ { "title": "Set up the SDK and connect to an environment", "path": "guides/set-up-the-sdk.md" },
28
+ { "title": "Create a plugin from scratch", "path": "guides/create-a-plugin.md" },
29
+ { "title": "Add an entity", "path": "guides/add-an-entity.md" },
30
+ { "title": "Build a page", "path": "guides/build-a-page.md" },
31
+ { "title": "Wire a page's data", "path": "guides/wire-a-pages-data.md" },
32
+ { "title": "Add a data provider, data view, or data service", "path": "guides/add-a-data-provider.md" },
33
+ { "title": "Add a create / edit form", "path": "guides/add-a-create-edit-form.md" },
34
+ { "title": "Add a KPI or aggregation", "path": "guides/add-a-kpi.md" },
35
+ { "title": "Add an approval workflow", "path": "guides/add-an-approval-workflow.md" },
36
+ { "title": "Add business rules and expressions", "path": "guides/add-business-rules.md" },
37
+ { "title": "Add a scheduled reminder job", "path": "guides/add-a-reminder-job.md" },
38
+ { "title": "Add a cross-row aggregation job", "path": "guides/add-an-aggregation-job.md" },
39
+ { "title": "Add a cadence-generator job", "path": "guides/add-a-cadence-job.md" },
40
+ { "title": "Add a compliance-sweep job", "path": "guides/add-a-compliance-job.md" },
41
+ { "title": "Seed config and reference data on install", "path": "guides/seed-data-on-install.md" },
42
+ { "title": "Add menus", "path": "guides/add-menus.md" },
43
+ { "title": "Add translations (i18n)", "path": "guides/add-i18n.md" },
44
+ { "title": "Make a plugin work on desktop, tablet, and mobile", "path": "guides/responsive-plugin.md" },
45
+ { "title": "Add a custom block (a \"widget\" you build yourself)", "path": "guides/add-a-custom-block.md" },
46
+ { "title": "Build a plugin with custom React + Java code (L4)", "path": "guides/build-a-code-plugin.md" },
47
+ { "title": "Validate and test a plugin", "path": "guides/validate-and-test.md" },
48
+ { "title": "Publish and upgrade a plugin", "path": "guides/publish-and-upgrade.md" },
49
+ { "title": "Build a tenant extension service (L5)", "path": "guides/build-a-tenant-extension-service.md" },
50
+ { "title": "Build a Service-mode plugin in Python or Node.js", "path": "guides/build-a-polyglot-service-plugin.md" },
51
+ { "title": "Use the MCP server with an AI agent", "path": "guides/use-the-mcp-server.md" },
52
+ { "title": "Expose a plugin operation as an AI tool", "path": "guides/expose-a-plugin-operation-as-an-ai-tool.md" }
53
+ ]
54
+ },
55
+ {
56
+ "section": "Reference",
57
+ "items": [
58
+ { "title": "Schema reference", "path": "reference/index.md" }
59
+ ]
60
+ },
61
+ {
62
+ "section": "Tutorial",
63
+ "items": [
64
+ { "title": "Overview: build the Office Equipment module", "path": "tutorial/index.md" },
65
+ { "title": "1. Create the plugin", "path": "tutorial/01-create-the-plugin.md" },
66
+ { "title": "2. The equipment category entity + seeded data", "path": "tutorial/02-category-entity-and-seed.md" },
67
+ { "title": "3. The equipment register entity", "path": "tutorial/03-equipment-entity.md" },
68
+ { "title": "4. The equipment register page", "path": "tutorial/04-equipment-register-page.md" },
69
+ { "title": "5. The checkout entity, page, and lookups", "path": "tutorial/05-checkout-entity-and-page.md" },
70
+ { "title": "6. The checkout approval workflow", "path": "tutorial/06-checkout-approval-workflow.md" },
71
+ { "title": "7. The return-due reminder job", "path": "tutorial/07-return-due-reminder-job.md" },
72
+ { "title": "8. Menus, i18n, validate, publish", "path": "tutorial/08-menus-i18n-publish.md" }
73
+ ]
74
+ },
75
+ {
76
+ "section": "Recipes",
77
+ "items": [
78
+ { "title": "Recipe index", "path": "recipes/index.md" },
79
+ { "title": "A searchable related-record lookup field", "path": "recipes/related-record-lookup-field.md" },
80
+ { "title": "A resolved reference column in a grid", "path": "recipes/resolved-reference-grid-column.md" },
81
+ { "title": "A KPI dashboard page", "path": "recipes/kpi-dashboard-page.md" },
82
+ { "title": "A master-detail page with a child grid", "path": "recipes/master-detail-page.md" },
83
+ { "title": "An entity with an approval workflow", "path": "recipes/entity-with-approval-workflow.md" },
84
+ { "title": "An N-days-before reminder job", "path": "recipes/n-days-before-reminder.md" },
85
+ { "title": "A cross-row aggregation into a summary entity", "path": "recipes/cross-row-aggregation.md" },
86
+ { "title": "A scheduled record-generation cadence", "path": "recipes/scheduled-record-generation.md" },
87
+ { "title": "A condition-based compliance flag", "path": "recipes/condition-based-compliance-flag.md" },
88
+ { "title": "A cross-plugin offboarding fan-out", "path": "recipes/cross-plugin-offboarding-fanout.md" },
89
+ { "title": "A per-row document artifact generator", "path": "recipes/per-row-document-generator.md" },
90
+ { "title": "Install-lifecycle data seeding", "path": "recipes/install-lifecycle-data-seeding.md" }
91
+ ]
92
+ },
93
+ {
94
+ "section": "Troubleshooting",
95
+ "items": [
96
+ { "title": "Known gotchas", "path": "troubleshooting/known-gotchas.md" },
97
+ { "title": "Publishing and install errors", "path": "troubleshooting/publishing-errors.md" },
98
+ { "title": "\"My grid shows 0 rows\"", "path": "troubleshooting/grid-shows-zero-rows.md" },
99
+ { "title": "\"My scheduled job never runs\"", "path": "troubleshooting/job-never-runs.md" }
100
+ ]
101
+ },
102
+ {
103
+ "section": "Applications",
104
+ "items": [
105
+ { "title": "HCM: Employee Profile", "path": "applications/hcm/employee-profile.md" }
106
+ ]
107
+ }
108
+ ]
109
+ }
@@ -0,0 +1,134 @@
1
+ ---
2
+ title: Add a cadence-generator job
3
+ audience: tenant
4
+ ---
5
+
6
+ # Add a cadence-generator job
7
+
8
+ ## What you're doing
9
+
10
+ "Create new records on a schedule." A quarterly equipment audit, a monthly
11
+ review, a follow-up task N days after some event. **No Java.** One
12
+ `entity_cadence_config` row, run by the generic `engine-entity.cadence-generator`
13
+ job, shipped as seed data.
14
+
15
+ ## Two modes
16
+
17
+ | Mode | One tick creates | Set |
18
+ | --- | --- | --- |
19
+ | **standalone** | one new record every `interval_days` | `target_entity`, `interval_days`, `template_json`, `dedupe_target_field` |
20
+ | **source-driven** | one record per *due* row of a source entity | also `source_entity`, `source_date_field`, `due_within_days`, `field_map_json` |
21
+
22
+ ## The complete example — standalone quarterly audit
23
+
24
+ `spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json`
25
+ (standalone example,
26
+ [real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json)):
27
+
28
+ ```json
29
+ {
30
+ "entity": "entity_cadence_config",
31
+ "keyFields": ["cadence_code"],
32
+ "source": "office-equipment",
33
+ "rows": [
34
+ {
35
+ "cadence_code": "equipment-audit-schedule",
36
+ "target_entity": "oeq_audit",
37
+ "interval_days": 90,
38
+ "dedupe_target_field": "cadence_marker",
39
+ "template_json": {
40
+ "audit_number": "AUDIT-${date}-${ts}",
41
+ "audit_name": "Scheduled Equipment Audit ${date}",
42
+ "status": "DRAFT"
43
+ },
44
+ "seeded_by": "office-equipment",
45
+ "active": true
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ ## Line by line
52
+
53
+ - **`cadence_code`** — unique id; the natural key.
54
+ - **`target_entity`** — the entity to create rows in.
55
+ - **`interval_days`** — a new row is created only if the last one is older than
56
+ this many days (tracked in `last_generated_at`).
57
+ - **`dedupe_target_field`** — a column on the target that the job stamps to
58
+ recognise its own creations and avoid double-generating in one window.
59
+ - **`template_json`** — the field map for the new row. String values support
60
+ substitutions: `${date}` (today), `${ts}` (a timestamp), `${uuid}`, and (in
61
+ source-driven mode) `${sourceId}`.
62
+
63
+ ### Source-driven variant
64
+
65
+ Add:
66
+
67
+ ```json
68
+ "source_entity": "oeq_equipment",
69
+ "source_date_field": "warranty_expiry_date",
70
+ "due_within_days": 30,
71
+ "field_map_json": { "equipment_id": "id", "equipment_tag": "asset_tag" },
72
+ "dedupe_target_field": "equipment_id"
73
+ ```
74
+
75
+ → for every `oeq_equipment` whose `warranty_expiry_date` is within 30 days and
76
+ which doesn't already have a target row (deduped on `equipment_id`), create one,
77
+ copying `id`→`equipment_id` and `asset_tag`→`equipment_tag`.
78
+
79
+ ## The register rule
80
+
81
+ `spk-assembly/metadata/rules/ensure_cadence_generator_job_registered.json`:
82
+
83
+ ```json
84
+ {
85
+ "entityType": "entity_cadence_config",
86
+ "name": "ensure_cadence_generator_job_registered",
87
+ "triggerEvent": "AFTER_CREATE",
88
+ "conditions": null,
89
+ "actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityCadenceGeneratorJobRegistered\"}]",
90
+ "priority": 10,
91
+ "active": true
92
+ }
93
+ ```
94
+
95
+ ## Ground yourself first
96
+
97
+ ```bash
98
+ erp schema pull entity-cadence-config
99
+ erp examples patterns --kind jobs # "scheduled-record-generation-cadence"
100
+ ```
101
+
102
+ ## How to verify it worked
103
+
104
+ ```bash
105
+ erp api get "/api/v1/jobs/engine-entity.cadence-generator"
106
+ # → { "status": "ENABLED", ... }
107
+
108
+ erp api post "/api/v1/jobs/engine-entity.cadence-generator/execute" --body "{}"
109
+ erp api get "/api/v1/entities/oeq_audit/records/query?size=5"
110
+ ```
111
+
112
+ ```json
113
+ { "rows": [
114
+ { "audit_number": "AUDIT-2026-09-10-1757492940", "audit_name": "Scheduled Equipment Audit 2026-09-10",
115
+ "status": "DRAFT", "cadence_marker": "equipment-audit-schedule" }
116
+ ], "total": 1 }
117
+ ```
118
+
119
+ Run it again immediately — no new row (interval not elapsed). That's correct.
120
+
121
+ ## Common mistakes
122
+
123
+ | Symptom | Cause | Fix |
124
+ | --- | --- | --- |
125
+ | a new row every single tick | `interval_days` missing or 0 | set it |
126
+ | duplicate rows | `dedupe_target_field` not set, or not a stable column | in source-driven mode use the source-id column |
127
+ | `${date}` appears literally in a non-string field | substitution only works in string template values | keep templated values as strings |
128
+ | job not registered | no register rule shipped | ship it |
129
+
130
+ ## What to read next
131
+
132
+ - [Add a compliance-sweep job](./add-a-compliance-job.md)
133
+ - [Seed config and reference data on install](./seed-data-on-install.md)
134
+ - Recipe: [scheduled record-generation cadence](../recipes/scheduled-record-generation.md)
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Add a compliance-sweep job
3
+ audience: tenant
4
+ ---
5
+
6
+ # Add a compliance-sweep job
7
+
8
+ ## What you're doing
9
+
10
+ "Find rows that break a policy and flag them — or raise an escalation." More
11
+ expressive than a [reminder job](./add-a-reminder-job.md): the condition is a
12
+ **boolean expression** over the row's fields (plus `now` and an optional
13
+ `related_count`), not just one date column. **No Java.** One
14
+ `entity_compliance_config` row, run by `engine-entity.compliance-sweep`, shipped
15
+ as seed data.
16
+
17
+ Use this instead of a reminder job whenever the condition is more than "one date
18
+ vs now".
19
+
20
+ ## Two outcomes
21
+
22
+ | Outcome | Set |
23
+ | --- | --- |
24
+ | **FLAG** — set a field on the offending row | `set_field`, `set_value` |
25
+ | **ESCALATION** — raise a row in a separate entity, deduped by source id | `escalation_entity`, `escalation_dedupe_field`, `field_map_json`, `escalation_template_json` |
26
+
27
+ ## The complete example — flag un-acknowledged checkouts
28
+
29
+ `spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json`
30
+ (standalone example,
31
+ [real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json)):
32
+
33
+ ```json
34
+ {
35
+ "entity": "entity_compliance_config",
36
+ "keyFields": ["compliance_code"],
37
+ "source": "office-equipment",
38
+ "rows": [
39
+ {
40
+ "compliance_code": "checkout-acknowledgement-compliance",
41
+ "source_entity": "oeq_checkout",
42
+ "when_status_in": "CHECKED_OUT,DUE_SOON,OVERDUE",
43
+ "condition_expr": "custody_acknowledged != true && daysBetween(checkout_date, now()) > 7",
44
+ "set_field": "compliance_flag",
45
+ "set_value": "NON_COMPLIANT",
46
+ "seeded_by": "office-equipment",
47
+ "active": true
48
+ }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ ## Line by line
54
+
55
+ - **`compliance_code`** — unique id; natural key.
56
+ - **`source_entity`** — table to evaluate.
57
+ - **`when_status_in`** — optional status pre-filter.
58
+ - **`condition_expr`** — a boolean expression in the platform
59
+ `ExpressionEvaluator` language. In scope: the row's own fields, `now`, and (if
60
+ configured) `related_count`. Whitelisted functions only — `daysBetween`,
61
+ `now`, `coalesce`, arithmetic, `&&`, `||`, `!`, comparisons. **No** ternary,
62
+ **no** `in`, **no** `^`. A row where the expression is **true** is
63
+ non-compliant.
64
+ - **`set_field` / `set_value`** — FLAG outcome. Idempotent: a row already equal
65
+ to `set_value` is skipped.
66
+
67
+ ### Related-count variant (cross-record)
68
+
69
+ ```json
70
+ "related_count_entity": "oeq_license_assignment",
71
+ "related_count_key_field": "license_id",
72
+ "related_count_status_in": "ACTIVE",
73
+ "condition_expr": "related_count > entitlement_quantity"
74
+ ```
75
+
76
+ → counts `ACTIVE` assignments whose `license_id` equals the source row's id,
77
+ binds it as `related_count`, and flags over-allocated licences.
78
+
79
+ ### Escalation variant
80
+
81
+ Replace `set_field`/`set_value` with:
82
+
83
+ ```json
84
+ "escalation_entity": "oeq_compliance_case",
85
+ "escalation_dedupe_field": "checkout_id",
86
+ "field_map_json": { "checkout_id": "id", "employee_id": "employee_id" },
87
+ "escalation_template_json": { "status": "OPEN", "severity": "HIGH" }
88
+ ```
89
+
90
+ → one `oeq_compliance_case` per offending checkout, deduped on `checkout_id`.
91
+
92
+ ## The register rule
93
+
94
+ `spk-assembly/metadata/rules/ensure_compliance_sweep_job_registered.json`:
95
+
96
+ ```json
97
+ {
98
+ "entityType": "entity_compliance_config",
99
+ "name": "ensure_compliance_sweep_job_registered",
100
+ "triggerEvent": "AFTER_CREATE",
101
+ "conditions": null,
102
+ "actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityComplianceSweepJobRegistered\"}]",
103
+ "priority": 10,
104
+ "active": true
105
+ }
106
+ ```
107
+
108
+ ## Ground yourself first
109
+
110
+ ```bash
111
+ erp schema pull entity-compliance-config
112
+ erp examples patterns --kind jobs # "condition-based-compliance-flagging"
113
+ ```
114
+
115
+ ## How to verify it worked
116
+
117
+ ```bash
118
+ erp api get "/api/v1/jobs/engine-entity.compliance-sweep"
119
+ # → { "status": "ENABLED", ... }
120
+
121
+ erp api post "/api/v1/jobs/engine-entity.compliance-sweep/execute" --body "{}"
122
+ erp api get "/api/v1/entities/oeq_checkout/records/query?size=10"
123
+ ```
124
+
125
+ Rows that break the policy now carry `"compliance_flag": "NON_COMPLIANT"`.
126
+
127
+ ## Common mistakes
128
+
129
+ | Symptom | Cause | Fix |
130
+ | --- | --- | --- |
131
+ | expression parse error | used ternary / `in` / `^` | rewrite with whitelisted operators |
132
+ | nothing flagged | `condition_expr` logic inverted (true = compliant) | true must mean *non*-compliant |
133
+ | `related_count` is always 0 | `related_count_key_field` isn't the FK back to the source id | fix the FK column name |
134
+ | escalations duplicate | `escalation_dedupe_field` not the source-id column | must be a numeric column holding the source id |
135
+
136
+ ## What to read next
137
+
138
+ - [Add business rules and expressions](./add-business-rules.md) — same expression language
139
+ - Recipe: [condition-based compliance flag](../recipes/condition-based-compliance-flag.md)
@@ -0,0 +1,167 @@
1
+ ---
2
+ title: Add a create / edit form
3
+ audience: tenant
4
+ ---
5
+
6
+ # Add a create / edit form
7
+
8
+ ## What you're doing
9
+
10
+ Adding a dialog to a list page that creates (or edits) a row. On this platform a
11
+ "form" for a list page is not a separate artifact — it's a `core.dialog` block on
12
+ the page, containing input blocks bound to page state, plus a Save button whose
13
+ action chain `POST`s (or `PUT`s) to the entity.
14
+
15
+ ## The pattern
16
+
17
+ 1. **A "New" button** clears the form's page-state keys and opens the dialog.
18
+ 2. **Each field** is an input block (`core.text-input`, `core.select`,
19
+ `core.date-picker`, `core.number-input`, `core.checkbox`, `core.lookup`)
20
+ whose `value` binds a page-state key and whose `committed` event writes
21
+ `${event.new}` back to that key.
22
+ 3. **Save** calls `POST /api/v1/entities/<name>/records` with a `params` object
23
+ built from the page-state keys, closes the dialog, toggles the grid refresh,
24
+ and shows a toast.
25
+
26
+ ## The complete example
27
+
28
+ From the tutorial's `equipment-register.json`. The "New" button:
29
+
30
+ ```json
31
+ {
32
+ "blockType": "core.button",
33
+ "properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.newBtn" },
34
+ "variant": { "source": "static", "value": "contained" } },
35
+ "events": { "clicked": { "source": "action-chain", "actions": [
36
+ { "id": "a0", "order": 0, "type": "setValue", "config": { "field": "page.formAssetTag", "value": null } },
37
+ { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formName", "value": null } },
38
+ { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.formCategoryId", "value": null } },
39
+ { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formStatus", "value": "AVAILABLE" } },
40
+ { "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
41
+ ] } }
42
+ }
43
+ ```
44
+
45
+ One text field inside the dialog:
46
+
47
+ ```json
48
+ {
49
+ "blockType": "core.text-input",
50
+ "properties": { "value": { "source": "binding", "binding": { "scope": "page", "key": "formAssetTag" } } },
51
+ "events": { "committed": { "source": "action-chain", "actions": [
52
+ { "id": "a0", "order": 0, "type": "setValue",
53
+ "config": { "field": "page.formAssetTag", "value": "${event.new}" } }
54
+ ] } }
55
+ }
56
+ ```
57
+
58
+ The Save button:
59
+
60
+ ```json
61
+ {
62
+ "blockType": "core.button",
63
+ "properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.form.saveBtn" },
64
+ "variant": { "source": "static", "value": "contained" } },
65
+ "events": { "clicked": { "source": "action-chain", "actions": [
66
+ { "id": "a0", "order": 0, "type": "callApi",
67
+ "config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records", "httpMethod": "POST",
68
+ "params": {
69
+ "asset_tag": "${page.formAssetTag}",
70
+ "name": "${page.formName}",
71
+ "category_id": "${page.formCategoryId}",
72
+ "status": "${page.formStatus}"
73
+ } },
74
+ "output": "createResult" },
75
+ { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": false } },
76
+ { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } },
77
+ { "id": "a3", "order": 3, "type": "showToast", "config": { "message": "Saved." } }
78
+ ] } }
79
+ }
80
+ ```
81
+
82
+ ## Line by line — the rules that bite
83
+
84
+ - **`committed` fires with `event.new`.** The value the user just entered is
85
+ `${event.new}`. **Not** `event.value`, **not** `event.record`. This is true for
86
+ text inputs, selects, date pickers, checkboxes, and `core.lookup`.
87
+ - **The handler must write back to state.** `committed` → `setValue page.<key> =
88
+ ${event.new}`. Without this the input shows what you type but page state never
89
+ updates, and Save sends nulls.
90
+ - **Save reads from `${page.<key>}`** in the `params` object — the same keys the
91
+ inputs write and the "New" button clears.
92
+ - **Clear the keys in the "New" button**, not on dialog close — so re-opening
93
+ always starts blank (or with defaults like `status: "AVAILABLE"`).
94
+ - **Booleans and numbers** flow through as their JSON types when you `setValue`
95
+ a real `true`/`10`; only `defaultValue` in an *entity* file must be a string.
96
+
97
+ ## Editing an existing row
98
+
99
+ Same dialog. The grid's `rowClicked` event fetches the record and populates the
100
+ same page-state keys, then opens the dialog:
101
+
102
+ ```json
103
+ "events": { "rowClicked": { "source": "action-chain", "actions": [
104
+ { "id": "a0", "order": 0, "type": "callApi",
105
+ "config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records/${event.id}", "httpMethod": "GET", "params": {} },
106
+ "output": "detail" },
107
+ { "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formAssetTag", "value": "${detail.asset_tag}" } },
108
+ { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.editingId", "value": "${event.id}" } },
109
+ { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
110
+ ] } }
111
+ ```
112
+
113
+ Save then branches on `page.editingId` — `PUT .../records/${page.editingId}` if
114
+ set, `POST` if not. (`${event.id}` is the clicked row's id.)
115
+
116
+ ## A lookup field (foreign key)
117
+
118
+ ```json
119
+ {
120
+ "blockType": "core.lookup",
121
+ "properties": {
122
+ "value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
123
+ "recordType": { "source": "static", "value": "OeqCategory" },
124
+ "optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
125
+ },
126
+ "events": { "committed": { "source": "action-chain", "actions": [
127
+ { "id": "a0", "order": 0, "type": "setValue",
128
+ "config": { "field": "page.formCategoryId", "value": "${event.new}" } }
129
+ ] } }
130
+ }
131
+ ```
132
+
133
+ `optionsSourceKey` is the `name` of a `search` Data Service. `recordType` is a
134
+ documentation-only label. `${event.new}` is the picked record's id. See
135
+ [the lookup recipe](../recipes/related-record-lookup-field.md).
136
+
137
+ ## How to verify it worked
138
+
139
+ Publish, then drive it from the API the same way the Save chain does:
140
+
141
+ ```bash
142
+ erp api post "/api/v1/entities/oeq_equipment/records" \
143
+ --body '{"asset_tag":"LAP-001","name":"Dell Latitude 7440","category_id":1,"status":"AVAILABLE"}'
144
+ ```
145
+
146
+ ```json
147
+ { "id": 1, "created_by": "you@example.com", "asset_tag": "LAP-001",
148
+ "name": "Dell Latitude 7440", "status": "AVAILABLE", "warranty_alert": false }
149
+ ```
150
+
151
+ If that works, the dialog works.
152
+
153
+ ## Common mistakes
154
+
155
+ | Symptom | Cause | Fix |
156
+ | --- | --- | --- |
157
+ | Save sends `null` for a field the user filled in | `committed` handler missing or reads `event.value` | write `setValue page.<key> = ${event.new}` |
158
+ | dialog re-opens with last row's data | keys cleared on close instead of on "New" | clear in the "New" button chain |
159
+ | grid doesn't show the new row | no `refreshTrigger` toggle in the Save chain | add `setValue page.gridRefreshTick = ${!page.gridRefreshTick}` |
160
+ | lookup saves a name, not an id | bound the label instead of `${event.new}` | `${event.new}` is the id |
161
+ | `POST` 400s on an enum field | value not one of `typeParams.enumValues` | check the entity's enum list |
162
+
163
+ ## What to read next
164
+
165
+ - [Add a data provider, data view, or data service](./add-a-data-provider.md)
166
+ - [Add an approval workflow](./add-an-approval-workflow.md) — a Submit button
167
+ - Recipe: [master-detail page with a child grid](../recipes/master-detail-page.md)