| name | browser4-plugin |
| title | Browser4 Plugin Development — AI Agent Skill |
| description | Guides the creation of Browser4 plugins from requirements gathering through deployment. Use when the user wants to create, build, scaffold, or extend Browser4 with a new plugin — whether for CAPTCHA solving, media processing, content conversion, page category detection, custom RPA actions, or new LLM agent tools. |
| tier | procedure |
Browser4 Plugin Development
Step-by-step guide to creating a Browser4 plugin — from scaffolding via the Maven archetype through implementing PluginMount interfaces, services, event handlers, tool executors, and tests.
Description
This skill covers the complete plugin development lifecycle: clarifying requirements, choosing the right PluginMount interfaces, scaffolding a project from the PDK archetype, implementing business logic (event handlers, services, LLM agent tools), wiring Spring auto-configuration, writing tests, building, and deploying. It includes decision trees to help identify which extension points to use and concrete code patterns drawn from the five built-in first-party plugins.
Dependencies
- JDK 17+
- Maven 3.9+
- Access to the Browser4 PDK parent POM (
ai.platon.pulsar:browser4-pdk) — published to Maven Central
- For building the archetype locally:
mvn -pl browser4-pdk install from this repository
Parameters
| Parameter | Type | Required | Default | Description |
|---|
groupId | String | Yes | — | Maven groupId for the new plugin project (e.g., com.example) |
artifactId | String | Yes | — | Maven artifactId, conventionally browser4-<feature-name> |
version | String | No | 1.0.0-SNAPSHOT | Plugin version |
pluginName | String | Yes | — | Human-readable plugin name (e.g., "My Feature Plugin") |
pluginDescription | String | No | "A Browser4 plugin that provides custom functionality" | One-line description |
mountPoints | String[] | No | ["BrowseEventMount"] | Which PluginMount interfaces to implement. Options: BrowseEventMount, LoadEventMount, CrawlEventMount, ToolMount, PageSnifferMount |
hasCustomTools | Boolean | No | false | Whether the plugin registers LLM agent tool executors |
hasLifecycle | Boolean | No | false | Whether the plugin implements the Browser4Plugin lifecycle interface |
features | String[] | No | — | List of concrete capabilities to implement (e.g., "detect media on page", "download files", "expose LLM tool") |
Return Value
A new Maven project directory at the path specified by artifactId. The project builds to a thin JAR (target/<artifactId>-<version>.jar) deployable to Browser4's plugins/ directory. The plugin is auto-discovered on restart and logs:
PluginManager: Found X PluginMount bean(s)
PluginManager: ✓ Configured browse event handlers
PluginManager: Found X Browser4Plugin bean(s)
- <plugin-name> v<version>
When to Create a Plugin
Create a plugin when you need to:
- Hook into the browse lifecycle — execute custom logic on page-navigation events (before navigation, after DOM is steady, before tab close). Examples: CAPTCHA detection and solving, ad blocking, custom RPA workflows.
- Hook into the load lifecycle — intercept or transform page content during loading. Examples: URL normalization and stripping tracking parameters, content extraction, HTML post-processing.
- Hook into the crawl lifecycle — accept or reject URLs during crawling. Examples: domain allowlists, duplicate URL filtering, paywall detection and skip.
- Register custom tools for LLM agents — expose new capabilities as callable functions. Examples: image download, PPTX generation, database queries, API integrations.
- Add page category sniffers — teach Browser4 to recognize new page types so it can adapt its behavior. Examples: CAPTCHA pages, login pages, paywalls, shopping carts.
Do NOT create a plugin for:
- Simple data extraction from a known site — use the browser4-cli HTML snapshot / X-SQL instead.
- One-off browser automation — the CLI or a quick script is faster.
- Modifying core Browser4 behavior — that belongs in the main source tree, not a plugin.
Usage Examples
Example 1: Scaffold a New Plugin (Quick Start)
mvn archetype:generate \
-DarchetypeGroupId=ai.platon.pulsar \
-DarchetypeArtifactId=browser4-plugin-archetype \
-DarchetypeVersion=4.12.0 \
-DgroupId=com.example \
-DartifactId=browser4-myfeature \
-Dversion=1.0.0-SNAPSHOT \
-DpluginName="My Feature" \
-DpluginDescription="A Browser4 plugin that performs custom page processing"
Example 2: Build and Deploy
cd browser4-myfeature
mvn package -DskipTests
cp target/browser4-myfeature-1.0.0-SNAPSHOT.jar /path/to/browser4/plugins/
Example 3: Install via REST API
curl -X POST http://localhost:8182/api/plugins/install \
-F "file=@target/browser4-myfeature-1.0.0-SNAPSHOT.jar"
curl http://localhost:8182/api/plugins
Example 4: Complete Auto-Configuration with BrowseEventMount + Config + Service
@AutoConfiguration
@ConditionalOnProperty(name = ["myfeature.enabled"], havingValue = "true", matchIfMissing = true)
@Lazy
open class MyFeatureAutoConfiguration(
private val applicationContext: ApplicationContext,
) : BrowseEventMount {
override fun configureBrowseHandlers(handlers: BrowseEventHandlers) {
handlers.onDocumentSteady.addLast { page, driver ->
val service = applicationContext.getBean(MyFeatureService::class.java)
service.process(page, driver)
}
}
@Bean
@ConditionalOnMissingBean
open fun myFeatureConfig(conf: Config): MyFeatureConfig =
MyFeatureConfig.fromConfig(conf)
@Bean
@ConditionalOnMissingBean
open fun myFeatureService(config: MyFeatureConfig): MyFeatureService =
MyFeatureService(config)
}
Step-by-Step Workflow
Step 1: Clarify Requirements
Before writing any code, ask the user these questions. The answers determine which PluginMount interfaces and project structure to use.
| Question | Why it matters |
|---|
| What is the plugin's core purpose? | Determines which PluginMount interfaces to implement and what services are needed |
| Should the plugin act automatically on every page, or only when explicitly invoked? | Automatic action → BrowseEventMount on onDocumentSteady. Explicit invocation → ToolMount. Both → implement both. |
| Does the plugin need to transform or extract data during page loading? | Yes → LoadEventMount or CrawlEventMount. No → skip these. |
| Should LLM agents be able to invoke the plugin's features as tools? | Yes → ToolMount. No → skip. |
| Does the plugin recognize a new category of page (CAPTCHA, paywall, etc.)? | Yes → PageSnifferMount. No → skip. |
| What external services or APIs does the plugin use? | Determines additional dependencies (OkHttp client, AWS SDK, etc.) and configuration properties. |
| Should the plugin be configurable (enabled/disabled, timeouts, endpoints)? | Yes → create a Config data class and @ConditionalOnProperty annotations. |
| Does the plugin need explicit startup/shutdown lifecycle hooks? | Yes → implement Browser4Plugin. No → auto-configuration only is sufficient. |
Step 2: Choose PluginMount Interfaces
Map the plugin's capabilities to one or more mount points. The auto-configuration class implements all chosen interfaces.
| If the plugin needs to... | Implement | Primary hook |
|---|
| Execute code when a page finishes loading and the DOM is stable | BrowseEventMount | onDocumentSteady — the recommended RPA hook |
| Intercept or block resources before navigation | BrowseEventMount | onWillNavigate — call driver.addBlockedURLs() |
| Take screenshots or extract data before tab closes | BrowseEventMount | onWillStopTab — last chance before tab teardown |
| Normalize or modify URLs before fetching | LoadEventMount | onNormalize |
| Extract data right after HTML is parsed | LoadEventMount | onHTMLDocumentParsed |
| Filter which URLs enter the crawl pipeline | CrawlEventMount | onWillLoad — return null to reject |
| Expose new LLM-callable functions | ToolMount | getToolExecutors() — must return a List<ToolExecutor> |
| Detect a page category (e.g., "this is a CAPTCHA page") | PageSnifferMount | getPageSniffers() — must return a List<PageCategorySniffer> |
| Have startup/shutdown lifecycle hooks | Browser4Plugin | Override onStartup() / onShutdown() |
Common combinations from built-in plugins:
| Plugin | Mount points used | Pattern |
|---|
| CAPTCHA | BrowseEventMount + ToolMount + PageSnifferMount | Auto-detect + manual solve + page classification |
| Images | BrowseEventMount + ToolMount + Browser4Plugin | Auto-detect + agent tool + explicit lifecycle |
| Media | BrowseEventMount + ToolMount | Auto-detect + agent tool |
| PPTX | BrowseEventMount + ToolMount | Agent-invoked conversion |
| Markdown | BrowseEventMount + ToolMount | Agent-invoked conversion |
| PDK Test | BrowseEventMount + LoadEventMount + CrawlEventMount | All event phases (reference/canary) |
Step 3: Scaffold via the Archetype
Run the Maven archetype command from the Quick Start. The generated project contains:
browser4-<feature>/
├── pom.xml # Maven build (parent: browser4-pdk)
├── .gitignore
├── README.md
└── src/main/
├── kotlin/<package>/
│ ├── MyPlugin.kt # Optional: Browser4Plugin lifecycle
│ ├── config/
│ │ └── PluginAutoConfiguration.kt # Required: @AutoConfiguration + mounts
│ ├── integration/
│ │ ├── MyBrowseEventHandler.kt # Optional: browse event handler
│ │ └── MyLoadEventHandler.kt # Optional: load event handler
│ └── tools/
│ └── MyToolExecutor.kt # Optional: LLM agent tool
└── resources/
└── META-INF/
├── browser4-plugin.json # Required: plugin manifest
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports # Required
Immediately after scaffolding, do these renames:
- Rename the generated
PluginAutoConfiguration class to <Feature>AutoConfiguration (e.g., CaptchaAutoConfiguration)
- Rename
MyPlugin to <Feature>Plugin (if keeping the lifecycle class)
- Rename handler classes:
MyBrowseEventHandler → <Feature>BrowseEventHandler
- Update
browser4-plugin.json — change the name, description, and autoConfigurationClasses to match
- Update
AutoConfiguration.imports — replace the FQN with the renamed class
- Delete stub files you won't use (removing unused files is better than leaving dead code)
Step 4: Implement Mount Points
4a. BrowseEventMount — Custom RPA on Every Page
For plugins that execute code automatically when a page loads. The key hook is onDocumentSteady, which fires when the DOM is fully rendered and the page is stable.
@AutoConfiguration
@ConditionalOnProperty(name = ["myfeature.enabled"], havingValue = "true", matchIfMissing = true)
@Lazy
open class MyFeatureAutoConfiguration : BrowseEventMount {
override fun configureBrowseHandlers(handlers: BrowseEventHandlers) {
handlers.onDocumentSteady.addLast { page, driver ->
}
handlers.onWillNavigate.addLast { page, driver ->
driver.addBlockedURLs(listOf("*.png", "*.jpg", "*analytics*"))
}
}
}
Browse event hooks in execution order:
| Position | Hook | Best for |
|---|
| 1 | onWillLaunchBrowser | Pre-launch setup |
| 2 | onBrowserLaunched | First access to WebDriver |
| 3 | onWillFetch | Pre-fetch configuration |
| 4 | onWillNavigate | Block resources, set headers |
| 5 | onNavigated | Post-navigation checks |
| 6 | onWillInteract | Pre-interaction setup |
| 7 | onWillCheckDocumentState | Check readyState |
| 8 | onDocumentFullyLoaded | DOM ready |
| 9 | onWillScroll | Pre-scroll |
| 10 | onDidScroll | Post-scroll |
| 11 | onDocumentSteady | ★ Best for custom RPA |
| 12 | onWillComputeFeature | Pre-feature computation |
| 13 | onFeatureComputed | Features computed |
| 14 | onDidInteract | All interactions complete |
| 15 | onWillStopTab | Last chance before tab close |
| 16 | onTabStopped | Tab stopped |
| 17 | onFetched | Fetch complete |
4b. LoadEventMount — Content Interception During Loading
For plugins that normalize URLs, extract data during parsing, or transform content.
override fun configureLoadHandlers(handlers: LoadEventHandlers) {
handlers.onNormalize.addLast { url ->
url.replace(Regex("\\?utm_.*"), "")
}
handlers.onHTMLDocumentParsed.addLast { page, doc ->
}
}
Load event hooks: onNormalize → onWillLoad → onWillFetch → onFetched → onWillParse → onWillParseHTMLDocument → onHTMLDocumentParsed (★ best for data extraction) → onParsed → onLoaded
4c. CrawlEventMount — URL Pipeline Filtering
For plugins that accept/reject URLs in the crawl pipeline.
override fun configureCrawlHandlers(handlers: CrawlEventHandlers) {
handlers.onWillLoad.addLast { url ->
if (isBlacklisted(url.url)) null else url
}
}
Crawl hooks: onWillLoad (return null to reject URL) → onLoaded (results are available)
4d. ToolMount — LLM Agent Tools
For plugins whose features should be invocable by AI agents. Extend AbstractToolExecutor from browser4-agentic.
class MyFeatureAutoConfiguration : ToolMount {
override fun getToolExecutors(): List<ToolExecutor> =
listOf(applicationContext.getBean("myFeatureToolExecutor") as ToolExecutor)
}
open class MyFeatureToolExecutor(
private val service: MyFeatureService,
) : AbstractToolExecutor() {
override val domain = "myfeature"
init {
toolSpec["doSomething"] = ToolSpec(
domain = domain,
method = "doSomething",
arguments = listOf(
ToolSpec.Arg("param1", "String"),
ToolSpec.Arg("param2", "Int?", "0"),
),
returnType = "MyResult",
description = "Does something useful with the current page"
)
}
override suspend fun callFunctionOn(
domain: String,
functionName: String,
args: Map<String, Any?>,
receiver: Any,
): Any? {
val driver = receiver as? WebDriver
return when (functionName) {
-> service.execute(driver, args)
-> IllegalArgumentException()
}
}
}
4e. PageSnifferMount — Page Category Detection
For plugins that recognize new page types. Implement PageCategorySniffer and return it.
4f. Browser4Plugin — Lifecycle Hooks (Optional)
Implement only if you need explicit startup/shutdown callbacks:
open class MyFeaturePlugin(
override val manifest: PluginManifest = PluginManifest(
name = "browser4-myfeature",
version = "4.12.0-rc.1",
description = "My plugin description",
dependsOn = listOf("browser4-protocol", "browser4-agentic"),
autoConfigurationClasses = listOf(
"ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration"
)
)
) : Browser4Plugin {
override fun onStartup() { }
override fun onShutdown() { }
}
Step 5: Implement Services and Config
Separate business logic from mount-point wiring. The auto-configuration class wires dependencies; the service class contains business logic; the config data class holds tunable settings.
data class MyFeatureConfig(
val enabled: Boolean = true,
val timeoutSeconds: Long = 30,
val endpoint: String = "https://default.example.com",
) {
companion object {
private const val PREFIX = "myfeature."
fun fromConfig(conf: Config): MyFeatureConfig = MyFeatureConfig(
enabled = conf.getBoolean("${PREFIX}enabled", true),
timeoutSeconds = conf.getLong("${PREFIX}timeout.seconds", 30),
endpoint = conf.get("${PREFIX}endpoint", "https://default.example.com"),
)
}
}
open class MyFeatureService(
private val config: MyFeatureConfig,
) {
suspend fun process(page: WebPage, driver: WebDriver): Result {
}
}
Step 6: Create Plugin Manifest and Auto-Configuration Imports
Two mandatory resource files in every plugin JAR:
src/main/resources/META-INF/browser4-plugin.json:
{
"name": "browser4-myfeature",
"version": "4.12.0-rc.1",
"description": "A Browser4 plugin that provides custom page processing functionality",
"dependsOn": ["browser4-protocol", "browser4-agentic"],
"autoConfigurationClasses": [
"ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration"
]
}
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (single line):
ai.platon.pulsar.myfeature.config.MyFeatureAutoConfiguration
Step 7: Write Tests
Place tests in src/test/kotlin/ mirroring the source package. Use JUnit 5 + kotlin-test-junit5 + spring-boot-test (all test scope).
Config test pattern:
@Test
fun `test config defaults`() {
val config = MyFeatureConfig()
assertTrue(config.enabled)
assertEquals(30, config.timeoutSeconds)
}
@Test
fun `test fromConfig reads properties`() {
val conf = Config.of(mapOf("myfeature.enabled" to "false"))
val config = MyFeatureConfig.fromConfig(conf)
assertFalse(config.enabled)
}
Service test pattern — use java.lang.reflect.Proxy for lightweight mocks:
@Suppress("UNCHECKED_CAST")
private fun webPageProxy(): WebPage {
return Proxy.newProxyInstance(
WebPage::class.java.classLoader,
arrayOf(WebPage::class.java)
) { _, _, _ -> null } as WebPage
}
Event handler test pattern — use runBlocking for coroutine testing:
@Test
fun `test browse event handler processes page`() = runBlocking {
val handler = MyFeatureService(mockConfig)
val result = handler.process(webPageProxy(), mockDriver)
assertNotNull(result)
}
Step 8: Build, Verify, and Deploy
mvn package -DskipTests
cp target/browser4-myfeature-1.0.0-SNAPSHOT.jar /path/to/browser4/plugins/
curl -X POST http://localhost:8182/api/plugins/install \
-F "file=@target/browser4-myfeature-1.0.0-SNAPSHOT.jar"
After restart, check application logs for:
PluginManager: Found X PluginMount bean(s)
PluginManager: ✓ Configured browse event handlers
PluginManager: Found X Browser4Plugin bean(s)
- browser4-myfeature v4.12.0-rc.1
File Reference
| File | Required | Purpose |
|---|
pom.xml | Yes | Maven build with browser4-pdk parent; all Browser4 deps in provided scope |
src/main/resources/META-INF/browser4-plugin.json | Yes | Plugin manifest: name, version, description, dependsOn, autoConfigurationClasses |
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | Yes | Single line: FQN of the @AutoConfiguration class |
config/<Feature>AutoConfiguration.kt | Yes | Spring @AutoConfiguration — implements PluginMount sub-interfaces and defines beans |
config/<Feature>Config.kt | Common | Configuration data class read from Properties/Config |
integration/<Feature>BrowseEventHandler.kt | Common | Browse-phase event handler with business logic for each hook |
integration/<Feature>LoadEventHandler.kt | Optional | Load-phase event handler for URL/parsing hooks |
service/<Feature>Service.kt | Common | Business logic service (injected into event handlers and tool executors) |
tools/<Feature>ToolExecutor.kt | Optional | LLM agent tool extending AbstractToolExecutor |
<Feature>Plugin.kt | Optional | Browser4Plugin lifecycle (manifest + onStartup/onShutdown) |
README.md | Common | Plugin documentation |
Reference Map
Key source files to read for patterns and examples:
| Resource | Path | What it demonstrates |
|---|
| Canonical test plugin | browser4-pdk/browser4-pdk-test-plugin/ | All three event-phase mount points (BrowseEventMount, LoadEventMount, CrawlEventMount) — the compatibility canary |
| Plugin archetype | browser4-pdk/browser4-plugin-archetype/src/main/resources/archetype-resources/ | Scaffolded project structure, templates for all required files |
| PluginMount interfaces | browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/plugin/MountPoints.kt | PluginMount, BrowseEventMount, LoadEventMount, CrawlEventMount interface definitions |
| Event lifecycle | browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/event/PageEvents.kt | LoadEventHandlers, BrowseEventHandlers, CrawlEventHandlers — all 28 hooks |
| Event handler types | browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/event/EventHandlers.kt | Chainable handler function types (WebPageWebDriverEventHandler, etc.) |
| Plugin manifest | browser4-core/browser4-skeleton/src/main/kotlin/ai/platon/pulsar/skeleton/plugin/PluginManifest.kt | PluginManifest data class schema |
| ToolMount + registry | browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/ToolMount.kt | ToolMount interface + CustomToolRegistry singleton |
| ToolExecutor base | browser4-agentic/src/main/kotlin/ai/platon/pulsar/agentic/tools/builtin/AbstractToolExecutor.kt | ToolExecutor interface and AbstractToolExecutor base class |
| PDK parent POM | browser4-pdk/pom.xml | Parent POM for plugin projects (standalone — inherits from pulsar-parent on Maven Central) |
| Plugin dev docs | docs/plugin-development.md |
Error Handling
| Symptom | Likely cause | Fix |
|---|
| Plugin not loaded at startup; no "registered plugin" log | browser4-plugin.json missing, malformed, or JAR not in the plugins directory | Verify JAR is in the configured plugins directory; verify JSON is valid; verify autoConfigurationClasses FQN matches |
ClassNotFoundException for Browser4 API classes | Dependency scope is compile instead of provided | Change all browser4-* and Spring Boot deps to <scope>provided</scope> |
| Mount point handlers never fire | Auto-configuration class doesn't implement the correct PluginMount interface, or AutoConfiguration.imports file is missing/wrong | Verify AutoConfiguration.imports contains the exact FQN; verify the auto-config class implements the mount interface |
BeanCreationException at startup | A bean dependency is missing or circular | Check bean constructor args; ensure @Lazy on the auto-config class |
| Tool executor not available to agents | getToolExecutors() returns empty list, or executor not exposed as a Spring bean | Verify the list contains beans from applicationContext; verify executor has @Bean in auto-config |
| Archetype generation fails | Maven can't resolve browser4-plugin-archetype | Build the PDK locally first: mvn -pl browser4-pdk install |
onDocumentSteady handler throws silently | Missing try-catch in handler body | Always wrap handler body in try-catch; log errors via a logger |
| Config properties have no effect | Config class not reading from Config or property prefix mismatch | Verify fromConfig() method reads all keys with correct prefix; verify property names in application config |
NoSuchMethodError at runtime | Plugin compiled against a different Browser4 version than the host | Rebuild with matching browser4-pdk version; or reinstall the matching Browser4 version |
Critical Warnings
Warning: All browser4-* and Spring Boot dependencies must use <scope>provided</scope>. Using compile scope causes ClassNotFoundException at runtime because the host application provides these classes. The plugin JAR should only bundle your own code and true third-party libraries (use compile scope for those).
Warning: Always annotate the auto-configuration class with @Lazy. Plugin beans depend on services that may not be available until the application context is fully initialized. Without @Lazy, startup may fail with BeanCreationException.
Warning: The browser4-plugin.json manifest and the Browser4Plugin.manifest property must stay in sync. The JSON file is always required for JAR discovery; the Browser4Plugin interface is optional. If both are present, the JSON manifest is authoritative for the plugin registry.
Warning: Event handlers run on the browser event loop. Never perform blocking I/O (HTTP calls, file writes) directly in a handler — use coroutines. For ToolMount executors, the framework handles coroutine dispatch automatically.
Warning: An uncaught exception in one event handler can break the entire handler chain. Always wrap handler bodies in try-catch and log failures.
Note: For lightweight plugin development without cloning the full Browser4 repo, the browser4-pdk parent POM extends pulsar-parent from Maven Central. Third-party developers only need the archetype and a Maven installation.
Tip: Build and test incrementally. After scaffolding, immediately run mvn package to verify the build works before writing any custom code. Then implement one mount point at a time, rebuilding and testing after each.
Tip: Read the browser4-pdk-test-plugin source before writing complex handlers. It demonstrates every mount point and serves as the compatibility canary.
Tip: When using @ConditionalOnProperty, prefer matchIfMissing = true so the plugin is enabled by default unless explicitly disabled. This matches the convention used by all first-party plugins.
Tip: The browser4-plugins/browser4-images/ plugin is the best all-around reference — it implements BrowseEventMount + ToolMount + Browser4Plugin with a config data class, service layer, browse event handler, and tool executor.
See Also
- Plugin Development Guide — Official plugin development documentation
- AGENTS.md — Project architecture, build commands, code style, and testing conventions
- PDK Test Plugin — Minimal reference plugin implementing all mount points
- Built-in Plugins — Five first-party plugin implementations (captcha, images, media, pptx, markdown)