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

> Explore advanced MetaGPT action classes like WritePRD and WriteCode. Learn how to build structured, autonomous software development pipelines using ActionNode and ProjectRepo for efficient code and PRD generation.

- Repository: [FoundationAgents/MetaGPT](https://github.com/FoundationAgents/MetaGPT)
- Tags: deep-dive
- Published: 2026-03-04

---

**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`](https://github.com/FoundationAgents/MetaGPT/blob/main/metagpt/actions/write_prd.py) and [`metagpt/actions/write_code.py`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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:

```python
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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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:

```python
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:

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

await code.run()

```

As implemented in [`metagpt/actions/write_code.py`](https://github.com/FoundationAgents/MetaGPT/blob/main/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:

```python
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`](https://github.com/FoundationAgents/MetaGPT/blob/main/metagpt/utils/report.py) that automatically process artefacts:

```python
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`](https://github.com/FoundationAgents/MetaGPT/blob/main/metagpt/tools/tool_registry.py) to make them available to the LLM as callable functions:

```python
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:

```python
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:

```python
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:

```python
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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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`](https://github.com/FoundationAgents/MetaGPT/blob/main/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.