Advanced Usage Patterns for MetaGPT Action Classes: WritePRD and WriteCode Deep Dive

MetaGPT action classes like WritePRD and WriteCode provide a modular framework for autonomous software development, leveraging ActionNode for structured LLM prompts and ProjectRepo for workspace management to generate product requirements and source code through composable, extensible pipelines.

MetaGPT is an open-source multi-agent framework that simulates a software company by orchestrating specialized roles through action classes. Understanding advanced usage patterns for MetaGPT action classes such as WritePRD and WriteCode enables developers to build custom AI-driven development workflows, extend default behaviors, and integrate external tools. This guide examines the underlying architecture in metagpt/actions/write_prd.py and metagpt/actions/write_code.py to demonstrate production-ready implementation patterns.

Architecture Deep-Dive

The MetaGPT action system is built on three core abstractions: the Action base class, ActionNode for prompt engineering, and ProjectRepo for workspace management.

The Action Base Class

All actions inherit from Action in metagpt/actions/action.py, which extends SerializationMixin, ContextMixin, and Pydantic BaseModel. Key attributes include:

  • name: Human-readable identifier auto-filled if empty
  • i_context: Raw input consumed by the action (e.g., Document or CodingContext)
  • prefix: Optional system-prompt prefix injected into the LLM
  • node: An ActionNode created when the class uses @register_tool
  • llm_name_or_type: Per-action LLM selection that falls back to global config

The abstract run() method must be implemented by concrete actions like WritePRD and WriteCode to define their async execution logic.

ActionNode and Prompt Schemas

ActionNode in metagpt/actions/action_node.py models a tree of sub-prompts that describes the expected LLM output structure. Each node defines:

  • key: The output field name
  • expected_type: Pydantic type (str, int, or nested model)
  • instruction: Natural-language directive for the LLM
  • example: Optional example value shown to the model

The fill workflow executes as follows:

node.set_llm(llm)               # Inject LLM instance

node.set_context(req)           # Set request context

await node.simple_fill(...)     # Generate prompt, call LLM, parse output

The simple_fill method compiles the final prompt using compile() and parses responses into Pydantic models via OutputParser or JSON post-processing.

ProjectRepo Workspace Abstraction

Both actions interact with ProjectRepo (self.repo) from metagpt/utils/project_repo.py to abstract workspace operations:

  • Document storage (repo.docs.*): Manages PRDs, requirements, and bug-fix records
  • Source code handling (repo.srcs): Reads and writes .py, .js, and other source files
  • Git integration (repo.git_repo.rename_root): Automatically renames workspace when project names are detected

The repository instantiates from the project_path supplied in the global Context, enabling persistent state across action executions.

WritePRD Workflow

The WritePRD action in metagpt/actions/write_prd.py handles product requirement generation through multiple pathways:

  1. API Entry (run): When called without with_messages, delegates to _execute_api for direct JSON-to-Markdown generation (lines 86-99)
  2. Bug-Fix Detection (_is_bugfix): Uses WP_ISSUE_TYPE_NODE to classify requirements, returning True for "BUG" classifications (lines 58-62)
  3. New PRD Creation (_handle_new_requirement): Fills WRITE_PRD_NODE, saves JSON files, renders PDFs, and triggers DocsReporter (lines 218-231)
  4. PRD Updates (_handle_requirement_update): For existing PRDs, merges new requirements via _update_prd and regenerates documentation (lines 322-336)
  5. Competitive Analysis (_save_competitive_analysis): Extracts quadrant charts from PRD JSON and writes Mermaid diagrams to SVG files (lines 274-283)

WriteCode Workflow

The WriteCode action in metagpt/actions/write_code.py generates source files from design context:

  1. Context Assembly (run): Retrieves CodingContext, gathers related code via _get_codes, and selects between incremental and full templates (lines 99-138)
  2. Incremental Development: When use_inc is enabled, _get_codes (lines 89-115) collects all source files, annotates them with markdown blocks, and highlights the target filename at the top
  3. Code Generation (write_code): Calls _aask on the LLM and extracts code blocks using CodeParser.parse_code (lines 94-99)
  4. Result Persistence: Writes generated code to self.repo.srcs and produces markdown versions of JSON outputs (lines 151-165)

