Build your first plugin¶
This tutorial creates atlas.hello-atlas: a deliberately small full-stack
plugin. It contributes one page and navigation item, but owns no catalog data,
permission, configuration, or runtime registry. That keeps the first result
visible while showing the two declarations every full-stack plugin needs.
Prerequisites and outcome¶
Start from a checkout that can run the local development guide. Read Choose an extension mechanism first: this tutorial chooses a frontend Contribution because it adds a page, not an Entity Kind or Facet.
If your plugin needs to attach data to an existing catalog entity, choose a Facet instead. The Database Schema architectural case study shows why that choice keeps plugin-owned data independent of the owning kind.
You will create the files in the checked-in first-plugin example bundle. Its Python descriptor and focused test are authoritative, source-backed examples; the page quotes only the short declarations that the bundle owns. At the end, composition accepts the selected plugin, its descriptor test passes, and Hello Atlas is visible in the running application.
1. Create the two package declarations¶
Copy the bundle into your plugin workspace, preserving the backend Python package and frontend source boundary. A real plugin package also needs normal Python and npm package metadata so the build can install it; use Choose a plugin layout for those package-level responsibilities.
Create backend/atlas_plugin_hello/plugin.py:
from atlas_plugin_api import PluginDescriptor
PLUGIN = PluginDescriptor(
id="atlas.hello-atlas",
version="0.1.0",
compatibility={"atlasCore": ">=0.1 <1"},
django_apps=(),
entry_point="atlas_plugin_hello.plugin:PLUGIN",
)
The descriptor is static: it can be read before Django starts. This first
plugin has no models, routes, permissions, capabilities, or jobs, so it has no
django_apps, requires_plugins, or register_runtime() hook. Add those only
when the behavior needs their contract.
Create frontend/src/index.ts:
import { defineFrontendPlugin, navItem, route, routeRef } from '@atlas/plugin-api'
import { HelloAtlasPage } from './HelloAtlasPage'
export const helloAtlasPlugin = defineFrontendPlugin({
id: 'atlas.hello-atlas',
contributions: [
route({ id: 'atlas.hello-atlas.home', path: '/hello-atlas', component: HelloAtlasPage }),
navItem({ id: 'atlas.hello-atlas.nav', title: 'Hello Atlas', route: routeRef('atlas.hello-atlas.home') }),
],
})
Then make HelloAtlasPage render a heading, for example
<h1>Hello Atlas</h1>. Contribution ids are globally unique in a distribution;
the composer rejects a duplicate id, duplicate route, or a route that conflicts
with Core. The frontend declaration is data at module load time. Do not mutate a
global registry or import a sibling plugin's source.
For a production-sized example of detail-page contributions, see Decision 2
in the Database Schema case
study.
It has two tabs and makes their applicability a capability predicate rather
than a hard-coded Resource check.
2. Select it in a development distribution¶
Add backend and frontend artifacts under the same id and version in the
development manifest. workspace is the source type the checked-in composer
can resolve:
plugins:
- id: atlas.hello-atlas
version: 0.1.0
backend:
package: atlas-plugin-hello-atlas
source: workspace
frontend:
package: "@atlas/plugin-hello-atlas"
source: workspace
Keep this entry next to the package paths that make the artifacts importable to the composer and available to the frontend workspace. Do not add the tutorial plugin to the checked-in default distribution: it is a learning fixture, not a product feature.
Resolve and validate your manifest (replace the paths with your development distribution; do not overwrite the checked-in default lock):
uv run --project composer atlas-compose resolve path/to/development-manifest.yaml -o /tmp/hello-atlas.lock.yaml
uv run --project composer atlas-compose validate path/to/development-manifest.yaml /tmp/hello-atlas.lock.yaml
Both commands should finish successfully. A missing package, identity/version mismatch, duplicate route, or contribution-id collision is a composition error; fix the declaration rather than bypassing validation. See Fix composition errors for the failure category and correction.
3. Test the backend declaration¶
The bundle test imports the exact descriptor file and asserts its composition
metadata. From core/backend, run:
uv run pytest ../../docs-site/examples/first-plugin/first-plugin-backend/tests -q
uv run ruff check ../../docs-site/examples/first-plugin/first-plugin-backend
uv run ruff format --check ../../docs-site/examples/first-plugin/first-plugin-backend
The focused test, lint, and format checks should pass. Keep an equivalent focused test in your plugin package; a descriptor that only appears in prose cannot prove that it remains importable or names the right entry point.
4. Run and visibly verify it¶
Generate your development composition inputs from the validated lock, then
start Atlas with the local development commands.
Open http://localhost:5173/hello-atlas, or select Hello Atlas in the
navigation. The page heading confirms that the selected frontend package was
composed, its route was accepted, and the running UI rendered its contribution.
If the page is absent, first check that the frontend artifact was selected and the generated composition inputs were rebuilt. If the route fails to compose, use the error's contribution or route id with the composition-error guide. If the backend cannot start, ensure the descriptor's package and entry point are importable before changing application settings.
What to add next¶
This plugin intentionally does not persist data or expose an API. Add an Entity Kind or Facet only when the plugin owns catalog data; use Extension points and capabilities for cross-plugin collaboration. The Database Schema architectural case study shows a real Facet plus capability-gated detail-page contributions.