Appearance
Extend a shipped application
This page answers a question the other guides don't: "HCM (or CRM, or any other shipped application) already has the screen I want — how do I add to it without forking the product?" It uses HCM as the worked example because it's the largest shipped application, but the pattern is identical for every other one.
What you're doing
There are exactly three ways to extend a shipped application. Picking the wrong one is the most common mistake — start here.
| You want to... | Use | Why |
|---|---|---|
| Add a field to an existing screen (e.g. a new column on the Employee form) | Custom Fields (no code) — see Document Settings, Custom Fields, Custom Forms & Numbering Sequences | It's a tenant admin setting, not a development task. No plugin needed. |
| Add a new screen, report, KPI, or workflow that reads existing data (e.g. a dashboard of employees whose certifications expire soon) | A companion plugin that reads the shipped module's entities through a Data Service / Data View (read-only) | This is what the rest of this page walks through. |
| Add a new approval step or business rule triggered by an existing entity changing | A workflow or business rule attached via metadata to the existing entity | See Add an approval workflow and Add business rules and expressions — no Java, and no edit to the shipped plugin. |
What you must never do: edit hcm-employee's (or any shipped plugin's) own Java source or its metadata files. Those are the platform's, upgraded independently of your tenant, and per justifies touching another module's code — a feature request never does. Every legitimate extension in the table above is additive: a new plugin, a new metadata file, a new rule. Nothing you write ever modifies a file that ships with hcm-employee, hcm-leave, or any other application module.
The complete example
A companion plugin, hcm-cert-tracker, that adds one new read-only page to HCM: Employees by Department — a KPI built entirely from data that already lives in the shipped hcm-employee module's own database table, without touching that module at all.
spk-assembly/metadata/data_view/employees-by-department-view.json:
json
{
"name": "hcm-cert-tracker-employees-by-department-view",
"description": "Read-only view over hcm-employee's own `employee` table — active headcount grouped by department. This plugin never writes to this table.",
"definition": {
"source": { "table": "employee", "alias": "e", "excludeDeleted": false, "schema": "hcm" },
"joins": [
{ "table": "org_unit", "alias": "dept", "type": "INNER", "on": [{ "leftRef": "e.department_id", "rightRef": "dept.id" }], "excludeDeleted": false, "schema": "hcm" }
],
"fields": [{ "ref": "dept.name", "outputName": "label" }],
"calculatedFields": [],
"filter": "{\"and\":[{\"field\":\"e.employment_status\",\"operator\":\"eq\",\"value\":\"active\"}]}",
"groupBy": ["dept.name"],
"aggregations": [{ "ref": "e.id", "fn": "COUNT", "outputName": "value" }],
"sort": [{ "ref": "dept.name", "descending": false }],
"pagination": { "defaultPageSize": 50, "maxPageSize": 100 },
"permissionKey": null
},
"metadata": {},
"modules": ["hcm-cert-tracker-dashboard"]
}spk-assembly/metadata/data_service/employees-by-department.json:
json
{
"name": "hcm-cert-tracker-employees-by-department",
"operation": "search",
"source": { "kind": "dataView", "ref": "hcm-cert-tracker-employees-by-department-view" }
}The page then binds a core.donut-chart (or core.list) block to POST /api/v1/data-services/hcm-cert-tracker-employees-by-department/execute — the exact same callApi → setValue → binding pattern in Wire a page's data.
Line by line
source.schema: "hcm"— every HCM module's tables live in the sharedhcmdatabase schema, not a per-plugin schema. Get this from the shipped module's own real, already-installeddata_viewfiles (as done here, copied fromhcm-employee's owndepartment-headcount-distribution-view.json) — never guess it, and never trust a plugin'splugin.jsonschemaNamefield, which can be stale (seesource.table: "employee"— the real table name. Reverse-engineer real table/column names the same way: read an existing, shippeddata_viewJSON from the module you're extending. Every shipped HCM module'smetadata/data_view/directory is real, readable reference material for exactly this purpose.- This view is read-only — a
dataView/dataServicepair can onlysearch/get/count; there is no write path through this mechanism. Writing to another module's table is not supported and not safe — if you need to change HCM data, do it through HCM's own real forms/APIs, not by reaching into its schema. modules: ["hcm-cert-tracker-dashboard"]— scopes this data service to your own plugin's page, not tohcm-employee's.
How to verify it worked
erp plugin build && erp plugin publish --env devcurl -X POST $BASE/api/v1/data-services/hcm-cert-tracker-employees-by-department/execute -H "Authorization: Bearer $TOKEN"and confirm it returns real{label, value}rows matching your tenant's actual employee/department data — spot-check one row against the HCM Employee Directory screen itself.- Confirm
hcm-employee's own files are untouched:git statusinsidebackend/modules/hcm-employeeshould show nothing.
Common mistakes
- Editing the shipped module instead of reading it. If you find yourself opening
hcm-employee's source to add a field or endpoint, stop — that's Custom Fields or a companion plugin, not a source edit. - Guessing the schema name. Always confirm it from a real, already-shipped
data_viewfile in the module you're reading from. - Building a bespoke REST controller instead of a Data Service. See reading rows, a
dataView/dataServicepair is almost always the right tool, not a hand-written@RestController. - Forgetting
TenantContext. If any part of your companion plugin adds aNO_AUTHendpoint that touches this data outside the normal tenant-request path, it must wrap the call inTenantContext.set()/finally clear()— see
What to read next
- Add a data provider, data view, or data service — the full reference for what this guide's worked example used.
- Build a page and Wire a page's data — to add the screen this data feeds.
- Add a KPI or aggregation — for the KPI-card version of this same pattern.
- Add an approval workflow — for the "add a workflow step to an existing entity" extension path.
- Add your own app-owned roles & permissions — the correct pattern if your companion plugin needs its own admin role, instead of widening a shipped module's access checks.