Advanced Usage Patterns

Composing Actions in Pipelines

Chain WritePRD and WriteCode to create full development cycles. Both actions share the same ProjectRepo instance through the global Context, eliminating manual file handling:

import asyncio
from metagpt.actions.write_prd import WritePRD
from metagpt.actions.write_code import WriteCode

async def full_cycle(requirement: str, project_path: str):
    # Generate or update the PRD

    prd = WritePRD()
    prd.context.kwargs.project_path = project_path
    await prd.run(
        user_requirement=requirement,
        output_pathname=f"{project_path}/docs/prd.json",
    )
    
    # Generate code based on the PRD

    code = WriteCode()
    code.context.kwargs.project_path = project_path
    await code.run()

The ProjectRepo created by WritePRD is automatically reused by WriteCode, maintaining consistent workspace state.

Incremental Development with WriteCode

Enable incremental mode to reuse existing code and rewrite only target files:

code = WriteCode()
code.config.inc = True  # Enable incremental mode

await code.run()

As implemented in metagpt/actions/write_code.py lines 89-115, _get_codes inserts each existing file as a markdown block prefixed with ### File Name: <filename>, placing the target filename at the top (### The name of file to rewrite). This context guides the LLM to produce diff-aware implementations that preserve existing utility functions.

Customizing Prompt Templates

Override default templates to adapt actions for domain-specific requirements. Subclass the action and modify the template constants:

from metagpt.actions.write_prd import WritePRD

class EnterpriseWritePRD(WritePRD):
    CUSTOM_TEMPLATE = """
    ### Enterprise Context

    {project_name}
    
    ### Compliance Requirements

    {requirements}
    """
    
    async def _new_prd(self, requirement: str):
        node = await self.WRITE_PRD_NODE.fill(
            req=self.CUSTOM_TEMPLATE.format(
                project_name=self.project_name,
                requirements=requirement
            ),
            llm=self.llm,
            schema=self.prompt_schema
        )
        return node

The subclass retains full run logic—including bug-fix detection and repo handling—while injecting tailored prompting.

Adding Post-Processing Reporters

MetaGPT includes visualization reporters in metagpt/utils/report.py that automatically process artefacts:

from metagpt.utils.report import DocsReporter, GalleryReporter

async def generate_with_reporting():
    prd = WritePRD()
    async with DocsReporter(enable_llm_stream=True) as reporter:
        await reporter.async_report({"type": "prd"}, "meta")
        await prd.run(user_requirement="Add user authentication")
        # Reporter logs generated PDF paths automatically

DocsReporter handles PDF generation during WritePRD._handle_new_requirement, while GalleryReporter processes competitive-analysis diagrams from _save_competitive_analysis.

Integrating External Tools via ToolRegistry

Register custom utilities with @register_tool from metagpt/tools/tool_registry.py to make them available to the LLM as callable functions:

from metagpt.tools.tool_registry import register_tool

@register_tool(tags=["code"])
class CodeFormatter:
    def format(self, code: str) -> str:
        """Format Python code according to PEP 8."""
        return code.strip()

# In your action, expose the tool to the LLM:

node = await WRITE_CODE_NODE.fill(
    req=prompt,
    llm=self.llm,
    schema=self.prompt_schema,
    include_functions=["format"],  # Expose formatter to LLM

)

When include_functions contains "format", the LLM can invoke CodeFormatter.format as a tool call, receiving cleaned output before final persistence.

Practical Implementation Examples

Standalone Async Execution

Execute actions outside the full MetaGPT framework for specific automation tasks:

import asyncio
from metagpt.actions.write_prd import WritePRD
from metagpt.actions.write_code import WriteCode

