Skip to content

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...UseWhy
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 SequencesIt'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 changingA workflow or business rule attached via metadata to the existing entitySee 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 shared hcm database schema, not a per-plugin schema. Get this from the shipped module's own real, already-installed data_view files (as done here, copied from hcm-employee's own department-headcount-distribution-view.json) — never guess it, and never trust a plugin's plugin.json schemaName field, which can be stale (see
  • source.table: "employee" — the real table name. Reverse-engineer real table/column names the same way: read an existing, shipped data_view JSON from the module you're extending. Every shipped HCM module's metadata/data_view/ directory is real, readable reference material for exactly this purpose.
  • This view is read-only — a dataView/dataService pair can only search/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 to hcm-employee's.

How to verify it worked ​

  1. erp plugin build && erp plugin publish --env dev
  2. curl -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.
  3. Confirm hcm-employee's own files are untouched: git status inside backend/modules/hcm-employee should 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_view file in the module you're reading from.
  • Building a bespoke REST controller instead of a Data Service. See reading rows, a dataView/dataService pair is almost always the right tool, not a hand-written @RestController.
  • Forgetting TenantContext. If any part of your companion plugin adds a NO_AUTH endpoint that touches this data outside the normal tenant-request path, it must wrap the call in TenantContext.set()/finally clear() — see