Skip to Content
DeerFlow

Extensions

🧩

An extension is an ordinary Python package that DeerFlow imports at Gateway startup. It depends only on the public deerflow-extension-api contract, so it can be built, tested, and released without importing DeerFlow itself.

Tools, MCP servers, and skills add capabilities the model can call. Extensions add behavior to the host: they observe every model and tool call, react when a run starts or stops, run background services next to the Gateway, and serve their own HTTP routes. An extension is the supported way to ship that kind of integration, such as audit logging, cost accounting, or a governance dashboard, without forking DeerFlow.

How to read this manual

ReaderStart with
Extension authorsThis page, Quick Start, Runtime Model, then the chapter for each contribution kind below
OperatorsOperating Extensions, Troubleshooting, Trust model
Looking up a nameReference
DeerFlow contributorsbackend/packages/harness/deerflow/extensions/AGENTS.md, which records the host-side design decisions

Should this be an extension?

Pick the lightest mechanism that does the job. Each row executes operator-trusted code, but the lower rows reach further into the runtime.

You want toUse
Give the model a new callable capabilityA custom tool (tools: in config.yaml) or an MCP server
Teach the model a workflow or domain procedureA skill
Add one AgentMiddleware class to every agent, configured in placeextensions.middlewares in config.yaml. See Customization
Ship a versioned package that observes runs, keeps state, runs a service, or serves routesAn extension (this manual)

The two middleware paths are easy to confuse. extensions.middlewares inserts a class at one fixed slot and imposes no contract. An extension middleware declares a semantic placement, runs inside a failure-isolating wrapper, and ships alongside the other contribution kinds below.

What an extension can contribute

install(registry, config) receives a write-only registry. Each registry method registers one contribution kind:

Registry methodContribution
registry.middlewares(contributor)AgentMiddleware instances inserted into the Lead Agent and subagent chains at a semantic placement. See Middleware Contributions
registry.task_lifecycle(contributor)on_task_start / on_task_stop for every lead run and every delegated subagent. See Lifecycle and Observers
registry.system_model_observer(obs)A snapshot of each model call DeerFlow makes for itself: goal evaluation, memory extraction, title generation, summarization. See Lifecycle and Observers
registry.agent_assembly_observer(obs)A descriptor of every assembled agent: model, prompt hash, tools, middleware stack, skills, and a fingerprint. See Lifecycle and Observers
registry.context_compaction_observer(obs)A CompactionEvent each time summarization removes messages from context. See Lifecycle and Observers
registry.service(service)An object started after the Gateway’s persistence layer is ready and stopped at shutdown. May read runs through the Run Evidence reader. See Services and Routes
registry.routers(routers)FastAPI routers mounted after every host route, behind Gateway authentication. See Services and Routes
registry.plugin(contribution)Experimental. Browser modules, authenticated backend actions, and model tools. Returns False on a host without plugin support. See Full-Stack Plugins

A single extension may register any combination. The bundled example  registers a middleware, a task-lifecycle contributor, a system-model observer, a service, and a router.

Installing and loading

Operators install extensions with the extension manager, which adds the package to the backend’s extensions dependency group, updates uv.lock, and writes one record under the top-level plugins: list in config.yaml:

plugins: - name: hello package: deerflow-extension-hello use: deerflow_extension_hello:install enabled: true required: false config: {}
FieldMeaning
useEntry point as module.path:install
enabledfalse skips the extension without importing it
requiredfalse (default): a load failure is logged and the Gateway starts without the extension. true: the Gateway refuses to start
configPrivate configuration passed verbatim to install() as its second argument (a shallow copy)

Loading happens exactly once, while the Gateway builds its application. The Gateway resolves each enabled entry in list order, checks the API version, and calls install(). If install() raises, the entries it registered are rolled back and the next extension loads normally. The Gateway logs Extensions loaded: N/M (...) when it finishes.

Because loading is startup-only, every change needs a Gateway restart: install, upgrade, enable, disable, remove, or a hand edit of plugins:. plugins: lives only in config.yaml, never in extensions_config.json, because the latter is writable through Gateway APIs and importing a package is code execution.

Failure model

Extensions are observational, so a broken extension degrades to a log line instead of a broken run:

  • A contribution that raises is skipped, and the Gateway logs an error attributed to its entry point (Extension <use>: ...).
  • Contributed middleware runs inside an isolating wrapper that never repeats a model call or tool side effect. See Failure isolation.
  • Task-lifecycle notifications share a bounded time budget, and observers are notified one by one: a failing observer does not skip the ones after it.

The single exception is required: true, which turns any load failure into a startup abort. Use it only when the deployment is wrong without the extension, because recovering from it needs shell access to the config file.

Trust model

An extension is not sandboxed. Its build hooks run during installation and its code runs inside the Gateway process with the Gateway’s privileges, including database access through the session factory handed to services. Install only sources you have reviewed and trust.

The extension manager asks for confirmation before installing, rejects source URLs with embedded credentials, and accepts only package requirements, HTTPS sources (including public Git over HTTPS), and local directories, which it copies as a snapshot. SSH Git URLs and local wheels are rejected. These checks prevent packaging accidents, not malicious code.

Versioning

The contract package is versioned separately from DeerFlow, and a host exposes its version as deerflow_extension_api.API_VERSION. This manual covers deerflow-extension-api 0.2.3.

  • Before 1.0, a minor release may break extensions and a patch release only adds. From 1.0 on, breaking changes bump the major.
  • Every Protocol method has a default implementation and every optional dataclass field has a default, so additive releases do not break already-released extensions.
  • Decorating install with @extension(api="0.2.0") declares the version you wrote against. The Gateway refuses the extension, with an actionable message, unless the host is the same 0.minor and at least the declared patch. Declare the lowest version whose features you use.

Declare the matching range in your package metadata as well, for example deerflow-extension-api>=0.2,<0.3.

Terminology

  • Host: the DeerFlow Gateway process that loads extensions.
  • Contribution: one object registered through the registry: a contributor, observer, service, router, or plugin.
  • Contributor: an object the host calls back to obtain contributions, such as a MiddlewareContributor that returns middleware for each agent it builds.
  • Scope: the lifetime a piece of state belongs to. The app scope lives as long as the Gateway; a task scope lives for one lead run or one subagent execution.
  • ExtensionData: the typed store attached to a scope, keyed by Python type so two extensions cannot collide.
  • Diagnostic: a load-time or run-time problem attributed to one extension and written to the Gateway log.