This guide explains how to add new integration and extend existing integration with new triggers and components in SuperPlane.
- Overview
- Integration Structure
- Creating a New Integration
- Adding Triggers
- Adding Components
- Adding Frontend Mappers
- Example: GitHub Issues Trigger
Integrations in SuperPlane are connections with external services that allow users to trigger workflows and interact with those services. Integrations consist of:
- Backend implementation (Go): Located in
pkg/integrations/<app-name>/ - Frontend mappers (TypeScript): Located in
web_src/src/pages/workflowv2/mappers/<integration-name>/
Each integration can have:
- Triggers: Event sources that start workflow executions (e.g., "On Pull Request", "On Pipeline Done")
- Components: Actions that can be executed as part of workflows (e.g., "Run Workflow")
Integrations are organized in pkg/integrations/ with the following structure:
pkg/integrations/
├── github/
│ ├── github.go # Main integration implementation
│ ├── client.go # API client (if needed)
│ ├── on_pull_request.go # Trigger implementation
│ ├── on_push.go # Another trigger
│ └── on_issue.go # Yet another trigger
└── semaphore/
├── semaphore.go
├── client.go
└── ...
Frontend mappers are organized in web_src/src/pages/workflowv2/mappers/:
web_src/src/pages/workflowv2/mappers/
├── github/
│ ├── index.ts # Exports all trigger/component renderers
│ ├── on_pull_request.ts # Trigger renderer
│ ├── on_push.ts
│ └── on_issue.ts
└── semaphore/
└── ...
To create a new integration, you need to:
-
Create the integration package in
pkg/integrations/<integration-name>/ -
Implement the main integration file (
<integration-name>.go):
package myintegration
import (
"github.com/superplanehq/superplane/pkg/configuration"
"github.com/superplanehq/superplane/pkg/core"
"github.com/superplanehq/superplane/pkg/registry"
)
func init() {
registry.RegisterIntegration("myintegration", &MyIntegration{})
}
type MyIntegration struct{}
type Configuration struct {
APIKey string `json:"apiKey"`
}
type Metadata struct {
// Store integration-level metadata
}
func (i *MyIntegration) Name() string {
return "myintegration"
}
func (i *MyIntegration) Label() string {
return "My Integration"
}
func (i *MyIntegration) Icon() string {
return "icon-name"
}
func (i *MyIntegration) Description() string {
return "Description of what this integration does"
}
func (i *MyIntegration) Configuration() []configuration.Field {
return []configuration.Field{
{
Name: "apiKey",
Label: "API Key",
Type: configuration.FieldTypeString,
Sensitive: true,
Description: "Your API key",
Required: true,
},
}
}
func (i *MyIntegration) Components() []core.Component {
return []core.Component{
// Add your components here
}
}
func (i *MyIntegration) Triggers() []core.Trigger {
return []core.Trigger{
// Add your triggers here
}
}
func (i *MyIntegration) Sync(ctx core.SyncContext) error {
// Validate configuration and set up the integration
// Set state to "ready" when done: ctx.Integration.Ready()
return nil
}
func (i *MyIntegration) HandleRequest(ctx core.HTTPRequestContext) {
// Handle incoming HTTP requests (e.g., OAuth callbacks, webhooks)
}- Register the integration in the
init()function (shown above)
If the integration manages webhooks, register a core.WebhookHandler along with the integration:
func init() {
registry.RegisterIntegrationWithWebhookHandler("myintegration", &MyIntegration{}, &MyIntegrationWebhookHandler{})
}Triggers listen to external events and start workflow executions. Here's how to add a new trigger:
Create a new file in your integration package (e.g., on_event.go):
package myintegration
import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/superplanehq/superplane/pkg/configuration"
"github.com/superplanehq/superplane/pkg/core"
"github.com/superplanehq/superplane/pkg/crypto"
)
type OnEvent struct{}
type OnEventMetadata struct {
// Store trigger-specific metadata
Resource string `json:"resource"`
}
type OnEventConfiguration struct {
Resource string `json:"resource"`
Actions []string `json:"action"`
}
func (t *OnEvent) Name() string {
return "myintegration.onEvent"
}
func (t *OnEvent) Label() string {
return "On Event"
}
func (t *OnEvent) Description() string {
return "Listen to event occurrences"
}
func (t *OnEvent) Icon() string {
return "icon-name"
}
func (t *OnEvent) Color() string {
return "gray"
}
func (t *OnEvent) Configuration() []configuration.Field {
return []configuration.Field{
{
Name: "resource",
Label: "Resource",
Type: configuration.FieldTypeString,
Required: true,
},
{
Name: "actions",
Label: "Actions",
Type: configuration.FieldTypeMultiSelect,
Required: true,
Default: []string{"created"},
TypeOptions: &configuration.TypeOptions{
MultiSelect: &configuration.MultiSelectTypeOptions{
Options: []configuration.FieldOption{
{Label: "Created", Value: "created"},
{Label: "Updated", Value: "updated"},
{Label: "Deleted", Value: "deleted"},
},
},
},
},
}
}
func (t *OnEvent) Setup(ctx core.TriggerContext) error {
var metadata OnEventMetadata
err := mapstructure.Decode(ctx.Metadata.Get(), &metadata)
if err != nil {
return fmt.Errorf("failed to parse metadata: %w", err)
}
// If metadata is already set, trigger is already setup
if metadata.Resource != "" {
return nil
}
config := OnEventConfiguration{}
err = mapstructure.Decode(ctx.Configuration, &config)
if err != nil {
return fmt.Errorf("failed to decode configuration: %w", err)
}
// Validate configuration
if config.Resource == "" {
return fmt.Errorf("resource is required")
}
// Store metadata
metadata.Resource = config.Resource
err = ctx.Metadata.Set(metadata)
if err != nil {
return fmt.Errorf("failed to set metadata: %w", err)
}
// Request webhook if needed
return ctx.Integration.RequestWebhook(WebhookConfiguration{
EventType: "event",
Resource: config.Resource,
})
}
func (t *OnEvent) Actions() []core.Action {
return []core.Action{}
}
func (t *OnEvent) HandleAction(ctx core.TriggerActionContext) (map[string]any, error) {
return nil, nil
}
func (t *OnEvent) HandleWebhook(ctx core.WebhookRequestContext) (int, *core.WebhookResponseBody, error) {
// Validate webhook signature
signature := ctx.Headers.Get("X-Signature")
if signature == "" {
return http.StatusForbidden, nil, fmt.Errorf("invalid signature")
}
// Verify the signature
secret, err := ctx.Webhook.GetSecret()
if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("error authenticating request")
}
if err := crypto.VerifySignature(secret, ctx.Body, signature); err != nil {
return http.StatusForbidden, nil, err
}
// Parse the webhook payload
data := map[string]any{}
err = json.Unmarshal(ctx.Body, &data)
if err != nil {
return http.StatusBadRequest, nil, fmt.Errorf("error parsing request body: %v", err)
}
// Filter by action type
config := OnEventConfiguration{}
err = mapstructure.Decode(ctx.Configuration, &config)
if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to decode configuration: %w", err)
}
action, ok := data["action"]
if !ok {
return http.StatusBadRequest, nil, fmt.Errorf("missing action")
}
if !slices.Contains(config.Actions, action.(string)) {
return http.StatusOK, nil, nil
}
// Emit the event to trigger workflow execution
err = ctx.Events.Emit(data)
if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("error emitting event: %v", err)
}
return http.StatusOK, nil, nil
}Add the trigger to your integration's Triggers() method:
func (i *MyIntegration) Triggers() []core.Trigger {
return []core.Trigger{
&OnEvent{},
}
}If your triggers or components require webhooks, implement a core.WebhookHandler and register it with the integration:
type WebhookConfiguration struct {
EventType string `json:"eventType"`
Resource string `json:"resource"`
}
type MyIntegrationWebhookHandler struct{}
// CompareConfig defines when two webhook configurations are equal.
// This is used to determine if an existing webhook can be reused.
func (h *MyIntegrationWebhookHandler) CompareConfig(a, b any) (bool, error) {
configA := WebhookConfiguration{}
if err := mapstructure.Decode(a, &configA); err != nil {
return false, err
}
configB := WebhookConfiguration{}
if err := mapstructure.Decode(b, &configB); err != nil {
return false, err
}
// Define equality based on your integration's webhook configuration.
// Webhooks with matching configurations can be shared across multiple triggers/components.
return configA.Resource == configB.Resource && configA.EventType == configB.EventType, nil
}
// Setup creates a webhook in the external service.
// This is called by the webhook provisioner for pending webhook records.
func (h *MyIntegrationWebhookHandler) Setup(ctx core.WebhookHandlerContext) (any, error) {
// Create webhook in the external service
// Return metadata about the created webhook (e.g., webhook ID)
return nil, nil
}
// Cleanup deletes a webhook from the external service.
// This is called by the webhook cleanup worker for deleted webhook records.
func (h *MyIntegrationWebhookHandler) Cleanup(ctx core.WebhookHandlerContext) error {
// Delete webhook from the external service using the metadata
return nil
}Webhook Logic Overview:
The webhook management logic is centralized in Integration.RequestWebhook(). When a trigger or component requests a webhook:
- The context lists all existing webhooks for the integration
- For each existing webhook, it calls your webhook handler's
CompareConfig()to check if configurations match - If a match is found, the node is associated with the existing webhook
- If no match is found, a new webhook is created
This means multiple triggers and components can share the same webhook if they have matching configurations, reducing the number of webhooks created in external services.
Components are actions that can be executed as part of workflows. The process is similar to triggers:
- Create a new file for your component (e.g.,
do_action.go) - Implement the
core.Componentinterface - Register it in your integration's
Components()method
Frontend mappers render triggers and components in the UI. They define how events are displayed and what information is shown to users.
Create a new file in web_src/src/pages/workflowv2/mappers/<app-name>/ (e.g., on_event.ts):
import { WorkflowsWorkflowEvent } from "@/api-client";
import { getColorClass, getBackgroundColorClass } from "@/utils/colors";
import { TriggerRenderer, NodeInfo, ComponentDefinition } from "../types";
import appIcon from "@/assets/icons/integrations/<app-name>.svg";
import { TriggerProps } from "@/ui/trigger";
interface OnEventMetadata {
resource: string;
}
interface OnEventConfiguration {
actions: string[];
}
interface OnEventEventData {
action?: string;
// Add other fields from your webhook payload
}
/**
* Renderer for the "myintegration.onEvent" trigger
*/
export const onEventTriggerRenderer: TriggerRenderer = {
getTitleAndSubtitle: (event: WorkflowsWorkflowEvent): { title: string; subtitle: string } => {
const eventData = event.data as OnEventEventData;
return {
title: `Event occurred`,
subtitle: eventData?.action || "",
};
},
getRootEventValues: (lastEvent: WorkflowsWorkflowEvent): Record<string, string> => {
const eventData = lastEvent.data as OnEventEventData;
return {
Action: eventData?.action || "",
// Add other relevant fields
};
},
getTriggerProps: (node: NodeInfo, definition: ComponentDefinition, lastEvent: WorkflowsWorkflowEvent) => {
const metadata = node.metadata as unknown as OnEventMetadata;
const configuration = node.configuration as unknown as OnEventConfiguration;
const metadataItems = [];
if (metadata?.resource) {
metadataItems.push({
icon: "database",
label: metadata.resource,
});
}
if (configuration?.actions) {
metadataItems.push({
icon: "funnel",
label: configuration.actions.join(", "),
});
}
const props: TriggerProps = {
title: node.name!,
iconSrc: appIcon,
iconBackground: "bg-white",
iconColor: getColorClass(definition.color),
headerColor: getBackgroundColorClass(definition.color),
collapsedBackground: getBackgroundColorClass(definition.color),
metadata: metadataItems,
};
if (lastEvent) {
const eventData = lastEvent.data as OnEventEventData;
props.lastEventData = {
title: `Event occurred`,
subtitle: eventData?.action || "",
receivedAt: new Date(lastEvent.createdAt!),
state: "triggered",
eventId: lastEvent.id,
};
}
return props;
},
};Update or create index.ts in your integration's mapper directory:
import { ComponentBaseMapper, TriggerRenderer } from "../types";
import { onEventTriggerRenderer } from "./on_event";
export const componentMappers: Record<string, ComponentBaseMapper> = {};
export const triggerRenderers: Record<string, TriggerRenderer> = {
onEvent: onEventTriggerRenderer,
};Here's a complete example of the GitHub Issues trigger that was recently added:
The trigger implements:
- Configuration fields for repository and action filtering
- Setup method to validate and store metadata
- Webhook handling with signature verification
- Action filtering to only emit events for configured actions
Key features:
- Supports 16 different issue action types (opened, closed, labeled, etc.)
- Validates that the repository is accessible to the GitHub app installation
- Filters events by action type before emitting
The mapper provides:
getTitleAndSubtitle: Formats event display as "#123 - Issue title"getRootEventValues: Extracts key fields (URL, Title, Action, Author, State)getTriggerProps: Renders the trigger node with repository and action metadata
After implementing your integration:
- Build and format: Run
make format.go && make lint && make check.build.app - Test the backend: Run
make test - Test the UI: Run
make check.build.ui - E2E tests: Consider adding E2E tests (see e2e_tests.md)
- Use descriptive names: Trigger and component names should clearly indicate what they do
- Validate configuration: Always validate configuration in the
Setup()method - Handle errors gracefully: Return appropriate HTTP status codes and error messages
- Use metadata for caching: Store frequently accessed data in metadata to avoid repeated API calls
- Filter early: Filter events as early as possible to avoid unnecessary processing
- Default configuration: The default configuration should be thought out in a way to cover the most common use case, and to avoid generating unnecessary events. For example, the
github.onPushtrigger filters by only the commits on themainbranch by default. - Verify signatures: Always verify webhook signatures to ensure authenticity
- Document action types: Clearly document all available action types for triggers
- Consistent styling: Follow the existing patterns for frontend mappers
- GitHub integration:
pkg/integrations/github/ - Semaphore integration:
pkg/integrations/semaphore/ - Core interfaces:
pkg/core/ - Frontend mappers:
web_src/src/pages/workflowv2/mappers/