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 emptyi_context: Raw input consumed by the action (e.g.,DocumentorCodingContext)prefix: Optional system-prompt prefix injected into the LLMnode: AnActionNodecreated when the class uses@register_toolllm_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 nameexpected_type: Pydantic type (str,int, or nested model)instruction: Natural-language directive for the LLMexample: 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:
- API Entry (
run): When called withoutwith_messages, delegates to_execute_apifor direct JSON-to-Markdown generation (lines 86-99) - Bug-Fix Detection (
_is_bugfix): UsesWP_ISSUE_TYPE_NODEto classify requirements, returningTruefor "BUG" classifications (lines 58-62) - New PRD Creation (
_handle_new_requirement): FillsWRITE_PRD_NODE, saves JSON files, renders PDFs, and triggersDocsReporter(lines 218-231) - PRD Updates (
_handle_requirement_update): For existing PRDs, merges new requirements via_update_prdand regenerates documentation (lines 322-336) - 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:
- Context Assembly (
run): RetrievesCodingContext, gathers related code via_get_codes, and selects between incremental and full templates (lines 99-138) - Incremental Development: When
use_incis enabled,_get_codes(lines 89-115) collects all source files, annotates them with markdown blocks, and highlights the target filename at the top - Code Generation (
write_code): Calls_aaskon the LLM and extracts code blocks usingCodeParser.parse_code(lines 94-99) - Result Persistence: Writes generated code to
self.repo.srcsand 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
Actionbase class,ActionNodeprompt schemas, andProjectRepoworkspace 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_paththroughContext; downstream actions automatically reuse theProjectRepoinstance created by upstream actions. - Template customization involves subclassing actions and overriding template strings like
CONTEXT_TEMPLATEorPROMPT_TEMPLATEwhile retaining core execution logic. - Tool integration via
@register_toolandinclude_functionsextends action capabilities with custom formatters, linters, or validators callable by the LLM duringActionNode.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →