How to Implement Custom Agent Nodes in ChatDev Workflow Graphs

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, 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. The built-in AgentNodeExecutor in 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.


# 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.


# 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 and invoke it with your configuration class, executor class, and capabilities.


# 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:


# 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, 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:

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 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 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.

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 executes the register_node_type call. Import the module in main/server_main.py or add it to 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →