Skip to main content
Plugins are self-contained modules that extend Halo’s functionality. They can add new features, customize behavior, integrate external services, and register custom extensions - all without modifying the core system.

What are plugins?

A plugin is a JAR file that contains:
  • Custom extensions and controllers
  • API endpoints and services
  • Configuration and settings
  • Static assets (JavaScript, CSS)
  • Event listeners and hooks
Halo uses PF4J as the foundation for its plugin system, providing hot-reloading and isolation.

Plugin structure

Every plugin follows a standard structure:
my-plugin/
├── src/main/
│   ├── java/
│   │   └── com/example/myplugin/
│   │       ├── MyPlugin.java          # Main plugin class
│   │       ├── controllers/           # API endpoints
│   │       ├── reconcilers/           # Extension controllers
│   │       └── extensions/            # Custom extensions
│   └── resources/
│       ├── plugin.yaml                # Plugin metadata
│       ├── extensions/                # Extension definitions
│       │   ├── settings.yaml
│       │   └── my-extension.yaml
│       └── console/                   # UI assets
│           ├── main.js
│           └── style.css
└── gradle.properties                  # Build configuration

Plugin metadata

Every plugin has a plugin.yaml manifest that declares its metadata:
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
  name: my-awesome-plugin
spec:
  version: 1.0.0
  requires: ">=2.0.0"
  author:
    name: John Doe
    website: https://example.com
  displayName: "My Awesome Plugin"
  description: "Add amazing features to your Halo site"
  homepage: https://github.com/example/my-plugin
  license:
    - name: MIT
  settingName: my-plugin-settings
  configMapName: my-plugin-config
The requires field uses semantic versioning to specify compatible Halo versions.

Plugin lifecycle

Plugins move through several states during their lifecycle:

Lifecycle phases

PENDING: Plugin uploaded but not yet processed. STARTING: Plugin is being initialized and dependencies are being resolved. STARTED: Plugin is running and fully functional. STOPPING: Plugin is being shut down gracefully. STOPPED: Plugin is disabled but still installed. FAILED: Plugin encountered an error during startup or operation.

Creating a plugin

Plugins extend the BasePlugin class:
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
import org.springframework.stereotype.Component;

public class MyPlugin extends BasePlugin {
    
    public MyPlugin(PluginContext context) {
        super(context);
    }
    
    @Override
    public void start() {
        System.out.println("Plugin started: " + getContext().getName());
    }
    
    @Override
    public void stop() {
        System.out.println("Plugin stopped: " + getContext().getName());
    }
}

Plugin context

The PluginContext provides access to plugin information:
PluginContext context = getContext();

// Get plugin metadata
String name = context.getName();
String version = context.getVersion();
RuntimeMode mode = context.getRuntimeMode();

// Access configuration
String configMapName = context.getConfigMapName();

Spring integration

Plugins have their own Spring application context, enabling dependency injection:
import org.springframework.stereotype.Component;

@Component
public class MyService {
    
    private final ReactiveExtensionClient client;
    
    public MyService(ReactiveExtensionClient client) {
        this.client = client;
    }
    
    public Mono<Post> getPost(String name) {
        return client.fetch(Post.class, name);
    }
}
Plugins can inject Halo’s core services like ExtensionClient, SchemeManager, and more.

Registering extensions

Plugins can define custom extensions in YAML files:
# src/main/resources/extensions/bookmark.yaml
apiVersion: example.com/v1alpha1
kind: Bookmark
metadata:
  name: bookmark-crd
spec:
  names:
    kind: Bookmark
    plural: bookmarks
    singular: bookmark
  schema:
    type: object
    properties:
      spec:
        type: object
        properties:
          title:
            type: string
          url:
            type: string
          description:
            type: string
Then create the Java class:
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;

@GVK(
    group = "example.com",
    version = "v1alpha1",
    kind = "Bookmark",
    plural = "bookmarks",
    singular = "bookmark"
)
public class Bookmark extends AbstractExtension {
    private BookmarkSpec spec;
    
    @Data
    public static class BookmarkSpec {
        private String title;
        private String url;
        private String description;
    }
}

Adding API endpoints

