# 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`](../plugin-api/overview.md) first. For the architectural reasoning behind the constraints, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../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`):
```kotlin
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.
```kotlin
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.
```yaml
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:
- `id` is globally unique. Use a reverse-DNS style. The host uses it as the plug-in's schema namespace (`plugin_helloprint__*`) and as the `source` tag (`plugin:com.example.helloprint`) on every metadata row the plug-in seeds.
- `version` is your plug-in's version, not the host's.
- `requiresApi: "1.x"` means "any 1.x release of `api.v1`". A mismatch fails at install time, not at runtime. Across a major version, the host loads `api.v1` and `api.v2` side by side for at least one major release window.
- `permissions` are auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace.
- `metadata` lists 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.`.
```kotlin
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("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.
```kotlin
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 registers `hello_print.greet.read` with 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](../i18n/guide.md).
- The handler returns through `ResponseBuilder`, not through Spring's `ResponseEntity`. 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:
```bash
./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:
```bash
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___*`, 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`](../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 the `platform-plugins` logger 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-persistence` logger, scoped to your plug-in id.
- **Runtime errors inside your plug-in:** logged under the plug-in's id (`com.example.helloprint`). Use the `PluginContext.log` handed to you in `start`; do not pull in your own logging framework.
- **Health endpoint:** `/actuator/health` reports 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.yml` for 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 `TaskHandler` implementations.
- 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`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md).