Customization
DeerFlow is designed to be adapted. You can extend agent behavior by writing
custom middlewares, adding new tools, building skill packs, and replacing any
built-in component through the config.yaml use: field.
DeerFlow’s pluggable architecture means most parts of the system can be replaced or extended without forking the core. This page maps the extension points and explains how to use each one.
Custom middlewares
Middlewares are the primary extension point for adding behavior to the Lead Agent. They wrap every LLM turn and can read and modify the agent’s state before or after the model call.
To add a custom middleware:
- Implement the
AgentMiddlewareinterface fromlangchain.agents.middleware. - Register its import path under
extensions.middlewares, or pass an instance through an embedded SDK API.
from langchain.agents import AgentState
from langchain.agents.middleware import AgentMiddleware
from langgraph.runtime import Runtime
class AuditMiddleware(AgentMiddleware[AgentState]):
def before_model(self, state: AgentState, runtime: Runtime) -> dict | None:
print(f"[audit] model input has {len(state.get('messages', []))} messages")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict | None:
messages = state.get("messages", [])
last_message = messages[-1] if messages else None
print(f"[audit] last message type: {type(last_message).__name__ if last_message else 'none'}")
return NoneLifecycle hooks can return a dictionary of state updates, which LangChain merges
into the agent state; return None when observing only.
For an operator-managed deployment, the class must have a zero-argument constructor and be importable by the Gateway process:
extensions:
middlewares:
- my_company.deerflow_middlewares:AuditMiddlewareConfigured middleware is loaded after the built-in middleware and optional loop/token guards. On the lead-agent pipeline, it runs before the terminal-response, model-length, safety, and clarification tail; subagents have no terminal-response, model-length, or clarification stage, so configured middleware is followed by the optional safety guard, DurableContextMiddleware, optional SummarizationMiddleware, then SubagentDateContextMiddleware and SystemMessageCoalescingMiddleware. Treat these class paths as trusted configuration because loading one executes Python code. Embedded callers can instead use DeerFlowClient(middlewares=[AuditMiddleware()]), which builds the full lead-agent chain and places middleware before its terminal-response, model-length, safety, and clarification tail. create_deerflow_agent(extra_middleware=[AuditMiddleware()]) instead builds a smaller feature-based lead-agent chain; unanchored extras are placed immediately before ClarificationMiddleware (anchored extras follow their @Next/@Prev placement). Neither API forwards middleware to subagents.
Choose the registration path by ownership and placement. The fixed-slot (not deprecated) extensions.middlewares list is accepted in config.yaml and extensions_config.json (config.yaml wins) and applies to both lead and subagent pipelines. Packaged extensions registered through the top-level plugins: list contribute middleware at semantic extension points. Contributor code that needs committed, programmatic lead-only wiring can use build_middlewares(..., custom_middlewares=[AuditMiddleware()]).
For create_deerflow_agent, an @Next or @Prev anchor must name a middleware that this smaller chain actually contains; anchors from the full middleware list otherwise fail to resolve.
Custom tools
Add new tools to the agent by registering them in config.yaml under tools::
tools:
- use: mypackage.tools:my_custom_tool
api_key: $MY_TOOL_API_KEYYour tool must be a LangChain BaseTool or a function decorated with @tool. It will be instantiated using the use: class path and any additional fields from the config entry.
For community-style tools, the pattern is a module-level function or class that returns a BaseTool:
# mypackage/tools.py
from langchain_core.tools import tool
@tool
def my_custom_tool(query: str) -> str:
"""Search my custom data source."""
return do_search(query)Custom sandbox provider
The sandbox can be replaced by implementing the SandboxProvider interface:
from deerflow.sandbox.sandbox_provider import SandboxProvider
from deerflow.sandbox.sandbox import Sandbox
class MyCustomSandboxProvider(SandboxProvider):
def acquire(self, thread_id: str | None = None) -> str:
# Return a sandbox_id
...
def get(self, sandbox_id: str) -> Sandbox | None:
# Return the sandbox instance for this id
...
def release(self, sandbox_id: str) -> None:
# Cleanup
...Then reference it in config.yaml:
sandbox:
use: mypackage.sandbox:MyCustomSandboxProviderCustom memory storage
Replace the file-based memory with any persistent store by implementing MemoryStorage:
from deerflow.agents.memory.storage import MemoryStorage
from typing import Any
class RedisMemoryStorage(MemoryStorage):
def load(self, agent_name: str | None = None) -> dict[str, Any]:
...
def reload(self, agent_name: str | None = None) -> dict[str, Any]:
...
def save(self, memory_data: dict[str, Any], agent_name: str | None = None) -> bool:
...Configure it in config.yaml:
memory:
storage_class: mypackage.storage:RedisMemoryStorageCustom skills
Skills are the easiest extension point. Create a directory under skills/custom/your-skill-name/ with a SKILL.md file. The skill is discovered automatically on the next load_skills() call.
See Skills for the full directory structure and SKILL.md format.
Custom models
Any LangChain-compatible chat model can be used by specifying it in the use: field:
models:
- name: my-custom-model
use: mypackage.models:MyCustomChatModel
# Any extra fields are passed as kwargs to the constructor
base_url: http://my-model-server:8080
api_key: $MY_MODEL_API_KEYThe model class must implement the LangChain BaseChatModel interface.
Custom checkpointer
Thread state persistence can use any LangGraph-compatible checkpointer:
checkpointer:
type: sqlite
connection_string: ./my-checkpoints.dbFor custom backends, implement the LangGraph BaseCheckpointSaver interface and configure it programmatically when initializing the DeerFlowClient.
Guardrails
Add pre-execution authorization for tool calls through the guardrails: config:
guardrails:
enabled: true
provider:
use: deerflow.guardrails.builtin:AllowlistProvider
config:
denied_tools: ["bash", "write_file"]For custom guardrail logic, implement a class with evaluate() and aevaluate() methods and reference it via use:.