Skip to main content

Creating a Custom Third-Party Integration

Overview

Soundcheck integrations (also called Fact Collectors) are responsible for collecting data about your Backstage entities from third-party services. A single piece of data collected about an entity is called a Fact. Facts are then referenced in Soundcheck Checks to determine whether an entity adheres to the rules you've defined.

Soundcheck comes with a set of pre-built integrations out-of-the-box, including GitHub, PagerDuty, Datadog, and more. However, you may need to create custom integrations to collect different types of data or connect to internal services specific to your organization.

note

It is not possible to extend or modify pre-built fact collectors shipped by Soundcheck. If you need extra functionality, you must create a custom integration or use the HTTP integration for simpler use cases.

This tutorial walks you through creating a custom integration from scratch, using the PagerDuty API as an example. The same pattern applies to any third-party API.

What you'll build: A fact collector called ExamplePagerDutyFactCollector that retrieves service details from PagerDuty for entities annotated with a pagerduty.com/service-id.

Time to complete: ~30 minutes

Prerequisites

Before starting, ensure you have:

  • Soundcheck backend installed
  • Soundcheck frontend installed
  • A PagerDuty API token (for this example)

Implementation

Step 1: Create the Backend Module

Use the Backstage CLI to create a new backend module for your integration:

yarn new

When prompted, enter:

PromptValue
What do you want to create?backend-plugin-module
ID of the pluginsoundcheck
ID of the moduleexample-pagerduty

You'll see: "Successfully created backend-plugin-module."

Next, install the required packages:

From your Backstage root directory
yarn --cwd plugins/soundcheck-backend-module-example-pagerduty add \
@spotify/backstage-plugin-soundcheck-node \
@spotify/backstage-plugin-soundcheck-common \
@backstage/catalog-model \
@backstage/config \
@backstage/types \
@pagerduty/pdjs \
luxon \
qs

Step 2: Create the Fact Collector

Create a new file called ExamplePagerDutyFactCollector.ts in the src folder of your new module.

Start with the imports and constants:

src/ExamplePagerDutyFactCollector.ts
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { api } from '@pagerduty/pdjs';
import {
CollectionConfig,
Fact,
FactRef,
stringifyFactRef,
} from '@spotify/backstage-plugin-soundcheck-common';
import { ConfigurableFactCollector } from '@spotify/backstage-plugin-soundcheck-node';
import { DateTime } from 'luxon';
import qs from 'qs';
import { JsonObject, JsonValue } from '@backstage/types';
import { Config, ConfigReader } from '@backstage/config';
import { PartialCall } from '@pagerduty/pdjs/build/src/api';
import { LoggerService } from '@backstage/backend-plugin-api';

// Unique identifier for this integration
const ID = 'example-pagerduty';
const SCOPE = 'default';
const SERVICE_FACT_REFERENCE = `${ID}:${SCOPE}/service`;

// Fields to request from PagerDuty API
const SERVICE_DETAILS = [
'escalation_policies',
'teams',
'auto_pause_notifications_parameters',
'integrations',
];
📖 What do these imports and constants mean?

Key imports:

ImportPackagePurpose
Entity@backstage/catalog-modelRepresents a single entity in the Backstage ecosystem against which an integration can collect facts
stringifyEntityRef@backstage/catalog-modelConverts an Entity to a string reference (e.g., component:default/my-service). Used when sending the entire entity is undesired
api@pagerduty/pdjsPagerDuty API client for making API calls
Config, ConfigReader@backstage/configUsed to parse the JSON configuration passed to setConfig()
CollectionConfig, Fact, FactRef@spotify/backstage-plugin-soundcheck-commonCore Soundcheck types for facts and collection configuration
ConfigurableFactCollector@spotify/backstage-plugin-soundcheck-nodeThe interface your collector must implement

Constants:

ConstantValuePurpose
ID'example-pagerduty'Unique identifier for this integration
SCOPE'default'Reserved word in Soundcheck—use 'default' unless you need branch-specific scoping (like the SCM integration)
SERVICE_FACT_REFERENCE'example-pagerduty:default/service'The type of fact returned by this integration, in collector:scope/factName format
SERVICE_DETAILS[...]Fields to request from the PagerDuty API

Step 3: Register the Module

Update the module.ts file in your backend module to register the fact collector:

src/module.ts
import {
createBackendModule,
coreServices,
} from '@backstage/backend-plugin-api';
import { factCollectionExtensionPoint } from '@spotify/backstage-plugin-soundcheck-node';
import { ExamplePagerDutyFactCollector } from './ExamplePagerDutyFactCollector';

export const soundcheckModuleExamplePagerduty = createBackendModule({
pluginId: 'soundcheck',
moduleId: 'example-pagerduty',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
collectorsExtension: factCollectionExtensionPoint,
},
async init({ logger, collectorsExtension }) {
collectorsExtension.addFactCollector(
ExamplePagerDutyFactCollector.create(logger),
);
},
});
},
});

This code creates a Backstage backend module that extends the Soundcheck plugin. When registered:

  1. The module declares a dependency on factCollectionExtensionPoint—an extension point exposed by Soundcheck that allows external modules to register custom fact collectors.
  2. During initialization, it calls addFactCollector() to register our ExamplePagerDutyFactCollector with Soundcheck's fact collection system.
  3. Once registered, Soundcheck manages the collector's lifecycle, including scheduling fact collection based on the frequency you define in app-config.yaml.

Step 4: Configure Your Integration

4.1 Add the collector configuration

Add the following to your app-config.yaml:

app-config.yaml
soundcheck:
collectors:
example-pagerduty:
token: ${PAGERDUTY_TOKEN} # Your PagerDuty API token
# Default settings for all collect configurations
frequency:
minutes: 5
initialDelay:
seconds: 15
filter:
kind: Component
spec.lifecycle: production
# What facts to collect
collects:
- type: Service
📖 Advanced: Multiple collection schedules

You can define multiple collection schedules with different filters and frequencies:

app-config.yaml
soundcheck:
collectors:
example-pagerduty:
token: ${PAGERDUTY_TOKEN}
frequency:
minutes: 5
filter:
kind: Component
spec.lifecycle: production
collects:
# Collect every 5 minutes (uses defaults above)
- type: Service
# Collect every 30 minutes for specific tags
- type: Service
frequency:
minutes: 30
filter:
kind: Component
metadata.tags: ['critical-service']

4.2 Annotate your entities

Add the PagerDuty service annotation to entities that should be tracked:

catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
annotations:
pagerduty.com/service-id: PXXXXXX # Your PagerDuty service ID
spec:
type: service
lifecycle: production
owner: team-a

Step 5: Create Checks and Tracks

Add checks and tracks to your app-config.yaml under the same soundcheck: key you configured in Step 4.

5.1 Define a check

Add a checks section to use the collected facts:

app-config.yaml
soundcheck:
# ... collectors config from Step 4 ...
checks:
- id: requires_service_status_to_be_active
description: Requires PagerDuty service status to be active
passedMessage: ✅ Service is active
failedMessage: ❌ Service is not active
rule:
factRef: example-pagerduty:default/service
path: $.status
operator: equal
value: active
Why no schedule on this check?

Notice this check has no schedule property. This is intentional—Soundcheck automatically triggers any check when the facts it depends on are collected.

Since this check references example-pagerduty:default/service in its factRef, it will run automatically whenever our ExamplePagerDutyFactCollector collects the service fact. The collector runs on the schedule we defined in Step 4 (e.g., every 5 minutes), so the check runs on that same cadence.

This is the recommended approach: schedule fact collection, not checks. It ensures checks always evaluate fresh data and respects the rate-limiting settings configured for the collector.

5.2 Create a track

Add a tracks section to group your checks:

app-config.yaml
soundcheck:
# ... collectors and checks config from above ...
tracks:
- id: pagerduty_track
name: PagerDuty
ownerEntityRef: group:default/platform-team # Update with your team
description: >
Ensure components are properly configured for incident management.
filter:
catalog:
spec.lifecycle: 'production'
levels:
- ordinal: 1
name: Incident Readiness
description: Basic PagerDuty integration checks
checks:
- id: requires_service_status_to_be_active
name: Service Status Active
description: Verifies the PagerDuty service is active

Step 6: Verify Your Integration

  1. Start your Backstage instance
  2. Navigate to SoundcheckIntegrations
  3. Confirm your custom integration appears in the list

Once the collector runs (based on your configured frequency), you'll see the checks execute:

PagerDuty Track

After facts are collected, the track shows pass/fail status for your entities:

PagerDuty Track with Data

🎉 Congratulations! Your custom PagerDuty integration is now running.


Next Steps

  • Create a No-Code UI for your integration to enable configuration through the Soundcheck interface