Plugins can expose custom REST APIs:
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/apis/example.com/v1alpha1/bookmarks")
public class BookmarkController {
    
    private final ReactiveExtensionClient client;
    
    public BookmarkController(ReactiveExtensionClient client) {
        this.client = client;
    }
    
    @GetMapping("/{name}")
    public Mono<Bookmark> getBookmark(@PathVariable String name) {
        return client.fetch(Bookmark.class, name);
    }
    
    @PostMapping
    public Mono<Bookmark> createBookmark(@RequestBody Bookmark bookmark) {
        return client.create(bookmark);
    }
}
Use the /apis/<group>/<version> pattern for consistency with Halo’s core APIs.

Implementing reconcilers

Reconcilers watch extensions and maintain their desired state:
import run.halo.app.extension.controller.*;
import reactor.core.publisher.Mono;

@Component
public class BookmarkReconciler implements Reconciler<Request> {
    
    private final ReactiveExtensionClient client;
    
    public BookmarkReconciler(ReactiveExtensionClient client) {
        this.client = client;
    }
    
    @Override
    public Result reconcile(Request request) {
        return client.fetch(Bookmark.class, request.name())
            .flatMap(bookmark -> {
                // Validate bookmark
                if (!isValidUrl(bookmark.getSpec().getUrl())) {
                    bookmark.setStatus(new Status("Invalid URL"));
                    return client.update(bookmark);
                }
                return Mono.just(bookmark);
            })
            .map(bookmark -> Result.doNotRetry())
            .defaultIfEmpty(Result.doNotRetry())
            .block();
    }
    
    @Override
    public Controller setupWith(ControllerBuilder builder) {
        return builder
            .extension(new Bookmark())
            .build();
    }
}

Plugin settings

Plugins can define settings for user configuration:
# src/main/resources/extensions/settings.yaml
apiVersion: v1alpha1
kind: Setting
metadata:
  name: my-plugin-settings
spec:
  forms:
    - group: basic
      label: Basic Settings
      formSchema:
        - $formkit: text
          name: apiKey
          label: API Key
          validation: required
        - $formkit: number
          name: maxItems
          label: Maximum Items
          value: 10
Access settings in your plugin:
import run.halo.app.plugin.SettingFetcher;

@Component
public class MyService {
    
    private final SettingFetcher settingFetcher;
    
    public MyService(SettingFetcher settingFetcher) {
        this.settingFetcher = settingFetcher;
    }
    
    public String getApiKey() {
        return settingFetcher.fetch("basic", "apiKey")
            .orElse("");
    }
}

Handling events

Plugins can listen to system events:
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import run.halo.app.plugin.event.PluginStartedEvent;

@Component
public class EventHandler {
    
    @EventListener(PluginStartedEvent.class)
    public void onPluginStarted(PluginStartedEvent event) {
        System.out.println("Plugin started: " + event.getPluginName());
    }
}

Console integration

Plugins can extend the admin console UI:
// src/main/resources/console/main.js
import { definePlugin } from '@halo-dev/console-shared';

export default definePlugin({
  name: 'MyPlugin',
  components: {},
  routes: [
    {
      path: '/my-plugin',
      name: 'MyPluginHome',
      component: () => import('./views/Home.vue'),
      meta: {
        title: 'My Plugin',
        menu: {
          name: 'My Plugin',
          icon: 'IconPlug',
        },
      },
    },
  ],
});

Plugin dependencies

Plugins can depend on other plugins:
spec:
  pluginDependencies:
    "plugin.halo.run/another-plugin": ">=1.0.0"
Halo resolves dependencies automatically and ensures plugins load in the correct order.

Best practices

Follow these guidelines for plugin development: Use semantic versioning: Follow semver for version numbers. Handle errors gracefully: Don’t crash the entire system on plugin errors. Clean up resources: Release resources in the stop() method. Namespace your APIs: Use unique groups for your extensions and endpoints. Document your plugin: Provide clear documentation for users and developers. Test thoroughly: Test loading, unloading, and upgrading scenarios.

Next steps

Plugin basics

Start building your first plugin

Plugin structure

Learn about plugin project structure

API extension

Add custom API endpoints

Configuration

Configure plugin settings

Build docs developers (and LLMs) love