Backend collaboration¶
Use a backend collaboration contract when one plugin needs another plugin's behavior or needs to participate in an owner-controlled workflow. Do not import a sibling plugin's models, ORM queries, views, or private modules. The owning plugin keeps its data and business rules; consumers depend on a small published contract instead.
Audience, prerequisites, and outcome¶
This guide is for backend plugin authors who have chosen a plugin layout, understand the extension decision guide, and can run composition checks for their distribution. It describes a narrow, versioned contract with a clear provider, consumer, availability behavior, and descriptor dependency.
Choose a kind capability when a consumer needs to know what an entity kind supports. Choose a backend service contract when a consumer needs to call behavior at runtime; these are different uses of the word “capability”.
Choose the narrowest contract¶
| Need | Use | Current Atlas example |
|---|---|---|
| Target compatible entity kinds semantically | Entity Kind provides capability |
C4 targets architecture.subject.v1, not a list of kind ids. |
| Resolve an optional named runtime service | Backend capability registry | The registry and typed result exist; no built-in plugin currently publishes a general service capability. |
| Let multiple implementations register under an owner-defined key | Plugin-owned keyed extension point | Ingestion resolves connectors and parsers by id. |
| Let each interested plugin take part in one Core workflow | Core-owned registry or direct contract | Plugins register one purge-reference scanner; Core runs every scanner during Purge. |
| Call a small Core-owned operation | Published API wrapper | Core binds entity helper implementations into atlas_plugin_api during runtime setup. |
Do not create an extension point for a one-off lookup. Conversely, do not add a direct import merely because a contract currently has one consumer: the provider's models and implementation remain private.
Register after Django setup¶
Static descriptor metadata is imported before Django starts. Runtime
registration happens only from register_runtime(), after django.setup().
The selected, active descriptors run this hook; a disabled plugin skips it, so
it contributes no kinds, permissions, or extension implementations.
from atlas_plugin_api import PluginDescriptor, register_purge_scanner
PLUGIN = PluginDescriptor(
id="atlas.inventory",
version="0.1.0",
compatibility={"atlasCore": ">=0.1 <1"},
django_apps=("atlas_plugin_inventory",),
entry_point="atlas_plugin_inventory.plugin:PLUGIN",
requires_plugins={"atlas.standard-catalog": ">=0.1 <1"},
)
def register_runtime() -> None:
from .purge import scan_inventory_references
register_purge_scanner(PLUGIN.id, scan_inventory_references)
Keep import-time modules declarative. Loading models, resolving a sibling service, or mutating a registry during import makes static composition and startup ordering unsafe. Duplicate registrations are errors: give every registration a stable, plugin-owned id rather than overwriting another provider.
Capabilities and unavailable providers¶
An Entity Kind declares stable, versioned strings in provides; a consumer
uses resolve_capability(kind_id, capability_id) when it must distinguish an
absent kind provider from a present kind that simply lacks the capability. Its
result is one of Ok(value), Unavailable(), or Error(reason). Handle all
three cases explicitly:
from atlas_plugin_api import Error, Ok, Unavailable, resolve_capability
result = resolve_capability(entity.kind, "architecture.subject.v1")
match result:
case Ok(True):
render_diagram(entity)
case Ok(False) | Unavailable():
return # This entity cannot participate, or its provider is inactive.
case Error(reason):
logger.warning("Capability lookup failed: %s", reason)
Unavailable is not permission denial and not a signal to reach into the
missing plugin's tables. Preserve a safe feature state, explain the absence if
the user needs an action, and let an operator restore a compatible plugin.
For the lifecycle effect on entities, see Entity kinds & facets.
Publish an extension point¶
An extension-point owner defines the identifier, contract, cardinality, keys,
and duplicate behavior. Put its registry with the owning plugin unless Core
needs to call every registered implementation. Use namespaced, versioned ids;
Ingestion's current keyed points are atlas.ingestion.connectors.v1 and
atlas.ingestion.parsers.v1.
For a keyed point, one key maps to one implementation. The owner resolves the
implementation and treats a missing key as a normal unavailable integration.
Two implementations claiming the same key must fail, rather than leaving the
choice to import order. Ingestion's built-in Git connector and
catalog-info.yaml parser register in its runtime hook, and the pipeline
resolves them through the point instead of calling a hard-coded class.
Atlas does not currently ship a general public helper for creating arbitrary extension points. Follow the source-backed Ingestion pattern only when your plugin truly owns a multi-implementation contract, and document its identifier, version, cardinality, allowed keys, owner, and error behavior. A general registry without a selected consumer is not a supported integration surface.
Direct contracts and failure isolation¶
Use a direct public wrapper for a narrow Core operation, or an owner-managed registry for a workflow that must query all participants. For example, the purge scanner is registered once per plugin and Core invokes every scanner in the purge transaction. A scanner must self-filter entities it does not own, return precise blocking references, and never silently delete another plugin's data.
The Core entity helpers follow the same boundary from the other direction:
Core binds its implementations during register_runtime() and plugins call
the atlas_plugin_api wrappers. Calling such a wrapper before its provider
has registered is a startup error, not an invitation to import server.
Isolate failures at the contract boundary:
- return or surface an explicit unavailable result when an optional provider is absent;
- make duplicate ids and keys fail composition or startup with the competing owners;
- keep exceptions actionable and scoped to the participating plugin; and
- do not let a disabled plugin keep a runtime contribution or scheduled work active.
For a state-changing collaboration, add the provider's backend permission check. Consumer-side UI gating only improves the user experience. See Permissions for the policy boundary.
Declare dependency direction¶
Use PluginDescriptor.requires_plugins only when the consumer cannot operate
without the provider. It maps a provider plugin id to a version range, and
composition rejects a selected distribution that omits the required provider or
contains a dependency cycle. For example, plugins that require Standard
Catalog declare {'atlas.standard-catalog': '>=0.1 <1'}.
Do not declare an optional collaboration as required merely to simplify a call. Check the capability or extension-point result and make the feature unavailable when necessary. Dependencies point from consumer to provider; never reverse that direction to force plugin load order.
Verify and troubleshoot¶
- Unit-test the provider contract, including each valid key or capability.
- Test the consumer with the provider selected, absent, and disabled. Assert the expected unavailable or error state without importing provider internals.
- Test duplicate registration and assert that the reported id/key and owners identify the correction.
- Run the distribution composition preflight after changing a descriptor.
| Symptom | Likely cause | Safe correction |
|---|---|---|
Unavailable() result |
Provider is not selected or is disabled | Render or return the documented unavailable state; select or enable a compatible provider if the feature is required. |
| Duplicate registration | Two active plugins claim one contract id or key | Give the new registration a plugin-owned id/key; never replace the existing registration. |
| Wrapper raises before serving requests | A published Core contract was called before runtime registration | Move the call out of import/setup code and use the public wrapper after Atlas starts. |
| Composition rejects a dependency | A required provider is absent or the graph cycles | Correct requires_plugins at the consumer and keep optional edges runtime-safe. |
Next, use Plugin contract reference for exact Python symbols, Entity kinds & facets for entity capabilities, and Assembling a distribution to validate the selected plugin set.