@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,153 @@
1
+ ---
2
+ title: Wire a page's data
3
+ audience: tenant
4
+ ---
5
+
6
+ # Wire a page's data
7
+
8
+ ## What you're doing
9
+
10
+ You have a page with a `core.grid` block and it shows **zero rows and no error
11
+ message**. This is the single most common confusion on the platform. This guide
12
+ explains the one chain that connects a grid to a table.
13
+
14
+ ## The mental model
15
+
16
+ ```
17
+ page.metadata.dataSource ──names──▶ a Data Provider file ──points at──▶ a REST base path
18
+ "oeq-equipment-provider" metadata/provider/ /api/v1/entities/oeq_equipment/records
19
+ ```
20
+
21
+ Three things must line up **exactly**:
22
+
23
+ 1. The page's top-level **`metadata.dataSource`** is a string.
24
+ 2. That string equals the **`name`** field of a file in
25
+ `spk-assembly/metadata/provider/`.
26
+ 3. That provider's **`definition.basePath`** is a real REST path that supports
27
+ `search`.
28
+
29
+ The grid block itself has **no** data configuration. It renders whatever the
30
+ page's `dataSource` provider returns.
31
+
32
+ ## The complete example
33
+
34
+ Page (`equipment-register.json`), last two lines of the file:
35
+
36
+ ```json
37
+ "metadata": { "dataSource": "oeq-equipment-provider" },
38
+ "modules": ["office-equipment"]
39
+ }
40
+ ```
41
+
42
+ Provider (`spk-assembly/metadata/provider/oeq-equipment-provider.json`) —
43
+ [the real file](../tutorial/example-plugin/spk-assembly/metadata/provider/oeq-equipment-provider.json):
44
+
45
+ ```json
46
+ {
47
+ "name": "oeq-equipment-provider",
48
+ "description": "Data Provider for oeq_equipment (grid + CRUD).",
49
+ "definition": {
50
+ "kind": "rest",
51
+ "connectionRef": "self",
52
+ "basePath": "/api/v1/entities/oeq_equipment/records",
53
+ "supports": ["search", "get", "create", "update"]
54
+ },
55
+ "metadata": {},
56
+ "modules": []
57
+ }
58
+ ```
59
+
60
+ The string `"oeq-equipment-provider"` appears in **both** files, spelled
61
+ identically. That is the whole wiring.
62
+
63
+ ## Line by line
64
+
65
+ - **`metadata.dataSource`** — page-level. Not inside `definition`, not on any
66
+ block. A grid with no matching `dataSource` shows nothing.
67
+ - **provider `name`** — the lookup key. Must match `dataSource` character for
68
+ character.
69
+ - **`kind: "rest"`** — the provider calls a REST endpoint.
70
+ - **`connectionRef: "self"`** — this ERP's own backend. (External systems use a
71
+ named connection.)
72
+ - **`basePath`** — for an entity you own, it's always
73
+ `/api/v1/entities/<entityName>/records`. The Entity Engine serves `search` at
74
+ `<basePath>/query`, `get` at `<basePath>/{id}`, `create` at `POST <basePath>`,
75
+ `update` at `PUT <basePath>/{id}`.
76
+ - **`supports`** — which of those the grid/dialogs may use.
77
+
78
+ ## Filtering the grid
79
+
80
+ The grid's `externalFilter` property binds a page state object. Its keys become
81
+ query filters:
82
+
83
+ ```json
84
+ "externalFilter": { "source": "binding", "binding": { "scope": "page", "key": "gridExternalFilter" } }
85
+ ```
86
+
87
+ A search input then writes into it:
88
+
89
+ ```json
90
+ "events": { "committed": { "source": "action-chain", "actions": [
91
+ { "id": "a0", "order": 0, "type": "setValue",
92
+ "config": { "field": "page.gridExternalFilter.search", "value": "${event.new}" } }
93
+ ] } }
94
+ ```
95
+
96
+ Now typing in the box filters the grid on `search`; picking a status filters on
97
+ `status`. (`${event.new}` — the committed value — see
98
+ [Add a create/edit form](./add-a-create-edit-form.md).)
99
+
100
+ ## Refreshing the grid after a write
101
+
102
+ Bind `refreshTrigger` to a page value and flip it at the end of a save chain:
103
+
104
+ ```json
105
+ { "id": "a2", "order": 2, "type": "setValue",
106
+ "config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } }
107
+ ```
108
+
109
+ ## When you need a computed / joined read
110
+
111
+ If the grid needs columns from more than one table, or server-side aggregation,
112
+ the provider's `basePath` can point at a **Data Service** or **Data View**
113
+ instead of a raw entity. See [Add a data provider, data view, or data
114
+ service](./add-a-data-provider.md).
115
+
116
+ ## How to verify it worked
117
+
118
+ The semantic linter catches the mismatch before you publish:
119
+
120
+ ```bash
121
+ erp plugin test office-equipment/spk-assembly
122
+ ```
123
+
124
+ If the wiring is broken you get:
125
+
126
+ ```
127
+ PAGE-DS-001 equipment-register.json: core.grid present but metadata.dataSource
128
+ "oeq-equipmnt-provider" does not match any metadata/provider/*.json name
129
+ ```
130
+
131
+ When it's right, `erp plugin test` is silent on `PAGE-DS-001` and after publish:
132
+
133
+ ```bash
134
+ erp api get "/api/v1/entities/oeq_equipment/records/query?size=3"
135
+ ```
136
+
137
+ returns real rows — which is exactly what the grid will show.
138
+
139
+ ## Common mistakes
140
+
141
+ | Symptom | Cause | Fix |
142
+ | --- | --- | --- |
143
+ | grid empty, no error | `dataSource` typo / absent / put on the grid block | move it to page `metadata.dataSource`, match the provider `name` |
144
+ | grid empty but the entity has rows | provider `basePath` wrong | for your own entity it's `/api/v1/entities/<name>/records` |
145
+ | grid loads once, never refreshes after save | no `refreshTrigger` binding | bind it and toggle it at the end of the save chain |
146
+ | filter box does nothing | `externalFilter` not bound, or `committed` writes the wrong key | bind `gridExternalFilter`; write `page.gridExternalFilter.<key>` |
147
+ | "0 rows" only for some users | row-level permissions | expected — check the user's role |
148
+
149
+ ## What to read next
150
+
151
+ - [Add a data provider, data view, or data service](./add-a-data-provider.md)
152
+ - [Add a create / edit form](./add-a-create-edit-form.md)
153
+ - Troubleshooting: [My grid shows 0 rows](../troubleshooting/grid-shows-zero-rows.md)
@@ -0,0 +1,118 @@
1
+ ---
2
+ title: Build and publish a one-page plugin
3
+ audience: tenant
4
+ ---
5
+
6
+ # Quickstart: a one-page plugin, live
7
+
8
+ By the end of this page you will have a plugin installed on your ERP tenant with
9
+ one working page reachable from the app menu. Every command is real against the
10
+ current `erp` CLI.
11
+
12
+ ## 0. Prerequisites
13
+
14
+ - Node.js 20+.
15
+ - The plugin SDK on your `PATH` (the `erp` command). Until the SDK is published
16
+ to npm you run it directly: `node <sdk>/tools/erp-cli/erp.mjs`. Substitute that
17
+ for `erp` below.
18
+ - The base URL of your ERP (e.g. `https://erp.example.com`).
19
+
20
+ ## 1. Point the CLI at your ERP and log in
21
+
22
+ ```bash
23
+ erp env use prod --base-url https://erp.example.com
24
+ erp login
25
+ ```
26
+
27
+ `erp login` opens your browser, you authorize the CLI, and the session is written
28
+ to `~/.erp/config.json`. Confirm it:
29
+
30
+ ```bash
31
+ erp whoami
32
+ ```
33
+
34
+ ## 2. Sync the authoring bundle (optional but recommended)
35
+
36
+ ```bash
37
+ erp env sync
38
+ ```
39
+
40
+ This pulls your ERP's schema/catalog/example/doc bundle into
41
+ `~/.erp/cache/`. If your ERP does not expose the bundle endpoint yet, this
42
+ no-ops and the SDK falls back to the bundle shipped inside the SDK package —
43
+ you can keep going.
44
+
45
+ ## 3. Wire up the MCP server for your IDE agent
46
+
47
+ ```bash
48
+ erp mcp install
49
+ ```
50
+
51
+ Writes a `.mcp.json` in the current directory pointing at the
52
+ `erp-plugin-language` MCP server, so Claude Code / Cursor / Codex can call
53
+ `erp_get_schema`, `erp_list_blocks`, `erp_platform_catalog`,
54
+ `erp_search_docs`, `erp_examples_patterns`, `erp_validate_page`, and the rest.
55
+
56
+ ## 4. Scaffold the plugin
57
+
58
+ ```bash
59
+ erp plugin create acme-hello --name "Acme Hello" --type business-app
60
+ ```
61
+
62
+ This creates `acme-hello/` with a `spk-assembly/` tree (`plugin.json`,
63
+ `metadata/page/`, `metadata/menu/`, `metadata/i18n/`, …).
64
+
65
+ > **No Java needed.** The scaffold leaves `plugin.json`'s `mainClass` `null` and
66
+ > that is exactly right for a pure-JSON plugin — the platform loads it through a
67
+ > built-in generic entry point. You compile a `Plugin` class only if your plugin
68
+ > ships its own Java behaviour. See
69
+ > [Create a plugin from scratch](../guides/create-a-plugin.md).
70
+
71
+ Author one page under `spk-assembly/metadata/page/hello.json`. Ground yourself
72
+ first — do not guess the shape:
73
+
74
+ ```bash
75
+ erp schema pull page # the page.json contract
76
+ erp blocks list --category display # what widgets exist
77
+ erp examples patterns --kind page # curated real page shapes
78
+ ```
79
+
80
+ Give the page a menu entry:
81
+
82
+ ```bash
83
+ erp menu create acme-hello acme-hello.main --display-name "Hello" --route /hello
84
+ ```
85
+
86
+ ## 5. Validate before you ship
87
+
88
+ ```bash
89
+ erp plugin validate acme-hello/spk-assembly
90
+ erp plugin test acme-hello/spk-assembly # semantic lints (i18n keys, grid data-source, …)
91
+ ```
92
+
93
+ Fix anything flagged. These are the same checks the platform runs at install
94
+ time — a clean run here means a clean install.
95
+
96
+ ## 6. Build and publish
97
+
98
+ ```bash
99
+ erp plugin build acme-hello/spk-assembly -o acme-hello.spk
100
+ erp plugin publish acme-hello.spk
101
+ ```
102
+
103
+ ## 7. See it live
104
+
105
+ Open your ERP, navigate to the app that owns the module, and you'll find the
106
+ **Hello** menu item. The page URL is
107
+ `/app/<app>/<module>/acme-hello/hello` (the route pattern in your menu file is
108
+ *not* the browser URL — see `troubleshooting/known-gotchas.md`).
109
+
110
+ ## Next
111
+
112
+ - [The tutorial](../tutorial/index.md) — build and publish a real 3-page module
113
+ end to end, every step verified.
114
+ - [Guides](../guides/index.md) — every task as its own page.
115
+ - [concepts/overview.md](../concepts/overview.md) — how the pieces fit together.
116
+ - [reference/](../reference/index.md) — every authoring artifact's JSON Schema.
117
+ - [troubleshooting/known-gotchas.md](../troubleshooting/known-gotchas.md) — the
118
+ mistakes everyone makes once.
@@ -0,0 +1,70 @@
1
+ ---
2
+ title: A condition-based compliance flag
3
+ audience: tenant
4
+ ---
5
+
6
+ # A condition-based compliance flag
7
+
8
+ ## When you need this
9
+
10
+ "Find rows that break a policy and flag them" — where the policy is more than one
11
+ date vs now. Un-acknowledged custody after 7 days, over-allocated licences,
12
+ missing documentation. Zero Java — one `entity_compliance_config` row run by
13
+ `engine-entity.compliance-sweep`.
14
+
15
+ ## The file
16
+
17
+ `metadata/seed-data/office-equipment-compliance-configs.json`
18
+ ([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json)):
19
+
20
+ ```json
21
+ {
22
+ "entity": "entity_compliance_config",
23
+ "keyFields": ["compliance_code"],
24
+ "source": "office-equipment",
25
+ "rows": [
26
+ {
27
+ "compliance_code": "checkout-acknowledgement-compliance",
28
+ "source_entity": "oeq_checkout",
29
+ "when_status_in": "CHECKED_OUT,DUE_SOON,OVERDUE",
30
+ "condition_expr": "custody_acknowledged != true && daysBetween(checkout_date, now()) > 7",
31
+ "set_field": "compliance_flag",
32
+ "set_value": "NON_COMPLIANT",
33
+ "active": true
34
+ }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ A row where `condition_expr` is **true** is non-compliant; the job sets
40
+ `compliance_flag = "NON_COMPLIANT"` on it (idempotent — a row already flagged is
41
+ skipped).
42
+
43
+ ## The expression language
44
+
45
+ The platform `ExpressionEvaluator` — the same one that powers formula fields and
46
+ cross-field validation. In scope: the row's fields, `now`, and (if configured)
47
+ `related_count`. Allowed: `&&`, `||`, `!`, comparisons, arithmetic, and
48
+ whitelisted functions (`daysBetween`, `now`, `coalesce`, …). **Not allowed:**
49
+ ternary `?:`, `in`, `^`.
50
+
51
+ ## Variants
52
+
53
+ - **Related-count** (cross-record) — add `related_count_entity`,
54
+ `related_count_key_field` (FK back to the source id), `related_count_status_in`;
55
+ then use `related_count` in the expression, e.g.
56
+ `related_count > entitlement_quantity`.
57
+ - **Escalation** (raise a row in another entity) — replace `set_field`/`set_value`
58
+ with `escalation_entity`, `escalation_dedupe_field` (a numeric column holding
59
+ the source id), `field_map_json`, `escalation_template_json`.
60
+
61
+ ## Also ship
62
+
63
+ - the shared `entity_compliance_config.json` (additive), and
64
+ - `metadata/rules/ensure_compliance_sweep_job_registered.json`
65
+ (`AFTER_CREATE` → `ensureEntityComplianceSweepJobRegistered`).
66
+
67
+ ## Related
68
+
69
+ - [Add a compliance-sweep job](../guides/add-a-compliance-job.md)
70
+ - [Add business rules and expressions](../guides/add-business-rules.md) — same expression language
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: A cross-plugin offboarding fan-out
3
+ audience: tenant
4
+ ---
5
+
6
+ # A cross-plugin offboarding fan-out
7
+
8
+ ## When you need this
9
+
10
+ "When an employee is separated, auto-create the follow-up records for everything
11
+ they hold" — asset return requests, licence reclamations, access revocations. The
12
+ trigger entity can belong to **another plugin** entirely. Zero Java, zero
13
+ cross-plugin HTTP — one `entity_cross_plugin_action_config` row run by
14
+ `engine-entity.cross-plugin-action`.
15
+
16
+ ## The file
17
+
18
+ `metadata/seed-data/office-equipment-cross-plugin-configs.json`:
19
+
20
+ ```json
21
+ {
22
+ "entity": "entity_cross_plugin_action_config",
23
+ "keyFields": ["action_code"],
24
+ "source": "office-equipment",
25
+ "rows": [
26
+ {
27
+ "action_code": "equipment-separation-return",
28
+ "trigger_entity": "employee",
29
+ "trigger_status_field": "employment_status",
30
+ "trigger_status_in": "TERMINATED,SEPARATED",
31
+ "trigger_key_field": "id",
32
+ "related_entity": "oeq_checkout",
33
+ "related_match_field": "employee_id",
34
+ "related_status_in": "APPROVED,CHECKED_OUT,DUE_SOON,OVERDUE",
35
+ "create_entity": "oeq_return",
36
+ "create_dedupe_field": "checkout_id",
37
+ "create_template_json": { "status": "REQUESTED", "reason": "EMPLOYEE_SEPARATION" },
38
+ "create_field_map_json": { "equipment_id": "equipment_id", "employee_id": "employee_id" },
39
+ "related_set_field": "status",
40
+ "related_set_value": "RETURN_PENDING",
41
+ "active": true
42
+ }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ ## How to read it
48
+
49
+ When an `employee` row's `employment_status` becomes `TERMINATED` or
50
+ `SEPARATED`, the job finds every `oeq_checkout` for that `employee_id` in an
51
+ active status, creates one `oeq_return` per checkout (deduped on `checkout_id`,
52
+ which **must be a numeric/long column**), copies the mapped fields, applies the
53
+ template, and sets the source checkout's status to `RETURN_PENDING`.
54
+
55
+ ## The rules that bite
56
+
57
+ - `create_dedupe_field` must be a numeric column holding the related row's id —
58
+ it's how the job avoids double-creating on the next tick.
59
+ - The `trigger_entity` can be owned by any plugin; you just need its name and
60
+ the status field.
61
+ - Ship the shared `entity_cross_plugin_action_config.json` (additive) and an
62
+ `AFTER_CREATE` register rule calling
63
+ `ensureEntityCrossPluginActionJobRegistered`.
64
+
65
+ ## Related
66
+
67
+ - `erp examples patterns --kind jobs` → `cross-plugin-offboarding-fanout`
68
+ - [Add a cadence-generator job](../guides/add-a-cadence-job.md) — the source-driven cousin
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: A cross-row aggregation into a summary entity
3
+ audience: tenant
4
+ ---
5
+
6
+ # A cross-row aggregation into a summary entity
7
+
8
+ ## When you need this
9
+
10
+ "Count / sum / average many rows on a schedule and store the result." A nightly
11
+ inventory reconciliation, per-asset maintenance-cost totals, seats-used per
12
+ licence. Zero Java — one `entity_aggregation_config` row run by
13
+ `engine-entity.aggregation-sweep`.
14
+
15
+ ## The file
16
+
17
+ `metadata/seed-data/office-equipment-aggregation-configs.json`
18
+ ([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-aggregation-configs.json)):
19
+
20
+ ```json
21
+ {
22
+ "entity": "entity_aggregation_config",
23
+ "keyFields": ["sweep_code"],
24
+ "source": "office-equipment",
25
+ "rows": [
26
+ {
27
+ "sweep_code": "equipment-status-reconciliation",
28
+ "source_entity": "oeq_equipment",
29
+ "aggregation": "count",
30
+ "group_by_field": "status",
31
+ "target_entity": "oeq_reconciliation",
32
+ "target_key_field": "metric_key",
33
+ "target_key_prefix": "equipment_status:",
34
+ "target_value_field": "metric_value",
35
+ "target_timestamp_field": "last_reconciled_at",
36
+ "active": true
37
+ }
38
+ ]
39
+ }
40
+ ```
41
+
42
+ Result: one `oeq_reconciliation` row per status, keyed
43
+ `equipment_status:AVAILABLE`, `equipment_status:ASSIGNED`, … with the count in
44
+ `metric_value`.
45
+
46
+ ## Variants
47
+
48
+ - **`sum` / `avg` / `min` / `max`** — add `agg_field` (the numeric column).
49
+ - **Write onto the parent row instead of a summary entity** — set
50
+ `target_key_field: "id"`, leave `target_key_prefix` blank; the `group_by_field`
51
+ values are the parent's primary keys. (E.g. sum line-item cost onto the
52
+ maintenance record's own `synced_cost`.)
53
+ - **Filter first** — add `status_field` + `when_status_in`.
54
+
55
+ ## Also ship
56
+
57
+ - `metadata/entities/oeq_reconciliation.json` (the summary entity), and the
58
+ shared `entity_aggregation_config.json` (additive).
59
+ - `metadata/rules/ensure_aggregation_sweep_job_registered.json` — an
60
+ `AFTER_CREATE` `EXECUTE_SERVICE` rule calling
61
+ `ensureEntityAggregationSweepJobRegistered` (this job does **not** self-register
62
+ from a platform rule — you ship it).
63
+
64
+ ## Related
65
+
66
+ - [Add a cross-row aggregation job](../guides/add-an-aggregation-job.md)
67
+ - [A KPI dashboard page](./kpi-dashboard-page.md) — the on-demand version
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: An entity with an approval workflow + reminder job
3
+ audience: tenant
4
+ ---
5
+
6
+ # An entity with an approval workflow + reminder job
7
+
8
+ ## When you need this
9
+
10
+ A record with a full lifecycle: it's drafted, submitted for approval, approved,
11
+ acted on, and eventually chased if it goes stale. This recipe combines
12
+ [the approval workflow](../guides/add-an-approval-workflow.md) and
13
+ [the reminder job](../guides/add-a-reminder-job.md) on one entity — exactly what
14
+ the tutorial's `oeq_checkout` does.
15
+
16
+ ## The five files
17
+
18
+ | File | Purpose |
19
+ | --- | --- |
20
+ | `metadata/entities/oeq_checkout.json` | the entity; status enum includes every lifecycle value (`DRAFT`, `PENDING_APPROVAL`, `APPROVED`, `CHECKED_OUT`, `DUE_SOON`, `OVERDUE`, `RETURNED`, `REJECTED`) |
21
+ | `metadata/workflow/office-equipment.checkout-approval.json` | single-stage manager approval |
22
+ | `metadata/rules/oeq_checkout_submit_workflow.json` | `AFTER_UPDATE` rule: status → `PENDING_APPROVAL` starts the workflow; decision writes `APPROVED`/`REJECTED` back |
23
+ | `metadata/entities/entity_status_date_sweep_config.json` | ship the shared config entity (idempotent-additive) |
24
+ | `metadata/seed-data/office-equipment-sweep-configs.json` | 2 sweep configs: `CHECKED_OUT` within 3 days → `DUE_SOON`; past due → `OVERDUE` |
25
+
26
+ All five are real in
27
+ [`tutorial/example-plugin/`](../tutorial/example-plugin/spk-assembly/metadata/).
28
+
29
+ ## The lifecycle, end to end
30
+
31
+ ```
32
+ DRAFT
33
+ │ user: Submit for approval (PUT status = PENDING_APPROVAL)
34
+
35
+ PENDING_APPROVAL ──rule──▶ workflow instance + manager task
36
+ │ manager approves ──callback──▶ status = APPROVED
37
+
38
+ APPROVED
39
+ │ equipment handed over (PUT status = CHECKED_OUT)
40
+
41
+ CHECKED_OUT
42
+ │ nightly sweep: due_date within 3 days
43
+
44
+ DUE_SOON
45
+ │ nightly sweep: due_date passed
46
+
47
+ OVERDUE
48
+ │ equipment returned (PUT status = RETURNED)
49
+
50
+ RETURNED
51
+ ```
52
+
53
+ The **user** drives `DRAFT → PENDING_APPROVAL`, `APPROVED → CHECKED_OUT`,
54
+ `* → RETURNED` (buttons on the page). The **workflow** drives
55
+ `PENDING_APPROVAL → APPROVED/REJECTED`. The **sweep job** drives
56
+ `CHECKED_OUT → DUE_SOON → OVERDUE`. No two mechanisms fight over the same
57
+ transition.
58
+
59
+ ## The rules that bite
60
+
61
+ - The submit rule needs the `status__previous` guard so it fires only on the
62
+ transition, not every later update.
63
+ - The `OVERDUE` sweep config's `when_status_in` includes `DUE_SOON`, so a warned
64
+ checkout still escalates.
65
+ - Sweep configs flip the **status enum**, not a boolean flag (the generic job
66
+ can't write a string to a boolean column).
67
+
68
+ ## Related
69
+
70
+ - [Add an approval workflow](../guides/add-an-approval-workflow.md)
71
+ - [Add a scheduled reminder job](../guides/add-a-reminder-job.md)
72
+ - Tutorial chapters [6](../tutorial/06-checkout-approval-workflow.md) and [7](../tutorial/07-return-due-reminder-job.md)
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: Recipes
3
+ audience: tenant
4
+ ---
5
+
6
+ # Recipes
7
+
8
+ Short, copy-paste solutions to shapes that come up again and again. Each recipe
9
+ is 2–3 sentences of "when you need this" plus the real file(s). They're drawn
10
+ from the curated `erp examples patterns` catalog — run that any time to see the
11
+ live list, each entry pointing at a real shipped file:
12
+
13
+ ```bash
14
+ erp examples patterns # all
15
+ erp examples patterns --kind jobs # just the scheduled-job shapes
16
+ ```
17
+
18
+ ## Screens
19
+
20
+ - [A searchable related-record lookup field](./related-record-lookup-field.md)
21
+ - [A resolved reference column in a grid](./resolved-reference-grid-column.md)
22
+ - [A KPI dashboard page](./kpi-dashboard-page.md)
23
+ - [A master-detail page with a child grid](./master-detail-page.md)
24
+
25
+ ## Logic
26
+
27
+ - [An entity with an approval workflow](./entity-with-approval-workflow.md)
28
+
29
+ ## Scheduled jobs (no Java)
30
+
31
+ - [An N-days-before reminder job](./n-days-before-reminder.md)
32
+ - [A cross-row aggregation into a summary entity](./cross-row-aggregation.md)
33
+ - [A scheduled record-generation cadence](./scheduled-record-generation.md)
34
+ - [A condition-based compliance flag](./condition-based-compliance-flag.md)
35
+ - [A cross-plugin offboarding fan-out](./cross-plugin-offboarding-fanout.md)
36
+ - [A per-row document artifact generator](./per-row-document-generator.md)
37
+
38
+ ## Install
39
+
40
+ - [Install-lifecycle data seeding](./install-lifecycle-data-seeding.md)
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Install-lifecycle data seeding
3
+ audience: tenant
4
+ ---
5
+
6
+ # Install-lifecycle data seeding
7
+
8
+ ## When you need this
9
+
10
+ Any time a data row must **travel with the `.spk`** instead of a manual
11
+ per-tenant `POST` — reference data you own, default settings, and (the important
12
+ case) rows in the **shared platform config entities** that make your scheduled
13
+ jobs exist.
14
+
15
+ ## The file
16
+
17
+ `metadata/seed-data/<name>.json`:
18
+
19
+ ```json
20
+ {
21
+ "entity": "oeq_category",
22
+ "keyFields": ["category_code"],
23
+ "source": "office-equipment",
24
+ "rows": [
25
+ { "category_code": "LAPTOP", "category_name": "Laptops", "status": "ACTIVE", "seeded_by": "office-equipment" }
26
+ ]
27
+ }
28
+ ```
29
+
30
+ - **`entity`** — must already exist (ship it in `metadata/entities/` if you own
31
+ it).
32
+ - **`keyFields`** — the natural key. On re-install a matching row is updated only
33
+ if a non-key value changed, else inserted. **Never duplicated.**
34
+ - **`source`** — your plugin id; stamped into a `seeded_by` field if present.
35
+ - **`rows`** — plain field maps, no tenant id.
36
+
37
+ ## Verified idempotency
38
+
39
+ First install of the tutorial module:
40
+
41
+ ```
42
+ PluginDataSeedInstaller: office-equipment-categories.json -> entity oeq_category (tenant 2): 4 created, 0 updated, 0 unchanged
43
+ ```
44
+
45
+ Re-publish with one row added:
46
+
47
+ ```
48
+ PluginDataSeedInstaller: office-equipment-categories.json -> entity oeq_category (tenant 2): 1 created, 0 updated, 4 unchanged
49
+ ```
50
+
51
+ ## For scheduled-job config
52
+
53
+ When the target is a shared config entity
54
+ (`entity_status_date_sweep_config`, `entity_aggregation_config`,
55
+ `entity_cadence_config`, `entity_compliance_config`, `entity_cross_plugin_action_config`,
56
+ `entity_document_generator_config`) also ship:
57
+
58
+ 1. **the shared config entity file** — with just the additive columns your rows
59
+ need. `PluginEntityInstaller` is idempotent-additive: an existing table gains
60
+ only the missing columns.
61
+ 2. **an `AFTER_CREATE` register rule** on that config entity calling
62
+ `ensure<Job>Registered`, so the first seeded row auto-registers the job. (The
63
+ status-date-sweep job's register rule ships with the platform; the other five
64
+ you ship.)
65
+
66
+ ## The rules that bite
67
+
68
+ - Non-unique `keyFields` → duplicates on re-install. Pick a real key.
69
+ - A `null` in a key field in some rows → matches nothing / everything. Keep key
70
+ fields populated in every row.
71
+ - Seed targets an entity you forgot to ship → `SEED-001` lint / install failure.
72
+
73
+ ## Related
74
+
75
+ - [Seed config and reference data on install](../guides/seed-data-on-install.md)
76
+ - Each job guide's "register rule" section