# How to Implement Custom Agent Nodes in ChatDev Workflow Graphs

> Learn to implement custom agent nodes in ChatDev workflow graphs by subclassing AgentConfig and NodeExecutor. Register your new nodes easily for extended functionality.

- Repository: [OpenBMB/ChatDev](https://github.com/OpenBMB/ChatDev)
- Tags: how-to-guide
- Published: 2026-04-01

---

**To implement custom agent nodes in ChatDev, create a configuration dataclass inheriting from AgentConfig, subclass NodeExecutor (or AgentNodeExecutor) for the execution logic, and register both via register_node_type in runtime/node/registry.py.**

ChatDev's workflow engine orchestrates multi-agent collaboration through a graph-based execution model where each step is a node. The OpenBMB/ChatDev repository provides a pluggable architecture that allows developers to extend the framework with custom agent nodes tailored to specific LLM behaviors or tool interactions. Implementing custom agent nodes in ChatDev requires understanding the strict separation between static configuration and dynamic execution logic.

## Architecture of ChatDev Nodes

ChatDev treats every workflow step as a node composed of two distinct components: a **Configuration dataclass** that declares static parameters, and an **Executor class** that implements the runtime behavior.

### Configuration Layer

Configuration classes live in `entity/configs/node/` and inherit from `BaseConfig` or specialized subclasses like `AgentConfig`. These dataclasses define fields such as model name, temperature, retry policies, and tool lists. In [`entity/configs/node/agent.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/configs/node/agent.py), the standard `AgentConfig` provides the base schema that custom nodes typically extend.

### Execution Layer

Executor classes reside in `runtime/node/executor/` and inherit from the abstract `NodeExecutor` defined in [`runtime/node/executor/base.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/executor/base.py). The built-in `AgentNodeExecutor` in [`runtime/node/executor/agent_executor.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/executor/agent_executor.py) serves as the primary reference implementation, handling LLM calls, tool execution, and message formatting. Custom executors override specific methods like `_prepare_prompt_messages` to inject bespoke behavior while reusing the platform's infrastructure.

## Step-by-Step Implementation

Adding a custom agent node requires four concrete steps: defining the configuration, implementing the executor, registering the type, and referencing it in workflow YAML.

### Step 1: Define the Configuration Dataclass

Create a dataclass inheriting from `AgentConfig` to declare node-specific parameters. This class must implement a `from_dict` classmethod for deserialization and optionally expose `FIELD_SPECS` for UI validation.

```python

# custom_nodes/my_agent_config.py

from dataclasses import dataclass
from typing import Any, Mapping

from entity.configs.base import ConfigFieldSpec, optional_str, optional_float
from entity.configs.node.agent import AgentConfig


@dataclass
class MyAgentConfig(AgentConfig):
    """Custom agent with configurable temperature and system prompt injection."""
    temperature: float = 0.7
    system_prompt: str | None = None

    @classmethod
    def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "MyAgentConfig":
        base = super().from_dict(data, path=path)  # type: ignore[arg-type]

        mapping = dict(data)
        
        temperature = optional_float(
            mapping.get("temperature", 0.7), 
            field_path=f"{path}.temperature"
        )
        system_prompt = optional_str(mapping, "system_prompt", path)
        
        return cls(
            **base.__dict__,
            temperature=temperature,
            system_prompt=system_prompt,
        )

    FIELD_SPECS = {
        **AgentConfig.FIELD_SPECS,
        "temperature": ConfigFieldSpec(
            name="temperature",
            display_name="Temperature",
            type_hint="float",
            default=0.7,
            description="Sampling temperature for the LLM",
        ),
        "system_prompt": ConfigFieldSpec(
            name="system_prompt",
            display_name="System Prompt",
            type_hint="text",
            description="Additional system prompt injected before user input",
        ),
    }

```

### Step 2: Implement the Node Executor

Subclass `AgentNodeExecutor` and override methods like `_prepare_prompt_messages` to modify input formatting or `_prepare_call_options` to adjust LLM parameters. The executor receives an `ExecutionContext` containing shared services.

```python

# custom_nodes/my_agent_executor.py

from typing import List

from entity.messages import Message, MessageRole
from runtime.node.executor.agent_executor import AgentNodeExecutor


class MyAgentExecutor(AgentNodeExecutor):
    """Executor that prepends a custom system prompt and uses configurable temperature."""
    
    def _prepare_prompt_messages(self, node, input_data: str, skill_manager) -> List[Message]:
        messages = super()._prepare_prompt_messages(node, input_data, skill_manager)
        cfg = node.as_config(MyAgentConfig)
        
        if cfg.system_prompt:
            messages.insert(0, Message(role=MessageRole.SYSTEM, content=cfg.system_prompt))
            
        return messages
    
    def _prepare_call_options(self, node):
        """Override to inject custom temperature from config."""
        options = super()._prepare_call_options(node)
        cfg = node.as_config(MyAgentConfig)
        options["temperature"] = cfg.temperature
        return options

```

### Step 3: Register the Node Type

Registration bridges the YAML type name to your Python classes. Import `register_node_type` from [`runtime/node/registry.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/registry.py) and invoke it with your configuration class, executor class, and capabilities.

```python

# custom_nodes/__init__.py

from runtime.node.registry import register_node_type, NodeCapabilities
from .my_agent_config import MyAgentConfig
from .my_agent_executor import MyAgentExecutor

register_node_type(
    name="my_agent",
    config_cls=MyAgentConfig,
    executor_cls=MyAgentExecutor,
    capabilities=NodeCapabilities(
        default_role_field="role",
        exposes_tools=True,
    ),
    summary="Custom agent node with configurable temperature and system prompt injection.",
)

```

Place this package under `main/custom_nodes/` or any location imported during startup. The registration executes at import time, populating the global `node_registry` used by the runtime.

### Step 4: Reference in Workflow YAML

Once registered, reference the node by its registered name in any workflow graph definition:

```yaml

# workflow.yaml

graph:
  - id: greeting_agent
    type: my_agent
    config:
      provider: openai
      name: gpt-4o-mini
      temperature: 0.9
      system_prompt: |
        You are a friendly assistant that always greets users with a joke.
      tools:
        - calculator

```

The `GraphBuilder` resolves `type: my_agent` via [`runtime/node/registry.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/registry.py), instantiates `MyAgentExecutor` with the shared `ExecutionContext`, and executes it within the graph.

## Key Source Files

Understanding these specific files in the OpenBMB/ChatDev repository is essential for implementation:

- **[`runtime/node/registry.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/registry.py)**: Contains `register_node_type` and the global registry dictionary that maps node type strings to `NodeRegistration` objects.
- **[`runtime/node/executor/base.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/executor/base.py)**: Defines the abstract `NodeExecutor` class and `ExecutionContext` passed to all executors.
- **[`runtime/node/executor/agent_executor.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/executor/agent_executor.py)**: Reference implementation showing how to handle LLM calls, tool invocation, and message history.
- **[`entity/configs/node/agent.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/configs/node/agent.py)**: Base configuration schema for agent nodes including retry policies and tool specifications.
- **[`runtime/node/builtin_nodes.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/builtin_nodes.py)**: Examples of how built-in nodes like `agent` and `literal` are registered using `register_node_type`.

## Summary

- **Configuration**: Inherit from `AgentConfig` in `entity/configs/node/` to define static parameters like temperature and custom prompts.
- **Execution**: Subclass `AgentNodeExecutor` in `runtime/node/executor/` to override message preparation or call options while reusing base LLM logic.
- **Registration**: Call `register_node_type` from [`runtime/node/registry.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/registry.py) to bind your config and executor to a YAML type name.
- **Workflow**: Reference the registered name in graph YAML files; the engine automatically resolves and executes your custom node during runtime.

## Frequently Asked Questions

### What is the difference between AgentConfig and BaseConfig?

`AgentConfig` in [`entity/configs/node/agent.py`](https://github.com/OpenBMB/ChatDev/blob/main/entity/configs/node/agent.py) extends `BaseConfig` with fields specific to LLM agents such as model provider, tool lists, and retry configurations. For custom agent nodes that interact with language models, inherit from `AgentConfig` to retain these standard fields. Use `BaseConfig` directly only when building non-LLM node types that require minimal configuration.

### Can I override tool execution behavior in my custom agent?

Yes. While `AgentNodeExecutor` handles standard tool calling, you can override the `_execute_tool` or `_handle_tool_calls` methods in your executor subclass to implement custom tool invocation logic, filtering, or pre/post-processing. The method signatures are defined in [`runtime/node/executor/agent_executor.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/executor/agent_executor.py).

### How does the engine locate my custom node if I place it outside the main package?

The `node_registry` only contains nodes imported during runtime. Place your custom package under `main/` and ensure [`custom_nodes/__init__.py`](https://github.com/OpenBMB/ChatDev/blob/main/custom_nodes/__init__.py) executes the `register_node_type` call. Import the module in [`main/server_main.py`](https://github.com/OpenBMB/ChatDev/blob/main/main/server_main.py) or add it to [`runtime/node/__init__.py`](https://github.com/OpenBMB/ChatDev/blob/main/runtime/node/__init__.py) to guarantee registration before the workflow engine initializes.

### What capabilities should I declare when registering a custom agent?

Pass `NodeCapabilities` to `register_node_type` with `default_role_field="role"` if your node produces messages with roles (user, assistant, system), and set `exposes_tools=True` if it supports tool use. These flags inform the UI and validation systems about node behavior without affecting execution logic.