Skip to main content

Plugin discovery

Discovery is the process by which the registry locates plugins on the file system. The implementation rests on the file-based model declared in ADR-0006: a plugin is a folder with a dagstack.toml. Everything else is an implementation detail.

Directory layout

my-app/
├── plugins/
│ ├── openai/
│ │ ├── dagstack.toml
│ │ └── plugin.py
│ ├── qdrant/
│ │ ├── dagstack.toml
│ │ └── plugin.py
│ └── echo/
│ ├── dagstack.toml
│ └── plugin.py
└── main.py

Every folder containing a dagstack.toml becomes one plugin. Nested plugin folders are not supported — a plugin cannot live inside another plugin.

Calling discovery

import asyncio
import logging
from dagstack.plugin_system import PluginContext, PluginRegistry


async def main() -> None:
registry = PluginRegistry()
registry.discover("plugins/")

ctx = PluginContext(
config={},
logger=logging.getLogger("app"),
registry=registry,
)
await registry.setup_all(ctx)


asyncio.run(main())

Relative paths are resolved against the process's current working directory. In production we recommend passing an absolute path or a path relative to a known application root.

Discovery rules

  1. The walk is recursive from the given folder downwards. Depth is unlimited.
  2. Every folder is checked for dagstack.toml. If the file is present, the folder becomes a plugin and its contents are not scanned further (nested plugins are forbidden).
  3. Folders without dagstack.toml are traversed transparently. This lets you group plugins into thematic subfolders (plugins/data-sources/, plugins/llm/).
  4. Plugins sharing the same kind+name pair raise AmbiguousPlugin. The name must be unique within a kind.

Ignored paths

By default discovery skips system and housekeeping folders:

__pycache__/
node_modules/
.git/
.venv/
venv/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.tox/
dist/
build/

The full list lives in the DEFAULT_IGNORE constant. To add your own rules pass the ignore parameter:

from dagstack.plugin_system import PluginRegistry
from dagstack.plugin_system import DEFAULT_IGNORE

registry = PluginRegistry()
registry.discover(
"plugins/",
ignore=[*DEFAULT_IGNORE, "experimental/*", "*.draft"],
)

Patterns support glob syntax (*, **, ?).

Discovering multiple roots

If the application combines plugins from several independent folders (for example, core plugins next to main.py and user plugins in ~/.config/my-app/plugins/), run discover against each root and merge the results:

:::warning merge is planned for Phase 2 Today the Python binding accepts a single discovery root per PluginRegistry. Combining several roots — and the matching merge / override semantics described below — is planned for Phase 2. For now, lay your plugins out under one discovery root and call registry.discover("plugins/") once. :::

When merge ships in Phase 2, conflicts at the kind+name level surface as AmbiguousPlugin. For the "user overrides a system plugin" case the explicit override mechanism (described below) is the planned path.

Overriding plugins (Phase 2)

Sometimes you need to swap a plugin out for testing or local development. The Phase 2 release will add an explicit override flow that takes a second registry and lets entries from it replace those of the first without raising AmbiguousPlugin. Until then, point a separate PluginRegistry() at the fixture folder and use it directly in the test:

:::warning override is planned for Phase 2 Today the Python binding does not provide an explicit override flow — discovery is single-rooted. To swap a plugin out for tests, point a test-only registry at a fixture folder (registry.discover("tests/fixtures/plugins/")) and use it directly. The conflict-aware override API lands together with merge in Phase 2. :::

When override ships, conflicts will not be treated as errors — entries from the second registry replace entries from the first.

Performance

For large directories (≥1000 plugins) the walk takes noticeable time. Recommendations:

  • Run discover once at application start-up and cache the result.
  • Use ignore to skip folders known to contain no plugins (tests, docs).
  • If the plugins are known up front, pass an explicit list of paths instead of a recursive walk.