Java Plugin Development
Java Plugin Development
Overview
Java plugins provide the most powerful and flexible way to extend Rundeck. They offer:
- Full Access to Rundeck's internal APIs
- Best Performance for complex operations
- Type Safety with compile-time checking
- Professional IDE Support for development and debugging
- Rich Ecosystem of Java libraries and frameworks
- Support for All Plugin Types
Java plugins are distributed as .jar files containing one or more service providers and their dependencies.
Quick Start
1. Set Up Development Environment
Requirements:
- JDK 11 or later
- Maven or Gradle build tool
- IDE (IntelliJ IDEA, Eclipse, or VS Code recommended)
2. Add Rundeck Dependencies
For Gradle:
dependencies {
implementation(group:'org.rundeck', name: 'rundeck-core', version: '5.18.0-20251216')
}
For Maven:
<dependencies>
<dependency>
<groupId>org.rundeck</groupId>
<artifactId>rundeck-core</artifactId>
<version>5.18.0-20251216</version>
<scope>compile</scope>
</dependency>
</dependencies>
For Storage Plugins:
Also add rundeck-storage-api:
implementation(group:'org.rundeck', name: 'rundeck-storage-api', version: '5.18.0-20251216')
Rundeck's jars are published to Maven Central:
3. Create Provider Class
package com.example.rundeck.plugin;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.plugins.ServiceNameConstants;
import com.dtolabs.rundeck.plugins.step.StepPlugin;
import com.dtolabs.rundeck.plugins.step.PluginStepContext;
import com.dtolabs.rundeck.plugins.descriptions.*;
@Plugin(name = "my-plugin", service = ServiceNameConstants.WorkflowStep)
@PluginDescription(title = "My Plugin", description = "Does something useful")
public class MyPlugin implements StepPlugin {
@PluginProperty(
title = "Message",
description = "Message to display",
required = true
)
private String message;
@Override
public void executeStep(PluginStepContext context,
Map<String, Object> configuration)
throws StepException {
context.getLogger().log(2, "Message: " + message);
}
}
4. Build and Package
Configure your build to create a plugin JAR with the proper manifest. The JAR must include specific metadata in META-INF/MANIFEST.MF.
Example Gradle configuration:
jar {
manifest {
attributes(
'Rundeck-Plugin-Version': '1.2',
'Rundeck-Plugin-Archive': 'true',
'Rundeck-Plugin-Classnames': 'com.example.rundeck.plugin.MyPlugin',
'Rundeck-Plugin-File-Version': '1.0',
'Rundeck-Plugin-Author': 'Your Name',
'Rundeck-Plugin-URL': 'https://example.com',
'Rundeck-Plugin-Date': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
)
}
}
5. Deploy
Copy the JAR file to Rundeck's plugin directory:
- Launcher:
$RDECK_BASE/libext - RPM/DEB:
/var/lib/rundeck/libext
Restart Rundeck or use dynamic plugin loading if enabled.
JAR Manifest Requirements
Your plugin JAR must have these entries in META-INF/MANIFEST.MF:
Required Entries
| Manifest Entry | Description |
|---|---|
Rundeck-Plugin-Version | Plugin mechanism version (use 1.2) |
Rundeck-Plugin-Archive | Must be true |
Rundeck-Plugin-Classnames | Comma-separated list of provider class names |
Example:
Rundeck-Plugin-Version: 1.2
Rundeck-Plugin-Archive: true
Rundeck-Plugin-Classnames: com.example.MyPlugin,com.example.MyOtherPlugin
Optional Entries
| Manifest Entry | Description |
|---|---|
Rundeck-Plugin-File-Version | Your plugin version (e.g., 1.0, 2.1.3) |
Rundeck-Plugin-Author | Author name |
Rundeck-Plugin-URL | Website URL |
Rundeck-Plugin-Date | Publication date (ISO8601 format) |
Rundeck-Plugin-Libs | Space-separated list of bundled dependencies |
Plugin Version
Rundeck-Plugin-Version: 1.2 indicates the plugin mechanism version:
1.2- Current version, enables localization and custom icons1.1- Previous version1.0- Original version
Plugin File Version
Rundeck-Plugin-File-Version is used to load only the newest plugin file when multiple providers of the same name and type are present.
Including Dependencies
If your plugin requires external libraries not included with Rundeck, you can bundle them in your JAR.
Check Available Libraries
Look in /var/lib/rundeck/lib to see third-party JARs already available at runtime.
Bundle Dependencies
1. Add to Manifest:
Rundeck-Plugin-Libs: lib/somejar-1.2.jar lib/anotherjar-1.3.jar
2. Include in JAR structure:
META-INF/
META-INF/MANIFEST.MF
com/
com/mycompany/
com/mycompany/rundeck/
com/mycompany/rundeck/plugin/
com/mycompany/rundeck/plugin/MyPlugin.class
lib/
lib/somejar-1.2.jar
lib/anotherjar-1.3.jar
Provider Classes
A "Provider Class" is a Java class that:
- Implements a specific Rundeck service interface
- Has the
@Pluginannotation - Declares its service type and provider name
Basic Structure
@Plugin(name="myprovider", service="NodeExecutor")
public class MyProvider implements NodeExecutor {
// Implementation
}
Naming Your Plugin
Choose a unique but simple name for your provider:
- Use lowercase with hyphens:
my-custom-plugin - Avoid generic names: ❌
step✅custom-api-step - Be descriptive:
aws-s3-uploader,jira-ticket-creator
Constructors
Your provider class must have at least a zero-argument constructor:
public MyProvider() {
// Initialize
}
Optionally, you can have a single-argument constructor that receives the Framework:
public MyProvider(com.dtolabs.rundeck.core.common.Framework framework) {
this.framework = framework;
}
Logging
Log messages using the ExecutionListener:
context.getExecutionListener().log(2, "Processing item: " + item);
Or write to standard output/error (will be captured):
System.out.println("Processing...");
System.err.println("Warning: something happened");
Available Service Types
Java plugins can implement any of these services:
Node Execution
- NodeExecutor - Execute commands on nodes
- FileCopier - Copy files to nodes
Workflow Steps
- WorkflowStep - Workflow-level step
- WorkflowNodeStep - Node-level step
- RemoteScriptNodeStep - Generate remote scripts
Resource Model
- ResourceModelSource - Provide node inventory
- ResourceFormatParser - Parse resource documents
- ResourceFormatGenerator - Generate resource documents
Notifications and Events
- Notification - Send notifications on job events
- WebhookEventPlugin - Process webhooks
Logging
- ExecutionFileStorage - Store/retrieve execution files
- StreamingLogWriter - Write log events
- StreamingLogReader - Read log events
- LogFilterPlugin - Filter/transform log output
Storage
- Storage - Backend storage for data
- StorageConverter - Encrypt/decrypt stored content
Orchestration
- Orchestrator - Control node execution order
Source Control
- ScmExportPlugin - Export jobs to SCM
- ScmImportPlugin - Import jobs from SCM
Configuration
- PluginGroup - Define shared properties
- OptionValuesPlugin - Provide dynamic option values
- FileUploadPlugin - Handle file uploads
- UserGroupSourcePlugin - Integrate authentication
Lifecycle
- ExecutionLifecyclePlugin - Hook execution lifecycle
- JobLifecyclePlugin - Hook job lifecycle
- AuditEventListenerPlugin - Listen to audit events
User Interface
- UIPlugin - Add custom UI components
- ContentConverterPlugin - Render content as HTML
Plugin Annotations
Annotations provide a declarative way to define plugin metadata and configuration properties.
Limitation
ResourceModelSource, NodeExecutor, and FileCopier plugins currently do not support description annotations. Use the interface-based approach for these plugin types.
@Plugin Annotation
Required for all provider classes:
@Plugin(name="myplugin", service=ServiceNameConstants.WorkflowStep)
public class MyPlugin implements StepPlugin {
// ...
}
@PluginDescription Annotation
Define display name and description:
@Plugin(name="myplugin", service=ServiceNameConstants.WorkflowStep)
@PluginDescription(title="My Plugin", description="Performs a custom step")
public class MyPlugin implements StepPlugin {
// ...
}
Attributes:
title- Display name shown in GUIdescription- Descriptive text shown next to the display name
If not provided, the plugin display name will be the same as the provider name.
@PluginMetadata Annotation
Provide additional metadata like icons:
@Plugin(name="myplugin", service=ServiceNameConstants.WorkflowStep)
@PluginMetadata(key="faicon", value="check-circle")
public class MyPlugin implements StepPlugin {
// ...
}
For multiple metadata entries:
@Plugin(name="myplugin", service=ServiceNameConstants.WorkflowStep)
@PluginMeta({
@PluginMetadata(key="faicon", value="check-circle"),
@PluginMetadata(key="glyphicon", value="ok-sign")
})
public class MyPlugin implements StepPlugin {
// ...
}
Available metadata keys:
glyphicon- Glyphicon icon namefaicon- Font Awesome icon namefabicon- Font Awesome brand icon name
See Provider Metadata for details.
Plugin Properties
Properties allow users to configure your plugin. Annotate class fields to define configuration properties.
Supported Field Types
StringBoolean/booleanInteger/intLong/longSet,List,String[](with@SelectValues(multiOption = true))
@PluginProperty Annotation
Basic property definition:
@PluginProperty(
name = "endpoint",
title = "API Endpoint",
description = "The API endpoint URL",
required = true,
defaultValue = "https://api.example.com"
)
private String endpoint;
Attributes:
| Attribute | Type | Description |
|---|---|---|
name | String | Property identifier |
title | String | Display name in GUI |
description | String | Help text |
required | boolean | Whether value is required (default: false) |
defaultValue | String | Default value |
scope | PropertyScope | Resolution scope (default: InstanceOnly) |
Property Types
String Property:
@PluginProperty(title = "Name", description = "Your name", required = true)
private String name;
Integer Property:
@PluginProperty(title = "Timeout", description = "Timeout in seconds", defaultValue = "30")
private int timeout;
Boolean Property:
@PluginProperty(title = "Enable Logging", description = "Enable detailed logging")
private boolean enableLogging;
@SelectValues Annotation
Create dropdown or multi-select properties:
Single Select:
@PluginProperty(title = "Environment", description = "Target environment")
@SelectValues(values = {"dev", "staging", "production"})
private String environment;
Free Select (with custom values allowed):
@PluginProperty(title = "Region", description = "AWS region")
@SelectValues(values = {"us-east-1", "us-west-2", "eu-west-1"}, freeSelect = true)
private String region;
Multi-Select:
@PluginProperty(title = "Tags", description = "Select tags")
@SelectValues(values = {"web", "database", "cache", "storage"}, multiOption = true)
private Set<String> tags;
Attributes:
values(String[]) - Available optionsfreeSelect(boolean) - Allow custom values (default: false)multiOption(boolean) - Allow multiple selections (default: false)
Tips
When multiOption is used with a String field, values are joined with commas.
@TextArea Annotation
Render as multi-line text area:
@PluginProperty(title = "Script Content", description = "Enter script")
@TextArea
private String scriptContent;
Property Scopes
Control where property values are resolved from:
@PluginProperty(
title = "API Key",
scope = PropertyScope.Project
)
private String apiKey;
Available scopes:
InstanceOnly- Only from instance configuration (default)Instance- Instance and all earlier levelsFramework- Only framework propertiesProjectOnly- Only project propertiesProject- Project and framework properties
See Property Scopes for complete documentation.
Advanced Property Options
Use rendering options for advanced display:
@PluginProperty(title = "Password", description = "API password", required = true)
@PluginRenderingOptions({
@PluginRenderingOption(key = StringRenderingConstants.DISPLAY_TYPE_KEY,
value = "PASSWORD"),
@PluginRenderingOption(key = StringRenderingConstants.SELECTION_ACCESSOR_KEY,
value = StringRenderingConstants.SELECTION_ACCESSOR_STORAGE_PATH)
})
private String password;
For complete rendering options, see Property Rendering Options.
Plugin Descriptions (Non-Annotation Approach)
For plugin types that don't support annotations, or when you need more control, implement one of these interfaces:
Describable Interface
Build the Description object yourself:
public class MyPlugin implements NodeExecutor, Describable {
@Override
public Description getDescription() {
return DescriptionBuilder.builder()
.name("myplugin")
.title("My Plugin")
.description("Custom node executor")
.property(PropertyUtil.string("endpoint", "Endpoint",
"API endpoint URL", true, null))
.property(PropertyUtil.integer("timeout", "Timeout",
"Timeout in seconds", false, "30"))
.build();
}
// Implementation methods...
}
Collaborator Interface
Modify the builder:
public class MyPlugin implements NodeExecutor,
DescriptionBuilder.Collaborator {
@Override
public void buildWith(DescriptionBuilder builder) {
builder.property(PropertyUtil.string("endpoint", "Endpoint",
"API endpoint URL", true, null));
}
// Implementation methods...
}
Using PropertyBuilder
For fine-grained control:
Property property = PropertyBuilder.builder()
.string("apiKey")
.title("API Key")
.description("Your API key")
.required(true)
.renderingOption(StringRenderingConstants.DISPLAY_TYPE_KEY,
StringRenderingConstants.DisplayType.PASSWORD)
.build();
Provider Lifecycle
Instantiation and Reuse
- Provider classes are instantiated when needed by the Framework
- Instances are retained and reused across multiple executions
- The Framework object may exist across multiple executions
- Provider instances may be used by multiple threads
Thread Safety
Important
Your provider class should:
- Avoid instance fields (use parameters passed to methods)
- Be thread-safe in all operations
- Not maintain state between invocations
Bad (not thread-safe):
public class BadPlugin implements StepPlugin {
private String lastResult; // ❌ Instance field
@Override
public void executeStep(PluginStepContext context,
Map<String, Object> config) {
lastResult = process(); // ❌ Not thread-safe
}
}
Good (thread-safe):
public class GoodPlugin implements StepPlugin {
@Override
public void executeStep(PluginStepContext context,
Map<String, Object> config) {
String result = process(); // ✅ Local variable
context.getLogger().log(2, "Result: " + result);
}
}
Failure Handling
Some plugin methods return a "Result" interface indicating success or failure.
Failure Reasons
Use FailureReason to indicate why an operation failed:
return new NodeStepResultImpl(
NodeStepFailureReason.ConnectionFailure,
"Failed to connect to " + hostname,
node
);
Common Failure Reasons:
From NodeStepFailureReason:
ConnectionFailure- Connection failedAuthenticationFailure- Authentication failedHostNotFound- Host not foundIOFailure- I/O errorNonZeroResultCode- Non-zero exit code
From StepFailureReason:
ConfigurationFailure- Configuration errorPluginFailed- Plugin-specific failure
Creating Custom Failure Reasons
Define an enum implementing FailureReason:
public enum CustomFailureReason implements FailureReason {
ApiCallFailed("API call failed"),
InvalidResponse("Invalid API response"),
RateLimitExceeded("Rate limit exceeded");
private String message;
CustomFailureReason(String message) {
this.message = message;
}
@Override
public String toString() {
return message;
}
}
Use in your plugin:
return new StepExecutionResultImpl(
CustomFailureReason.ApiCallFailed,
"API returned error code: " + errorCode
);
Localization and Icons
Java plugins support internationalization and custom icons.
Enable Localization
Set manifest entry:
Rundeck-Plugin-Version: 1.2
Include message files in your JAR:
resources/
└── i18n/
├── messages.properties
├── messages_es.properties
├── messages_fr.properties
└── WorkflowNodeStep.myplugin.messages.properties
See Plugin Localization for complete documentation.
Add Icons
Include icon files in your JAR:
resources/
├── icon.png
└── WorkflowNodeStep.myplugin.icon.png
Or use metadata for CSS icons:
@PluginMetadata(key="faicon", value="check-circle")
See Plugin Icons for details.
Complete Example
Here's a complete example of a Java plugin that calls an external API:
package com.example.rundeck.plugin;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.plugins.ServiceNameConstants;
import com.dtolabs.rundeck.plugins.step.PluginStepContext;
import com.dtolabs.rundeck.plugins.step.StepException;
import com.dtolabs.rundeck.plugins.step.StepPlugin;
import com.dtolabs.rundeck.plugins.descriptions.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
@Plugin(name = "api-caller", service = ServiceNameConstants.WorkflowStep)
@PluginDescription(
title = "API Caller",
description = "Calls an external REST API"
)
@PluginMetadata(key = "faicon", value = "cloud")
public class ApiCallerPlugin implements StepPlugin {
@PluginProperty(
title = "API Endpoint",
description = "The API endpoint URL",
required = true
)
private String endpoint;
@PluginProperty(
title = "Method",
description = "HTTP method",
defaultValue = "GET",
required = true
)
@SelectValues(values = {"GET", "POST", "PUT", "DELETE"})
private String method;
@PluginProperty(
title = "Timeout (seconds)",
description = "Request timeout in seconds",
defaultValue = "30"
)
private int timeout;
@PluginProperty(
title = "API Key",
description = "API authentication key",
required = false,
scope = PropertyScope.Project
)
@SelectValues(values = {})
@PluginRenderingOptions({
@PluginRenderingOption(
key = StringRenderingConstants.DISPLAY_TYPE_KEY,
value = "PASSWORD"
),
@PluginRenderingOption(
key = StringRenderingConstants.SELECTION_ACCESSOR_KEY,
value = StringRenderingConstants.SELECTION_ACCESSOR_STORAGE_PATH
)
})
private String apiKey;
@Override
public void executeStep(PluginStepContext context,
Map<String, Object> configuration)
throws StepException {
context.getLogger().log(2, "Calling API: " + endpoint);
try {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(timeout))
.build();
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.timeout(Duration.ofSeconds(timeout));
if (apiKey != null && !apiKey.isEmpty()) {
requestBuilder.header("Authorization", "Bearer " + apiKey);
}
HttpRequest request = requestBuilder.method(method,
HttpRequest.BodyPublishers.noBody()).build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
context.getLogger().log(2,
"Response code: " + response.statusCode());
context.getLogger().log(3,
"Response body: " + response.body());
if (response.statusCode() >= 400) {
throw new StepException(
"API call failed with status: " + response.statusCode(),
StepFailureReason.PluginFailed
);
}
} catch (Exception e) {
throw new StepException(
"Failed to call API: " + e.getMessage(),
e,
StepFailureReason.PluginFailed
);
}
}
}
Best Practices
Error Handling
Always provide meaningful error messages:
try {
// Operation
} catch (IOException e) {
throw new StepException(
"Failed to read file " + filename + ": " + e.getMessage(),
e,
StepFailureReason.IOFailure
);
}
Logging
Use appropriate log levels:
context.getLogger().log(0, "ERROR: Critical failure");
context.getLogger().log(1, "WARN: Something unexpected");
context.getLogger().log(2, "INFO: Normal operation");
context.getLogger().log(3, "VERBOSE: Detailed info");
context.getLogger().log(4, "DEBUG: Debug information");
Resource Cleanup
Always clean up resources:
HttpClient client = null;
try {
client = HttpClient.newHttpClient();
// Use client
} finally {
if (client != null) {
// Cleanup if needed
}
}
Configuration Validation
Validate configuration early:
@Override
public void executeStep(PluginStepContext context,
Map<String, Object> config) throws StepException {
if (endpoint == null || endpoint.trim().isEmpty()) {
throw new StepException(
"Endpoint is required",
StepFailureReason.ConfigurationFailure
);
}
if (!endpoint.startsWith("http")) {
throw new StepException(
"Endpoint must be a valid URL",
StepFailureReason.ConfigurationFailure
);
}
// Continue with execution...
}
Development Tools
Plugin Bootstrap
Use the Rundeck Plugin Bootstrap tool to quickly generate plugin project templates.
Maven Archetype
Use the Rundeck Plugin Archetype for Maven projects.
Testing
Test your plugins locally before deployment:
@Test
public void testPluginExecution() throws StepException {
MyPlugin plugin = new MyPlugin();
plugin.endpoint = "https://api.example.com";
plugin.timeout = 30;
PluginStepContext context = mock(PluginStepContext.class);
when(context.getLogger()).thenReturn(mock(PluginLogger.class));
plugin.executeStep(context, new HashMap<>());
verify(context.getLogger()).log(eq(2), anyString());
}
Related Documentation
- Plugin Properties Reference - Complete property documentation
- Script Plugin Development - Alternative approach
- Plugin Development Overview - Compare all approaches
- Step Plugins - Workflow and node steps
- Notification Plugins - Job notifications
- Java API Documentation - JavaDoc