Appearance
1. Create the plugin
Scaffold
bash
erp plugin create example-plugin --name "Office Equipment" --type business-appThe folder is
example-plugin/to match this docs tree. The plugin id we use isoffice-equipment— set below. (In real life pick one name and use it for both.)
You get example-plugin/spk-assembly/ with empty metadata/* folders and a plugin.json with "mainClass": null.
Edit the manifest
spk-assembly/plugin.json — the finished version (real file):
json
{
"id": "office-equipment",
"name": "Office Equipment",
"version": "1.0.3",
"type": "business-application",
"schemaName": "erp_core",
"vendor": "ACME Corp",
"license": "Proprietary",
"licenseTier": "free",
"category": "custom",
"erpVersion": "*",
"minErpVersion": "1.0",
"maxErpVersion": "99.0",
"mainClass": null,
"dependencies": [],
"optionalDependencies": [],
"owner": "business",
"editable": false,
"extendable": true,
"capabilitiesProvided": [],
"capabilitiesRequired": [],
"featureFlags": [],
"configSchemaJson": null,
"roles": [
{
"name": "Office Equipment Manager",
"code": "OFFICE_EQUIPMENT_MANAGER",
"description": "Full control of the Office Equipment module.",
"permissionKeys": [
"OeqCategory.View", "OeqCategory.Create", "OeqCategory.Edit",
"OeqEquipment.View", "OeqEquipment.Create", "OeqEquipment.Edit",
"OeqCheckout.View", "OeqCheckout.Create", "OeqCheckout.Submit", "OeqCheckout.Approve",
"Dashboard.View"
]
}
],
"runtimeModes": ["embedded"],
"serviceDeployment": null
}Key choices:
id: "office-equipment"— used in every route, page id, and i18n key from here on. Chosen once.role.nameandrole.codeare prefixed (Office Equipment Manager/OFFICE_EQUIPMENT_MANAGER). A plain "Equipment Manager" would collide with another installed plugin's role and fail the install — this actually happened while writing the tutorial.schemaName: "erp_core"— the scaffold default. See the note in Add an entity about when a dedicated schema is worth it; for this moduleerp_coreis fine end to end, including the reminder job in chapter 7.
No Java class
This tutorial module ships zero Java. plugin.json keeps "mainClass": null exactly as the scaffold wrote it, and there is no spk-assembly/lib/. On install the platform loads the plugin through a built-in generic no-op PF4J entry point — start/stop/enable/disable/upgrade, migrations, and data seeding all run normally. Everything the module does is JSON under spk-assembly/metadata/.
You would only add a Java class (a Plugin subclass, compiled against pf4j + slf4j-api, jar in spk-assembly/lib/, mainClass set to its FQN) for behaviour the Entity / Query / Rule / Workflow / Job engines genuinely can't express — this module never needs one.
The application and module (standalone plugin)
A standalone plugin (one that isn't part of a bigger suite like HCM) ships its own application and module so its pages are routable.
spk-assembly/metadata/application/office-equipment-app.json (real file):
json
{
"name": "office-equipment-app",
"description": "The Office Equipment plugin's own Application.",
"definition": {
"id": "office-equipment",
"titleKey": "office-equipment.application.title",
"urlSlug": "office-equipment",
"homeRoute": "/app/office-equipment/equipment/office-equipment/equipment-register",
"tier": "solution",
"icon": "devices",
"color": "#2563EB"
}
}spk-assembly/metadata/module/office-equipment-module.json (real file):
json
{
"name": "office-equipment",
"description": "The single Module the Office Equipment pages attach to.",
"definition": {
"id": "office-equipment-module",
"titleKey": "office-equipment.module.title",
"urlSlug": "equipment",
"tier": "solution",
"icon": "inventory_2",
"color": "#2563EB"
},
"applications": ["office-equipment-app"]
}The reachable URL for a page is /app/<app urlSlug>/<module urlSlug>/<pluginId>/<page name> — so /app/office-equipment/equipment/office-equipment/equipment-register.
Verify
bash
erp plugin test example-plugin/spk-assemblyPlugin Tests
────────────────────────────────────────
✓ plugin.json:valid-json
1 passed, 0 failed, 0 semantic warning(s), 3 skippedA healthy empty plugin. Next: give it a table.