Plug-in author: getting started
This walkthrough is for a developer building their first vibe_erp plug-in. By the end you will have a JAR that drops into ./plugins/, registers an extension, exposes a REST endpoint, and shows up in the plug-in loader log on the next restart.
For the conceptual overview of the plug-in API, read ../plugin-api/overview.md first. For the architectural reasoning behind the constraints, see ../architecture/overview.md and the full spec at ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.
1. The only dependency you need
Add org.vibeerp:api-v1 from Maven Central. This is the only vibe_erp dependency a plug-in is allowed to declare. Importing anything from org.vibeerp.platform.* or any PBC's internal package will fail the plug-in linter at install time.
In Gradle (build.gradle.kts):
plugins {
kotlin("jvm") version "2.0.0"
}
repositories {
mavenCentral()
}
dependencies {
compileOnly("org.vibeerp:api-v1:1.0.0")
// Test against the same artifact at runtime
testImplementation("org.vibeerp:api-v1:1.0.0")
}
compileOnly is correct: at runtime the plug-in classloader is given api.v1 from the host. Bundling it inside your plug-in JAR causes classloader confusion.
2. Implement org.vibeerp.api.v1.plugin.Plugin
The plug-in's entry class is the single point where the host hands you a context and asks you to wire yourself up.
package com.example.helloprint
import org.vibeerp.api.v1.plugin.Plugin
import org.vibeerp.api.v1.plugin.PluginContext
class HelloPrintPlugin : Plugin {
override fun start(context: PluginContext) {
context.log.info("hello-print plug-in starting")
// Register listeners, seed metadata, etc.
}
override fun stop(context: PluginContext) {
context.log.info("hello-print plug-in stopping")
}
}
The host calls start after the plug-in's classloader, Spring child context, and Liquibase migrations are ready. Anything the plug-in needs from the host (event bus, translator, repositories, configuration) comes through PluginContext.
3. Write a plugin.yml manifest
The manifest sits at the root of the JAR (src/main/resources/plugin.yml). It tells the host who you are, what API version you target, and what permissions you need.
id: com.example.helloprint
version: 0.1.0
requiresApi: "1.x"
name: Hello Print
entryClass: com.example.helloprint.HelloPrintPlugin
description: >
A minimal worked example for the vibe_erp plug-in author guide.
Registers one extension and exposes one REST endpoint.
vendor:
name: Example Print Co.
url: https://example.com
permissions:
- hello_print.greet.read
metadata:
forms:
- metadata/forms/greeting.json
i18n:
- i18n/en-US.properties
- i18n/de-DE.properties
Field notes:
-
idis globally unique. Use a reverse-DNS style. The host uses it as the plug-in's schema namespace (plugin_helloprint__*) and as thesourcetag (plugin:com.example.helloprint) on every metadata row the plug-in seeds. -
versionis your plug-in's version, not the host's. -
requiresApi: "1.x"means "any 1.x release ofapi.v1". A mismatch fails at install time, not at runtime. Across a major version, the host loadsapi.v1andapi.v2side by side for at least one major release window. -
permissionsare auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace. -
metadatalists files inside the JAR that the host should pick up on plug-in start: form definitions, BPMN files, message bundles, seed rules.
4. Register an extension via @Extension
Extensions are how a plug-in plugs into a typed seam declared by the core or another PBC. Every seam lives in org.vibeerp.api.v1.ext.<area>.
package com.example.helloprint
import org.vibeerp.api.v1.plugin.Extension
import org.vibeerp.api.v1.workflow.TaskHandler
import org.vibeerp.api.v1.workflow.WorkflowTask
@Extension(point = TaskHandler::class)
class GreetCustomerHandler : TaskHandler {
override val id: String = "hello_print.greet_customer"
override fun handle(task: WorkflowTask) {
val name = task.variable<String>("customerName") ?: "world"
task.setVariable("greeting", "hello, $name")
task.complete()
}
}
The host scans @Extension-annotated classes inside the plug-in JAR and registers them against the declared extension point. A BPMN service task referencing hello_print.greet_customer will now be routed to this handler.
5. Add a @PluginEndpoint controller
Plug-ins add REST endpoints through @PluginEndpoint. The endpoint runs inside the plug-in's Spring child context and is auto-listed in the OpenAPI document under the plug-in's namespace.
package com.example.helloprint
import org.vibeerp.api.v1.http.PluginEndpoint
import org.vibeerp.api.v1.http.RequestContext
import org.vibeerp.api.v1.http.ResponseBuilder
import org.vibeerp.api.v1.i18n.MessageKey
import org.vibeerp.api.v1.security.Permission
@PluginEndpoint(
path = "/api/plugin/hello-print/greet",
method = "GET",
permission = "hello_print.greet.read",
)
class GreetEndpoint {
fun handle(request: RequestContext): ResponseBuilder {
val name = request.query("name") ?: "world"
val greeting = request.translator.format(
MessageKey("hello_print.greet.message"),
mapOf("name" to name),
)
return ResponseBuilder.ok(mapOf("greeting" to greeting))
}
}
A few things this snippet is doing on purpose:
- The endpoint declares a
permission. The plug-in loader registershello_print.greet.readwith the role editor; an admin grants it before the endpoint becomes callable. - The greeting goes through the
Translator. There is no string concatenation in user-facing code. See the i18n guide. - The handler returns through
ResponseBuilder, not through Spring'sResponseEntity. Plug-ins do not see Spring directly.
6. Loading the plug-in
Build the JAR and drop it into the host's plug-in directory:
./gradlew :jar
cp build/libs/hello-print-0.1.0.jar /opt/vibe-erp/plugins/
# or, for a Docker deployment
docker cp build/libs/hello-print-0.1.0.jar vibe-erp:/opt/vibe-erp/plugins/
Restart the host:
docker restart vibe-erp
Hot reload of plug-ins without a restart is not in v1.0 — it is on the v1.2+ roadmap. For v1.0 and v1.1, install and uninstall require a restart.
On boot, the host scans ./plugins/, validates each manifest, runs the plug-in linter, creates a classloader and Spring child context per plug-in, runs the plug-in's Liquibase changesets in plugin_<id>__*, seeds metadata, and finally calls Plugin.start. The lifecycle in full is described in section 7 of ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.
7. Where to find logs and errors
-
Boot log:
/opt/vibe-erp/logs/vibe-erp.log. Filter on theplatform-pluginslogger to see exactly which plug-ins were scanned, accepted, rejected, and why. -
Linter rejections: if your JAR imports a class outside
org.vibeerp.api.v1.*, the linter rejects it at install time and logs the offending import. Fix the import; never work around the linter. -
Migration failures: Liquibase output is logged under the
platform-persistencelogger, scoped to your plug-in id. -
Runtime errors inside your plug-in: logged under the plug-in's id (
com.example.helloprint). Use thePluginContext.loghanded to you instart; do not pull in your own logging framework. -
Health endpoint:
/actuator/healthreports per-plug-in status. A plug-in stuck in a half-loaded state shows up here before it shows up in user-visible failures.
8. The reference printing-shop plug-in is your worked example
reference-customer/plugin-printing-shop/ is a real, production-shaped plug-in built and CI-tested on every PR. It expresses the workflows in raw/业务流程设计文档/ using only api.v1. When this guide leaves a question unanswered, the answer is almost always "look at how plugin-printing-shop does it":
- The shape of
plugin.ymlfor a non-trivial plug-in. - How a plug-in declares a brand-new entity (printing plates, presses, color proofs) without touching any PBC.
- How a multi-step BPMN workflow is shipped inside a JAR and wired up to typed
TaskHandlerimplementations. - How form definitions reference custom fields.
- How i18n bundles are organized for a plug-in that ships in five locales.
The plug-in is built by the main Gradle build but not loaded by default. Drop its JAR into ./plugins/ to load it locally.
What this guide does not yet cover
- Publishing a plug-in to a marketplace — the marketplace and signed plug-ins are a v2 deliverable.
- Hot reload without restart — v1.2+.
- Calling the MCP endpoint as an AI agent — v1.1.
For everything else, the source of truth is ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.