ScreenshotNeo

BlogHow-to

How to Build a Custom Appium Plugin

Build a Node.js Appium plugin from package metadata through command interception, local development, configuration, testing, and distribution.

By the ScreenshotNeo team4 October 20268 min read

An Appium plugin is an optional Node.js package that extends or changes Appium server behavior. To build one, declare Appium as a peer dependency, add the pluginName and mainClass fields in the appium package metadata, export a class extending BasePlugin, then install and activate it on the server. A plugin has no effect until the server administrator enables it.

This guide uses the current Appium plugin-development workflow. Compatibility depends on the Appium version you target, so check the current Appium plugin guide and test against the specific version you intend to support. The API reference cited below describes Appium 2.0 and is background for the handler interface, not a guarantee of compatibility with every release.

1. Decide whether a plugin fits

First define the server behavior you need. A plugin can add behavior around an existing command, handle commands more broadly, or change how a command works. Since plugins can intercept or replace behavior, keep the scope narrow and make activation an explicit decision by the server administrator.

Appium’s plugin ecosystem includes examples such as Execute Driver for command batches, Images for image matching and comparison, Relaxed Caps for capability-prefix handling, Storage for server-side storage, and Universal XML for a common XML definition across iOS and Android. These are examples from the ecosystem documentation, not a definitive current inventory. Review existing plugins before creating another one.

2. Create the package

A minimal package needs a Node.js entry point, an Appium peer dependency, and Appium extension metadata. The metadata’s mainClass must name an exported class.

{
  "name": "appium-plugin-example",
  "version": "1.0.0",
  "description": "An example Appium plugin",
  "type": "module",
  "main": "./index.js",
  "peerDependencies": {
    "appium": "<range supported by this plugin>"
  },
  "appium": {
    "pluginName": "example",
    "mainClass": "ExamplePlugin"
  }
}

Replace the peer dependency placeholder with the Appium version range you have actually validated. The package format and entry point are project choices; this example uses a JavaScript ES module and does not require a build step. If you compile TypeScript or another source format, point main to the compiled entry file and include the necessary build scripts and published output.

3. Implement a command handler

To wrap a command already handled by a driver, add an asynchronous class method with that command’s name. The handler receives next, the session driver, and command arguments. Call await next() to continue to the rest of the behavior chain, including the normal command behavior where applicable. If you omit it, that behavior does not run.

import { BasePlugin } from 'appium/plugin';

class ExamplePlugin extends BasePlugin {
  async setUrl(next, driver, url) {
    // Work before the driver's normal setUrl command.
    const result = await next();
    // Work after the normal command completes.
    return result;
  }
}

export { ExamplePlugin };

This wrapper intentionally adds no logging or page-source work, so it is safe as a small starting point. Add your own behavior before or after next() as needed. Preserve and return the downstream result unless your plugin is deliberately changing the command’s return value.

For broader command inspection, implement handle:

class ExamplePlugin extends BasePlugin {
  async handle(next, driver, cmdName, ...args) {
    if (cmdName === 'setUrl') {
      // Optional command-specific work before delegation.
    }
    return await next();
  }
}

The handle signature and command-specific methods are extension interfaces; the Appium 2.0 Plugin API reference provides background on the interface. Check the current development guide for the target release’s requirements.

4. Understand the behavior chain

  • Before next(): validate arguments, record information, or prepare state before the driver runs.
  • After next(): inspect the result or perform follow-up work after the command completes.
  • Call next() once when delegating: this lets the next plugin or default driver behavior proceed.
  • Do not call it when replacing behavior intentionally: this prevents the rest of the chain from running, so document that choice.
  • Proxy mode: if your plugin takes over a command but wants normal proxy behavior, call next().

Keep command interception precise. A handler that runs for more commands than intended can affect unrelated sessions or drivers.

5. Add plugin options and scripts

Plugins can define custom command-line arguments in extension metadata. Appium prefixes an argument with --plugin-<plugin-name>-. For a plugin named pluggo with an electro-port option, the command-line flag is --plugin-pluggo-electro-port. The same value can be supplied through server configuration at server.plugin.<plugin-name>.

Use the current plugin guide’s metadata format to declare arguments and read them in your plugin. Keep defaults explicit, validate values when the plugin starts or first uses them, and report invalid configuration with an actionable error. The documentation describes the argument naming and configuration location; exact metadata and runtime patterns should follow the guide for the Appium version being targeted.

A plugin can also map script names to JavaScript files in its metadata. Users run one with:

appium plugin run <plugin-name> <script-name>

Scripts suit maintenance or setup tasks that belong with the extension but are not part of a WebDriver command.

6. Install and activate it locally

One local development route is to let Appium’s extension CLI install your package from its directory:

appium plugin install --source=local /path/to/your/plugin
appium --use-plugins=example

