Skip to content

Create a plugin from scratch

What you're doing

A plugin is a folder of JSON metadata that the ERP's generic engines execute — no Java required. You declare what you want — a table here, a screen there, an approval there — and the platform does the rest. This guide gets you an empty, valid, buildable plugin.

The complete sequence

bash
erp plugin create office-equipment --name "Office Equipment" --type business-app
erp menu create office-equipment office-equipment-menu \
  --display-name "Equipment" --route /office-equipment/equipment-register

That produces:

office-equipment/
├── README.md
└── spk-assembly/
    ├── plugin.json                 <- the manifest
    └── metadata/
        ├── entities/               <- one *.json per table
        ├── page/                   <- one *.json per screen
        ├── provider/               <- REST data providers for grids
        ├── data_service/           <- parameterized read queries
        ├── data_view/              <- SQL joins over physical tables
        ├── rules/                  <- lifecycle rules
        ├── workflow/               <- approval processes
        ├── seed-data/              <- rows to upsert on install
        ├── menu/                   <- navigation
        └── i18n/                   <- en.json etc.

spk-assembly/ is the build root. The .spk file erp plugin build produces is just a zip of this tree.

The manifest (spk-assembly/plugin.json)

Here is a complete, real manifest — the one the tutorial module ships:

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": [
        "OeqEquipment.View", "OeqEquipment.Create", "OeqEquipment.Edit",
        "OeqCheckout.View", "OeqCheckout.Create", "OeqCheckout.Submit", "OeqCheckout.Approve"
      ]
    }
  ],
  "runtimeModes": ["embedded"],
  "serviceDeployment": null
}

Line by line

  • id — globally unique, lowercase kebab-case. It's baked into every route, i18n key, and page id. Choose it once. Renaming later means rewriting every file. (It is also the PF4J plugin id, and the platform cannot load two plugins with the same id even across versions — see Publish and upgrade.)
  • version — semver. Bump it before every publish or the publish is a silent no-op (see known gotchas).
  • typebusiness-application for a normal feature plugin.
  • schemaName — the Postgres schema your entity tables route to. The scaffold default is erp_core. See the caveat below.
  • mainClass — leave it null (as the scaffold writes it) for a pure-JSON plugin. Only set it if you ship your own Java extensions. See "Java: only if you need it" below.
  • roles — the roles this plugin creates on install. code is stable and referenced by workflows; name must be unique across all installed plugins.
  • dependencies — plugin ids that must already be installed. Leave [] for a standalone plugin.
  • runtimeModes: ["embedded"] — the plugin runs inside the ERP process. The alternative, "service", is for plugins deployed as their own container and is out of scope here.

Java: only if you need it

A pure-JSON plugin ships no Java at all. Leave "mainClass": null and omit spk-assembly/lib/. On install the platform loads it through a built-in generic no-op PF4J entry point; start/stop/enable/disable/upgrade, migrations and data seeding all run exactly as for a plugin with its own mainClass. This is the default and the recommended shape.

You only add Java when a feature genuinely needs behaviour the Entity / Query / Rule / Workflow / Job engines can't express. Then write a Plugin subclass, compile it against pf4j + slf4j-api, drop the jar in spk-assembly/lib/, and set mainClass to its fully-qualified name — erp plugin build zips whatever is in lib/ into the .spk.

The schemaName caveat

Entity tables get created in the schema named by plugin.json's schemaName. The scaffold default erp_core works for CRUD and for the pages in this guide, but some generic write paths (notably the status-date-sweep job) assume a tenant-scoped table and can fail against a table that isn't laid out the way an app-schema table is. If your plugin owns entities and uses scheduled jobs, prefer a dedicated schema name and confirm your entities behave against it with erp plugin test and a live sweep before you rely on it. The tutorial uses erp_core and documents where this bites.

How to verify it worked

bash
erp plugin validate office-equipment/spk-assembly
erp plugin test office-equipment/spk-assembly

A fresh scaffold with no pages yet prints:

no page JSON found under .../metadata/page — nothing to validate
Plugin Tests
────────────────────────────────────────
✓ plugin.json:valid-json
1 passed, 0 failed, 0 semantic warning(s), 3 skipped

That's a healthy empty plugin. Now add an entity.

Common mistakes

SymptomCauseFix
erp menu create route becomes C:/Program Files/Git/...Git Bash rewrites the leading /run it from PowerShell, or MSYS_NO_PATHCONV=1
you renamed id after building pagesid is in every page/i18n/menu keypick the id once; if you must rename, sed the whole tree
publish rejected: role name already existsanother installed plugin has a role with the same namemake your role name distinctive