async def demo():
    # Write PRD

    prd = WritePRD()
    await prd.run(
        user_requirement="Create a CLI todo-list manager with SQLite storage",
        output_pathname="demo_project/docs/prd.json",
        extra_info="Include sub-commands: add, list, remove",
    )
    
    # Write code with incremental support

    code = WriteCode()
    code.config.inc = True
    await code.run()
    print("Artifacts generated in demo_project/")

if __name__ == "__main__":
    asyncio.run(demo())

Updating Existing PRDs

MetaGPT automatically detects and merges updates when you reference legacy PRDs:

await WritePRD().run(
    user_requirement="Add multi-user sharing to the todo-list",
    legacy_prd_filename="demo_project/docs/prd.json",
    output_pathname="demo_project/docs/prd_v2.json",
)

The action loads the legacy PRD, detects the update relationship via _is_related, and merges specifications through WRITE_PRD_NODE before writing the updated JSON file.

Registering Custom Validation Tools

Integrate quality gates directly into the code generation pipeline:

from metagpt.tools.tool_registry import register_tool

@register_tool(tags=["validation"])
class SecurityLinter:
    def scan(self, code: str) -> dict:
        """Scan for hardcoded secrets."""
        return {"safe": "password" not in code.lower()}

# Use in WriteCode or subclass:

node = await WRITE_CODE_NODE.fill(
    req=prompt,
    llm=self.llm,
    schema=self.prompt_schema,
    include_functions=["scan"],
)

The LLM can invoke scan to validate code before the write_code method persists files to self.repo.srcs.

Summary

  • MetaGPT action classes provide a unified interface for autonomous software development through the Action base class, ActionNode prompt schemas, and ProjectRepo workspace management.
  • WritePRD handles requirement analysis, bug-fix detection, and competitive-analysis visualization through structured nodes in metagpt/actions/write_prd.py.
  • WriteCode supports incremental development by embedding existing source files into LLM context via _get_codes, enabling targeted file rewrites while preserving project structure.
  • Pipeline composition requires only sharing the project_path through Context; downstream actions automatically reuse the ProjectRepo instance created by upstream actions.
  • Template customization involves subclassing actions and overriding template strings like CONTEXT_TEMPLATE or PROMPT_TEMPLATE while retaining core execution logic.
  • Tool integration via @register_tool and include_functions extends action capabilities with custom formatters, linters, or validators callable by the LLM during ActionNode.fill().

Frequently Asked Questions

How do MetaGPT action classes handle context sharing between WritePRD and WriteCode?

MetaGPT action classes share context through the global Context object and ProjectRepo abstraction. When WritePRD.run() executes with a project_path, it instantiates a ProjectRepo in self.repo that manages the workspace. Subsequent WriteCode instances automatically discover this repository through self.context.kwargs.project_path, enabling seamless access to generated PRDs and existing source files without explicit file path passing.

What is the difference between full and incremental mode in WriteCode?

In full mode, WriteCode generates files from scratch using only the design context and task description. In incremental mode (config.inc = True), the _get_codes method (lines 89-115 in metagpt/actions/write_code.py) embeds all existing source files from repo.srcs into the LLM prompt as markdown blocks. This allows the model to reference existing implementations, maintain consistency with established patterns, and produce targeted rewrites of specific files while preserving the rest of the codebase.

How can I customize the LLM prompts used by WritePRD?

Subclass WritePRD and override the template attributes or node definitions. The action uses WRITE_PRD_NODE for new requirements and WP_ISSUE_TYPE_NODE for classification. By creating a subclass that overrides CUSTOM_TEMPLATE or modifies the fill() call parameters, you can inject domain-specific instructions while maintaining the bug-fix detection logic in _is_bugfix and the repository handling in run().

Can external tools be integrated into the action execution pipeline?

Yes, through the ToolRegistry system in metagpt/tools/tool_registry.py. Decorate utility classes with @register_tool(tags=["code"]) and specify method signatures. When calling ActionNode.fill(), pass include_functions=["method_name"] to expose these tools to the LLM. The LLM can then invoke these functions during generation, enabling automated formatting, security scanning, or custom validation before final artefact persistence.

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 →