Use the plugin name from pluginName when activating it. Another documented route for an npm-based development project is to include Appium and your local plugin package among development dependencies, then start the server through npm exec appium or npx appium. That keeps the development project’s dependency versions together.

After editing plugin code, restart the Appium server to load the change. Alternatively, set APPIUM_RELOAD_EXTENSIONS to request extension reloading when a new session starts. This does not imply that an active session is hot-reloaded.

For a quick activation check, start Appium with the plugin enabled, create a session that can issue the command your plugin handles, and verify the expected effect. Also verify the delegated command still works when your handler calls next().

7. Test behavior and compatibility

Appium’s development guide recommends local installation to observe plugin behavior before publishing. A practical test matrix should cover the cases relevant to your plugin:

  1. Start the targeted Appium version with the plugin enabled and confirm it loads.
  2. Run the intercepted command with valid input and verify both plugin behavior and downstream behavior.
  3. Exercise invalid input, command failures, and missing configuration.
  4. Check behavior when another plugin participates in the same command chain, if your deployment uses one.
  5. Repeat against each Appium version in the package’s declared peer dependency range.
  6. Start a server without the plugin enabled and confirm the plugin has no effect.

This is an engineering test recommendation, not a prescribed Appium test suite. Document which versions you have verified and what commands the plugin intercepts.

8. Publish, install, update, and remove

For npm distribution, publish the package through your normal npm release process, then install it with:

appium plugin install --source=npm <package-name>
appium --use-plugins=example

The extension CLI also supports local, Git, and GitHub sources. Git and GitHub installs require the package name. These options serve different workflows:

Source Useful for Consideration
Local Development from a directory on the same machine Convenient for iteration; not a distribution channel for remote users.
npm Public or private package releases Use package versions and release notes to communicate compatibility.
Git Installing from a repository source Coordinate the reference and package name with your release process.
GitHub Installing from a GitHub-hosted project Suitable for repository-based distribution where users can access it.

Manage installed extensions with the CLI:

appium plugin list
appium plugin update <plugin-name>
appium plugin uninstall <plugin-name>

The CLI also supports running extension scripts with appium plugin run. Updates default to minor and patch changes; the --unsafe option permits major updates that may break compatibility. Read the extension CLI reference for current command syntax and options.

9. Troubleshooting

Symptom Likely cause Fix
Appium says the plugin is unknown or unavailable The package is not installed, or the name passed to --use-plugins does not match pluginName. Install it using the intended source and activate it with the exact metadata name.
The package installs but the plugin fails to load The entry point, module format, or named mainClass export does not match the package. Check main, confirm the exported class name exactly matches mainClass, and ensure the class extends BasePlugin from appium/plugin.
The handler never runs The plugin may not be activated, the method may not match the command, or a different command path may be in use. Confirm startup activation and check the command name and method signature against the current plugin guide.
The original command or proxy behavior stops working The handler took over but did not call next(). Call and await next() where downstream behavior should run; omit it only when intentionally replacing behavior.
Code edits have no effect The server is still running with the previously loaded extension. Restart Appium, or configure APPIUM_RELOAD_EXTENSIONS for reload on a new session.
A custom option is ignored The CLI prefix, plugin name, or configuration key may be wrong. Use the --plugin-<plugin-name>-<argument> form or the server.plugin.<plugin-name> configuration path.
The plugin works on one Appium version but not another The declared compatibility range exceeds the versions actually supported or tested. Narrow the peer dependency range to validated versions and check the current development documentation for each target.

10. Reliability, trust, and performance

Plugins run in the server’s command path, so extra work in a handler can add latency to every intercepted command. Keep per-command work bounded, avoid repeated expensive operations where they are unnecessary, and make errors and timeouts understandable to the caller. The cited documentation does not provide plugin performance benchmarks or reliability guarantees; measure the behavior in the deployment that matters to you.

Plugins are opt-in because they can change or replace server command behavior. Explain what commands the plugin touches, whether it delegates with next(), what configuration it reads, and what data it handles. Test locally or in a controlled Appium server before enabling it for other users.

Or skip the browser setup

If your Appium plugin project also needs website screenshots for docs, reports, or visual checks, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

FAQ

Does a plugin run as soon as it is installed?

No. The server administrator must activate it when starting Appium, for example with --use-plugins=example.

Can a plugin replace a built-in command?

Yes. A handler can intentionally omit next() and provide replacement behavior. That also prevents the rest of the behavior chain from running, so make the effect clear to users.

Should every plugin publish to npm?

No. Appium supports local, npm, Git, and GitHub installation sources. Choose the source that fits how your users obtain and update the package.

Does reloading extensions update an active session?

The documented reload setting requests reloading on a new session. Restart the server when you need a straightforward reload during development.