Quick start
dagstack/plugin-system is a generic plugin registry for any application that needs pluggable extension points. Typical scenarios: data sources, event handlers, notification channels, authentication providers, pipeline steps, integrations with external systems. Plugins are discovered in a directory by their manifests, loaded into a registry, and called through one of five dispatch classes (singleton, broadcast-collect, broadcast-notify, chain, capability).
:::info Release status
- Python:
dagstack-plugin-systemis being prepared for PyPI; today it ships from an internal repository. - TypeScript:
@dagstack/plugin-system0.1.0-rc.2ships spec-emitted types only (VERSION,ToolV1,OrchestratorV1); the full runtime lands in Phase 1. - Go:
go.dagstack.dev/plugin-systemv0.1.0-rc.1is published as a release candidate (Phase 1 in-process surface). :::
Installation
:::warning Snippets show the planned high-level API
The TabItem snippets across this site illustrate the language-agnostic contract from ADR-0001..0006. Concrete bindings ship the same contract under slightly different names: Python today exposes discover(registry, path) rather than the convenience discover(path) -> Registry shown here, and Go uses Discover() returning []ManifestEntry plus a separate NewRegistry() + RegisterManifest() loop. For exact, today-runnable signatures see the per-binding API reference.
:::
- Python
- TypeScript
- Go
pip install dagstack-plugin-system
npm install @dagstack/plugin-system
go get go.dagstack.dev/plugin-system
Your first plugin
A minimal working plugin consists of two files in a dedicated folder: a dagstack.toml manifest and an implementation module.
Plugin manifest
The manifest declares the kind, name, runtime, the compatible core version (core_version, PEP 440 specifier), and the entry_point that the loader resolves to a Python class.
[plugin]
schema_version = "1"
name = "echo"
kind = "tool"
kind_api_version = "1"
core_version = ">=0.1.0,<1.0.0"
runtime = "in_process"
license = "Apache-2.0"
entry_point = "plugin:EchoTool"
The entry_point resolves to plugins/echo/plugin.py and the class EchoTool inside it (ADR-0006 §2 — modules load into the isolated dagstack._discovered.<name>.<module> namespace).
Plugin implementation
- Python
- TypeScript
- Go
from dagstack.plugin_system import PluginContext
class EchoPlugin:
"""Returns the input message wrapped under the `echoed` key.
Mirrors the canonical `examples/echo_plugin.EchoTool` shipped
with the binding. Both sync and async `setup` / `teardown` are
accepted by the registry; the published example uses sync.
"""
def setup(self, context: PluginContext) -> None:
self._ctx = context
def execute(self, args: dict) -> dict:
return {"echoed": args["msg"]}
def teardown(self) -> None:
self._ctx = None
:::warning TypeScript runtime ships in Phase 1
@dagstack/plugin-system@0.1.0-rc.2 exports only the spec-emitted types — VERSION, ToolV1, OrchestratorV1. The runtime (PluginRegistry, discover, dispatchers, contract suite) lands in Phase 1. Today: implement the kind contract against the published types, then host plugins through Python over mcp_stdio or wait for the Phase 1 release. See the TypeScript API reference for the planned shape.
:::
package echo
import (
"context"
pluginsystem "go.dagstack.dev/plugin-system"
)
type EchoPlugin struct{}
func (p *EchoPlugin) Unwrap() any { return p }
func (p *EchoPlugin) Setup(ctx context.Context, pluginCtx *pluginsystem.PluginContext) error {
return nil
}
func (p *EchoPlugin) Invoke(payload string) (string, error) {
return payload, nil
}
func (p *EchoPlugin) Teardown(ctx context.Context) error {
return nil
}
Discover and load the registry
Construct a PluginRegistry, then call registry.discover(path) to walk a directory recursively, find every folder that contains a dagstack.toml, validate the manifests, and load the plugins into the registry. The setup_all(ctx) method initialises each plugin while honouring inter-plugin dependencies (topological sort).
- Python
- TypeScript
- Go
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)
for manifest in registry.list_manifests():
print(manifest.kind, manifest.name)
await registry.teardown_all()
asyncio.run(main())
:::warning TypeScript runtime ships in Phase 1
@dagstack/plugin-system@0.1.0-rc.2 exports only the spec-emitted types — VERSION, ToolV1, OrchestratorV1. The runtime (PluginRegistry, discover, dispatchers, contract suite) lands in Phase 1. Today: implement the kind contract against the published types, then host plugins through Python over mcp_stdio or wait for the Phase 1 release. See the TypeScript API reference for the planned shape.
:::
ctx := context.Background()
entries, err := pluginsystem.Discover("plugins/")
if err != nil {
panic(err)
}
reg := pluginsystem.NewRegistry()
for _, entry := range entries {
plugin := buildPlugin(entry.Manifest) // user-defined factory
if err := reg.RegisterManifest(entry.Manifest, plugin); err != nil {
panic(err)
}
}
pluginCtx := &pluginsystem.PluginContext{
Logger: slog.Default(),
Registry: reg,
}
if err := reg.SetupAll(ctx, pluginCtx); err != nil {
panic(err)
}
defer reg.TeardownAll(ctx)
for _, m := range reg.ListManifests() {
fmt.Println(m.Kind, m.Name)
}
Calling a plugin
Plugins are retrieved from the registry by the kind + name pair. The returned object is a proxy that applies the dispatch rules, injects resources, and runs contract checks.
- Python
- TypeScript
- Go
echo = registry.get_plugin("tool", name="echo")
result = echo.execute({"msg": "hello"})
assert result == {"echoed": "hello"}
:::warning TypeScript runtime ships in Phase 1
@dagstack/plugin-system@0.1.0-rc.2 exports only the spec-emitted types — VERSION, ToolV1, OrchestratorV1. The runtime (PluginRegistry, discover, dispatchers, contract suite) lands in Phase 1. Today: implement the kind contract against the published types, then host plugins through Python over mcp_stdio or wait for the Phase 1 release. See the TypeScript API reference for the planned shape.
:::
p, err := reg.Resolve("echo")
if err != nil {
panic(err)
}
echo := p.Unwrap().(*echo.EchoPlugin)
result, _ := echo.Invoke("hello")
fmt.Println(result) // hello
What's next
Concepts — how the main components are wired:
- Plugin registry, Manifest, Discovery — the base objects.
- Plugin kinds, Runtimes, Dispatch — architectural concepts.
- Resources (Resources DI), Runtime invariants — behaviour across environments.
Guides — how to solve typical tasks:
Reference — exact tables for quick lookup:
Specification — normative architecture decisions:
- Overview, ADR-0001: Core architecture, ADR-0002: Hook invocation, and the rest.
API reference (generation deferred until the bindings reach stable):
- Python, TypeScript (roadmap), Go (roadmap).