Part 4 of “My Journey in Building Agents from Scratch”
Introduction
If you want to scale your AI projects, automating agent creation with templates is a game changer. After building my first few agents, I ran into a familiar developer headache: boilerplate. Creating a new agent meant creating a whole new Python class. While each agent was unique, its core structure was nearly identical to the others. I found myself repeatedly defining the same properties: the list of tools, the lengthy system prompt, the model client object… it was a mountain of copy-paste.
The tipping point came when I was setting up a third agent and realized I was spending more time duplicating code than writing new logic. That’s when I had my “enough is enough” moment. I thought, “Why can’t this be automated from a simple input file?” Why should agent logic be so tightly coupled to the code? Developers should write Python functions for tools, but everything else—prompts, model config, tool selection—should live outside the code. Enter automating agent creation with templates and YAML configs. Suddenly, building agents felt like snapping together LEGO blocks.
- Introduction
- Why Automating Agent Creation with Templates?
- What Goes Into an Agent Template?
- Agent template config example:
- MCP server template config example:
- Local vs. Shared Tools: Integrating the Model Context Protocol (MCP)
- Example `config.yaml` using MCP:
- How I Use Agent Templates in Practice
- The Agent Loader: Bringing It All Together
- From Agent Template to API
- Key Takeaways: Automating Agent Creation with Templates
- Try It Yourself: Automating Agent Creation with Templates
Why Automating Agent Creation with Templates?
Automating agent creation with templates solves the problem of repeated setup. By standardizing agent and MCP server creation, I could:
- Ensure consistency across projects
- Reduce setup time (from hours to minutes)
- Make maintenance easier (no more hunting for tool bugs)
– Share best practices with teammates (even those new to agents)
In Post 3, I described moving from manual tool schemas to Python-first utilities and centralized tools with Model Context Protocol (MCP). Automating agent creation with templates is the next logical step: package all that hard-won knowledge and reuse it everywhere.
What Goes Into an Agent Template?
Automating agent creation with templates means your templates are structured, explicit, and easy to read. For both agents and MCP servers, the YAML config includes:
- name: The name of the agent or server
- description: What it does (for docs, UI, or debugging)
- model_client (for agents): Endpoint, model name, and env key for authentication
- tools: A list of tool definitions, each with:
- file_path: Path to the Python file with tool functions
- functions: List of function names to expose
- prompt (for agents): The system prompt or path to a prompt file
Agent template config example:
name: "MathAgent"
description: "An agent that performs math operations using tools."
model_client:
endpoint: "https://api.openai.com/v1"
model: "gpt-4.1"
env_key: "OPENAI_API_KEY"
tools:
- file_path: "./tools/math.py"
functions: ["add", "sub", "mul"]
prompt:
system_message_file: "./prompts/system.txt"
MCP server template config example:
name: "MathMCPServer"
description: "MCP server exposing math tools."
tools:
- file_path: "./tools/math.py"
functions: ["add", "sub", "mul"]
This parallel structure means you can reuse the same tools and config patterns for both agents and MCP servers. It’s simple, explicit, and keeps everything DRY. I intentionally started with this straightforward structure because it covers the core needs and provides a solid foundation to build on. This is where I began, and it’s proven easy to add more complex features over time.
Local vs. Shared Tools: Integrating the Model Context Protocol (MCP)
This template system is flexible enough to handle both scenarios we’ve discussed: agent-specific tools and shared tools hosted on a Model Context Protocol (MCP) server. The rule of thumb is simple:
- Use local tools (via `file_path`) for functions that are unique to a specific agent.
- Use an MCP endpoint for tools that are shared and maintained centrally for many agents.
To use shared tools, the `tools` section in the `config.yaml` would simply change to point to the MCP server’s URL.
Example `config.yaml` using MCP:
name: "ReportingAgent"
description: "An agent that uses centralized tools to build reports."
model_client:
endpoint: "https://api.openai.com/v1"
model: "gpt-4.1"
env_key: "OPENAI_API_KEY"
tools:
- mcp_server:
url: "http://mcp.internal-service.com/tools"
# Optionally, specify which functions to use from the MCP
functions: ["generate_sales_report", "get_user_analytics"]
prompt:
system_message_file: "./prompts/reporting_prompt.txt"
This hybrid approach means the template accommodates everything from quick, single-purpose agents to complex agents participating in a larger ecosystem.
How I Use Agent Templates in Practice
My journey started with writing agent classes that bundled everything together: prompt, tool calling logic, and model client. For each new use case, I’d create a new class—like `CalculatorAgent` for math tools (see Post 2). But every time requirements changed, I had to touch the Python files. Not scalable.
My mind went to my past experience using `.ini` files to configure applications; I knew separating configuration from code was a powerful concept. So, I started by splitting everything into separate files: prompts, tools, and model client config. But even then, changing the model or tools still meant code changes. That’s when I had the real breakthrough: why not define *everything* in a single, structured YAML file?
Now, automating agent creation with templates means I just pass a different YAML file—no code changes required. The agent class reads the config, loads the model client, prompt, and tools automatically. This made my agents fully generic and dramatically sped up development.
The Agent Loader: Bringing It All Together
The magic of automating agent creation with templates happens in the agent loader. This script reads the YAML config, loads the prompt, imports the tools, and initializes the model client. Suddenly, creating a new agent is as easy as editing a config file and running one command.
Here’s what a typical agent template folder looks like with the loader:
agent-template/
├── config.yaml # All agent config (model, prompt, tools)
├── prompts/
│ └── system.txt # System prompt text
├── tools/
│ └── math.py # Python functions for tools
├── agent_loader.py # Generic agent loader
├── requirements.txt # Dependencies
└── README.md # How to use the template
# agent_loader.py
import yaml
import importlib.util
def load_tools(file_path, tool_names):
spec = importlib.util.spec_from_file_location("tools_module", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
tools = {}
for name in tool_names:
func = getattr(module, name)
tools[name] = func
return tools
def load_agent(config_path):
with open(config_path) as f:
config = yaml.safe_load(f)
with open(config['prompt']['system_message_file']) as pf:
system_message = pf.read()
tool_cfg = config['tools'][0]
# This is a simplified loader; a real one would handle MCP tools, etc.
tools = load_tools(tool_cfg['file_path'], tool_cfg['functions'])
model_client = config['model_client']
return {
'system_message': system_message,
'tools': tools,
'model_client': model_client
}
From Agent Template to API
The template system was a huge internal success. It made our team faster and more consistent. But the real “aha!” moment came when a colleague working on a TypeScript web app needed to integrate one of our agents. They couldn’t use our Python code directly, and rewriting all the tool-calling logic, prompts, and orchestration in TypeScript would have been a massive undertaking.
This is where the true power of automating agent creation with templates became clear. Because our agent’s entire definition—its prompt, tools, and model—is encapsulated in a portable, self-contained folder, we could create a single, generic API server that could load and serve any agent on demand.
Instead of the web developer rebuilding the agent, they could simply call it.
Why FastAPI for Agent APIs?
For the API server, I chose FastAPI. It’s a modern, high-performance Python web framework that’s incredibly easy to use. Its key benefits for this use case are:
- Automatic Docs: It creates interactive API documentation (like Swagger UI) automatically, which is a huge help for consumers of your API.
- Data Validation: It uses Pydantic for data validation, ensuring that requests coming into your API have the correct structure.
- Async Support: It’s built for asynchronous programming, which is perfect for handling I/O-bound operations like waiting for a response from a language model.
The Generic Agent Server: Deploying Agent Templates as APIs
The goal is to create one server that doesn’t need to be changed when you add, remove, or modify agents. The server’s only job is to find an agent’s template folder, use our `agent_loader.py` to load it into memory, and pass on the user’s request.
I created an `api_server.py` file to house this logic. Here is the updated template structure:
agent-project/
├── api_server.py # Our new generic API server
├── agent_loader.py # The loader from before
├── requirements.txt # Now includes fastapi, uvicorn
└── agents/
└── MathAgent/
├── config.yaml
├── prompts/
│ └── system.txt
└── tools/
└── math.py
#api_server.py
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
import os
from agent_loader import load_agent
app = FastAPI()
class ChatRequest(BaseModel):
message: str
# You could add session_id, etc. here
# In a real app, you'd have a more robust agent execution loop.
# This is a simplified example.
def run_agent_once(agent, message):
# Here you would implement the logic to call the model with the message
# and tools, handle tool calls, and return the final response.
print(f"Running agent with message: {message}")
print(f"System Prompt: {agent.get('system_message')}")
return f"Agent executed with message: '{message}'. Tools available: {list(agent['tools'].keys())}"
@app.post("/agents/{agent_name}/chat")
def chat_with_agent(agent_name: str, request: ChatRequest):
# Note: In a real app, validate agent_name to prevent path traversal issues.
config_path = os.path.join("agents", agent_name, "config.yaml")
if not os.path.exists(config_path):
return {"error": f"Agent '{agent_name}' not found."}
try:
agent = load_agent(config_path)
response = run_agent_once(agent, request.message)
return {"response": response}
except Exception as e:
return {"error": f"Failed to load or run agent: {str(e)}"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Running and Using the API
With this server, deploying any agent is now a two-step process:
- Create the agent’s template folder inside the `agents/` directory.
- Run the API server.
To run the server:
uvicorn api_server:app --reload
Now, our TypeScript developer can interact with the `MathAgent` by calling the API:
curl -X POST "http://127.0.0.1:8000/agents/MathAgent/chat" \
-H "Content-Type: application/json" \
-d '{"message": "What is 2 plus 2?"}'
If we create a new `CodeWriterAgent`, we just create the folder and `config.yaml`, and it’s instantly available at `/agents/CodeWriterAgent/chat`. No changes to the API server needed.
Key Takeaways: Automating Agent Creation with Templates
This journey transformed how I build agents. By automating agent creation with templates and combining a template-driven design with a generic API server, we achieved a powerful workflow:
- Templates Make Agent Creation Declarative: Instead of writing Python classes, we declare an agent’s properties in a YAML file. This makes creation faster, less error-prone, and accessible to non-developers.
- Separation of Concerns is Crucial: The agent’s logic (Python tool code), its definition (YAML config), and its instruction (prompt file) are kept separate, making maintenance a breeze.
- An API Makes Agents Reusable Everywhere: Wrapping the agent loader in an API means any application, in any language, can leverage your agents. This breaks down silos and prevents duplicated work.
- The `config.yaml` is the Single Source of Truth: To create, modify, or deploy an agent, you only need to touch its template folder.
Try It Yourself: Automating Agent Creation with Templates
1. Create a New Agent Template: Take the template folder and create a `JokeAgent`. Write a Python tool `get_joke()` that returns a hardcoded joke. Write a prompt and `config.yaml` and test it with the API.
2. Add Configuration to a Tool: Modify the `JokeAgent` tool to take a `category` (e.g., “dad jokes”, “programming jokes”). Add a `configuration` block to your `config.yaml` to specify the default category, and update your `agent_loader.py` to pass this to the tool.
3. Make the API an Agent Tool: Create a *new* agent, the `AgentOrchestrator`, that has a tool called `call_another_agent(agent_name: str, message: str)`. This tool would use `requests` to call your own API server. This is the first step towards multi-agent systems!
Previous: Tools and MCP

Great job keeping things both accurate and engaging.
The platform is quietly exceptional.
Very high signal-to-noise ratio here. This site has turned into my go-to reference.
Automating agent creation with templates indeed sounds like a game changer for scaling AI projects. The idea of reducing boilerplate code and focusing more on writing new logic is very appealing. I found the analogy of snapping together LEGO blocks particularly relatable—it simplifies the process and makes it more intuitive. However, I’m curious about how you handle scenarios where agents require highly customized logic that doesn’t fit neatly into templates. Do you find that the YAML configs limit creativity in some cases, or is there always a way to adapt the template system to complex needs? Also, how do you ensure that teammates, especially those new to agents, can easily grasp and utilize these templates without extensive training? It seems like a fantastic approach, but I wonder if there’s a learning curve involved. What’s your experience with onboarding new developers to this system?
Great question — and honestly, one I wrestled with myself.
On customization: the YAML config is just the declaration — what the agent is called, which model to use, where the prompt lives, which tools to load. The actual logic lives entirely in Python. So if an agent needs something unusual — a multi-step tool, stateful behavior, a custom retry loop — that all goes in the tools file. The template never restricts what the tool does, only how it gets wired up. In practice, I’ve never hit a wall because of the YAML. The Python tools are the escape hatch.
On limiting creativity: I’d flip the question slightly. The template actually freed me to be more creative with the logic, because I stopped thinking about plumbing. When the boring parts (loading, wiring, serving) are solved once and never touched again, you spend more time on the interesting parts.
On onboarding: there is a learning curve, but it’s front-loaded. The first time someone sees agent.yaml, they tend to ask “where’s the real code?” — and the answer (“the tools file”) clicks pretty quickly. What I found works well is having a working example agent in the repo. New developers copy it, rename things, swap the prompt and tools, and have something running in under an hour. After that, the pattern sticks.
The honest caveat: this system works well when agents are relatively focused. If you’re building something with deeply tangled inter-agent state or dynamic tool composition at runtime, the template starts to feel thin. That’s when you move toward orchestration — which is exactly what the next post is about.