Install
$ agentstack add skill-bendaamerahmed-backstage-idp-plugin-backstage-catalog ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Backstage Software Catalog
Model entities correctly, ingest them from external systems without destroying data, and diagnose what the catalog actually believes.
Preconditions
- Release line from
backstage.json; catalog packages resolved viayarn why @backstage/plugin-catalog-backend. - Backend generation:
packages/backend/src/index.tsusingcreateBackend()+backend.add(import('@backstage/plugin-catalog-backend'))is the new backend system. ACatalogBuilderinpackages/backend/src/plugins/catalog.tsis the legacy backend — migrate it (backstage-plugin-migrate) before adding modules, or register through the builder and say so in your report. - Exact interface shapes (
EntityProvider,EntityProviderConnection,CatalogProcessor,DeferredEntity,processingResult) read from the installed@backstage/plugin-catalog-nodetypes, not from memory. - A running local backend or a reachable catalog base URL, plus a token if auth is enforced, before any debugging step.
Procedure
- Read the catalog's current belief before changing anything. Query the API rather than guessing:
GET /api/catalog/entities/by-query?filter=kind=component&fields=metadata.name,metadata.annotations— what exists and where it came from.POSTto the same path for$all/$any/$not/$exists/$inpredicates.GET /api/catalog/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true— the orphan set.GET /api/catalog/entity-facets?facet=kind— a kind census, fastest way to spot a whole integration that stopped ingesting.GET /api/catalog/locations— registered roots. Staticcatalog.locationsentries cannot be removed through this API.
- Model the entity before writing ingestion code. Envelope is
apiVersion+kind+metadata+spec.metadata.nameis 1–63 chars of alphanumerics separated by[-_.], unique per kind per namespace;metadata.namespacedefaults todefault. Kinds: Component, API, Resource, System, Domain, Group, User, Location, Template. Usemetadata.titlefor display strings that cannot be a valid name.metadata.uidis output-only — never reference entities by uid. - Write entity refs as
[:][/], lowercased. Kind and namespace default from context (spec.ownerdefaults to Group-ish org kinds,providesApistoapi, namespace to the referring entity's). Produce refs withstringifyEntityReffrom@backstage/catalog-modeland parse withparseEntityRef; compare case-insensitively. Never hand-build refs with string concatenation across namespaces. - Express relations through spec fields, never by hand. Processors emit relations from the spec; stitching merges incoming and outgoing edges into the final entity.
relationsandstatuswritten into a descriptor are discarded.
spec.owner→ownedBy/ownerOf. This is the whole of ownership resolution: one owner ref per entity, normally a Group.spec.system,spec.domain,spec.subcomponentOf→partOf/hasPart.spec.providesApis,spec.consumesApis→providesApi/apiProvidedBy,consumesApi/apiConsumedBy.spec.dependsOn→dependsOn/dependencyOf;spec.memberOf→memberOf/hasMember;spec.parent,spec.children→parentOf/childOf.
- Choose the ingestion mechanism deliberately.
- External system, scheduled or webhook-driven, fits in memory → EntityProvider.
- Enrichment, custom-kind validation, or a custom file format already inside the processing loop → CatalogProcessor.
- Paginated source too large to hold in memory (100k+ records) → incremental entity provider from
@backstage/plugin-catalog-backend-module-incremental-ingestion.
Processors cannot delete entities; providers can, eagerly. That asymmetry decides most cases. Before writing anything, check whether a built-in or @backstage-community/plugin-catalog-backend-module-* provider already covers the source (backstage-repo-discovery).
- Scaffold rather than hand-roll:
yarn new --select catalog-provider-moduleoryarn new --select catalog-processor-module. Both generate aplugins/catalog-backend-module--*package with the class,readProviderConfigs, schedule wiring,config.d.ts, tests, and amodule.tsregistered frompackages/backend/src/index.ts. - Wire the module against the right extension point in
createBackendModule({ pluginId: 'catalog', moduleId: ... }):
catalogProcessingExtensionPoint(@backstage/plugin-catalog-node) →addEntityProvider(...),addProcessor(...).catalogModelExtensionPoint(@backstage/plugin-catalog-node/alpha) →setEntityDataParser(...)for non-catalog-info.yamlformats,setFieldValidators(...)for envelope/metadata rules.incrementalIngestionProvidersExtensionPoint→addProvider({ provider, options }).
Confirm the method names against the installed package's .d.ts before writing the call.
- Make the provider identity stable.
getProviderName()names the provider's private entity bucket in the database and must be unique and unchanged across restarts and deploys. Renaming it abandons the old bucket; with the defaultorphanProviderStrategythose entities are deleted. - Stamp every emitted entity with
ANNOTATION_LOCATION(backstage.io/managed-by-location) andANNOTATION_ORIGIN_LOCATION(backstage.io/managed-by-origin-location), both in:form (targets may contain colons — never split on the first one). Entities missing these are dropped at ingestion with only a warning log. - Pick the mutation type.
type: 'full'replaces the whole bucket — correct when you can batch-fetch the complete set, and only then.type: 'delta'withadded/removedis correct for webhook and event streams, where you never see the whole set. Do not emit afullmutation built from a partially successful fetch; let the task throw and retry on the next schedule instead. - Set
locationKeyon everyDeferredEntityto a string identifying the provider instance (e.g.frobs-provider:${id}), and keep it constant. On a duplicate entity ref the catalog resolves:
- existing entity has no location key → the incoming entity wins and takes it over;
- existing key matches the incoming key → update;
- existing key differs → the incoming entity is discarded, silently.
This is the only defence against one provider taking over another's entities, so an entity emitted without a locationKey is permanently up for grabs.
- Handle upstream pagination and rate limits in the provider, not the processor. Schedule via
scheduler.createScheduledTaskRunnerwith afrequency/timeoutread fromcatalog.providers..schedule. For incremental providers, tuneburstLength,burstInterval,restLength,backoff, and setrejectEmptySourceCollections: trueplusrejectRemovalsAbovePercentageso a degraded upstream cannot delete the catalog. - In processors, do no network I/O. Every processor runs on every entity every cycle. If you must call out, use the
CatalogProcessorCachepassed intopreProcessEntity/postProcessEntitywith an ETag andIf-None-Match, and bump the cache key string whenever the cached shape or the processor logic changes. - Implement processor methods for their actual stage, all of which run on every entity on every cycle:
preProcessEntity— enrichment, before validation. Filter by kind first; skip when the field already has a value so acatalog-info.yamlcan override you.validateEntityKind—truefor a kind you own and validated,falsefor a kind you do not recognise (passing it to other processors), throw to mark the entity invalid. Build it fromentityKindSchemaValidator(schema)over a JSON schema exported from an isomorphic*-commonpackage so frontend and backend share it.postProcessEntity— emit relations and child entities viaprocessingResult.relation/.entity/.location, errors via.generalError/.inputError/.notFoundError.readLocation— only for genuinely new location types; prefer a provider.
- Register new kinds in config. If
catalog.ruleshas anallowlist, add the kind or nothing will be ingested. Usecatalog.processorOptions..priority(default20, lower runs earlier) when order matters — registration order is only guaranteed within a single module. - Run locally and prove the loop:
yarn start-backend, then trigger the provider's schedule and re-query the endpoints from step 1.
Verification
yarn tscandyarn testfrom the repo root;yarn backstage-cli config:check --laxif you addedconfig.d.ts.- Descriptor sanity without registering:
POST /api/catalog/validate-entitywith{ entity, location }, orPOST /api/catalog/locations?dryRun=truewith{ type: 'url', target: ... }. - Entity present and final:
GET /api/catalog/entities/by-name///returnsrelationspopulated and nometadata.annotations['backstage.io/orphan']. - Processing errors: same response's
status.items— empty means the last processing pass was clean. - Provenance:
metadata.annotations['backstage.io/managed-by-location']must name your provider for entities you own. A different value means another source won the ref. - Parentage:
GET /api/catalog/entities/by-name////ancestryshows which root keeps the entity alive. - Force a cycle instead of waiting:
POST /api/catalog/refreshwith{ entityRef }; for incremental providersPOST /api/catalog/incremental/providers/:provider/triggerandGET /api/catalog/incremental/providers/:providerfor state.
Failure modes
- Entities vanish after a refresh. A provider emitted
type: 'full'from a failed, partial, or empty upstream response. Providers delete eagerly: the bucket entity and the entire subtree processed out of it go immediately. Fix the provider to throw on partial reads; recovery is a successful re-run, not a manual re-import. - Everything from one integration disappeared after a deploy.
getProviderName()changed, or the provider was removed from the backend. Its bucket is now an orphaned provider and is deleted by default. Restore the exact old name, or setcatalog.orphanProviderStrategy: keepbefore the deploy. backstage.io/orphan: 'true'appears. A parent stopped emitting the child — a movedcatalog-info.yaml, a removedtargetfrom aLocation, or a crawler that no longer finds the source. The defaultorphanStrategydeletes these;keepretains them with the annotation. Use/ancestryto find the severed parent. A file that is unreadable or corrupt does not orphan — it surfaces as a hard error instatus.items.- Deleting an entity in the UI does nothing. An active parent re-emits it on the next processing cycle. Only genuinely orphaned entities stay deleted; otherwise remove the registration root — which also removes every entity under it.
- Two sources claim the same entity ref. Whoever wrote it first with a
locationKeykeeps it; the loser is discarded with no visible error. Comparemanaged-by-locationacross the two sources. Separately, twocatalog-info.yamlfiles with the samemetadata.namemean one is processed and the rest are skipped with a log line only. If the correct owner is not determinable from the repo, return a BLOCKED report naming both sources. - Entity never appears at all. In order of likelihood: missing
managed-by-location/managed-by-origin-locationannotations; kind excluded bycatalog.rules; no processor returnedtruefromvalidateEntityKindfor a custom kind;catalog.readonly: trueblocking API registration. - No errors in the logs. Catalog processing errors stopped being logged by default (Backstage v1.26.0 /
@backstage/plugin-catalog-backendv1.21.9). Readstatus.itemson the entity, placeEntityProcessingErrorsPanelon the entity page, or subscribe to catalog error events through@backstage/plugin-events-backend— the durable fix when operators keep asking why an entity is stale (backstage-incident-debug). - The processing loop falls behind. A processor doing synchronous HTTP, or a
fullmutation dumping a huge bucket at once and flooding the queue. Move I/O to a provider, or switch to incremental ingestion.catalog.processingIntervalis a suggested minimum only — raising it will not fix a blocking processor. - An entity re-stitches every cycle for no reason. The entity hash covers body, relations, errors, referred entities and parents — including array order, so a source that returns
metadata.tagsin shifting order churns the catalog. Sort arrays in the provider. - A custom processor works locally, not in the deployed backend. Cross-module processor ordering follows module load order. Set
getPriority()orcatalog.processorOptions..priority.
Do not
- Do not
applyMutationwhen the upstream fetch was incomplete, and never emitfullfrom an event handler. - Do not change
getProviderName()orlocationKeyon a populated catalog without an explicit stop-and-get-authorization step — both destroy entities. - Do not call
DELETE /entities/by-uid/...,DELETE /locations/..., or the incrementalcleanupendpoint against a shared or production catalog without explicit authorization. - Do not reference entities by
metadata.uid, or split location strings on the first colon. - Do not hand-write
relations,status, orbackstage.io/orphaninto a descriptor. - Do not invent annotation keys under the reserved
backstage.io/prefix; use your own domain prefix. - Do not loosen
setFieldValidatorsto import legacy names without checking for colons, slashes, and URL-unsafe characters — plugins assume they never occur. - Do not make processors that fetch remote data or that mutate entities they do not filter by kind.
- Do not use
type: filelocations for anything but local development and examples.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: bendaamerahmed
- Source: bendaamerahmed/backstage-idp-plugin
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.