Now you edit Python. Deploy. Test everything. Hope nothing breaks. A week later someone asks:
Example
"Can we switch the Analyst Agent to Claude?"
Back to Python. Then:
Example
"Can the Gospel Agent use a different prompt?"
Python again. Eventually your orchestration layer stops orchestrating. It becomes a giant collection of if statements. While building PHHM we realized something. The problem wasn't Python. The problem was that business decisions were living inside application code. So we moved almost everything into configuration. That single decision fundamentally changed how the platform evolved.
Section 1
Part 1
The Hidden Cost of Hardcoding
Hardcoded systems feel productive. Initially. Need another agent?
Need another one? agents["communications"] = Communications() Still easy. Then six months later... Your orchestration layer starts looking like this.
Example
if role == "care":
...
elif role == "analyst":
...
elif role == "communications":
...
elif role == "gospel":
...
Example
elif role == "welcome":
Every feature means another deployment. Every deployment introduces another opportunity for bugs. The architecture slowly becomes more rigid.
The Real Problem
The code isn't complicated. The coupling is. Business logic now depends on implementation. Changing organizational behaviour requires engineering work. That's backwards. Business decisions should be data. Not code.
The Shift
One sentence changed how we designed PHHM.
If non-engineers should be able to change it, it probably shouldn't live in Python.
That principle pushed almost everything into configuration. Models. Prompts. Permissions. Routing. Workflows. Feature flags. Defaults.
Thresholds. The Python code became smaller. The platform became dramatically more flexible.
Configuration Is an API
Instead of asking:
What does the application do?
We started asking:
What does the configuration describe?
That's a subtle but profound shift. The application became an execution engine. The configuration became the product.
Part 2
Designing the Agent Registry
Instead of constructing agents inside Python... We describe them.
Notice what didn't happen. No hardcoded imports. No switch statements. No routing tables buried inside Python. Everything comes from configuration.
Dynamic Loading
Instead of:
Example
Analyst()
Care()
Welcome()
The orchestrator builds agents dynamically.
Example
for name, config in registry["agents"].items():
agents[name] = AgentFactory(config)
One loop. Unlimited agents. That's the power of metadata.
Part 3
Routing Without if Statements
Once every agent became configuration, another question appeared. How does the Overseer know where to send work? Most AI applications solve this with increasingly large routing functions. Something like this:
Example
if intent == "analysis":
return analyst
elif intent == "care":
return care
elif intent == "communications":
return communications
It works. Until the tenth agent. Or the twentieth. Every new capability requires modifying the routing logic. Ironically, the orchestrator becomes the least flexible part of the platform. We wanted exactly the opposite.
Let Configuration Drive Routing
Instead of embedding routing rules inside Python, we moved them into configuration.
The routing engine no longer asks: "Which Python function should I call?" Instead it asks: "What does the configuration say this workflow requires?" That's a huge architectural difference.
The Orchestrator Becomes Generic
Once routing lives in configuration, the orchestration engine becomes surprisingly small.
That's it. No growing list of conditionals. No expanding switch statement. No duplicated routing logic. The engine doesn't know anything about newsletters, care plans, or onboarding. It only knows how to execute whatever the configuration describes. That's exactly where orchestration belongs.
From Business Logic to Business Data
This was probably the biggest mindset shift in PHHM. Originally we wrote code that expressed business decisions. Eventually we realized business decisions change far more often than orchestration logic. So we flipped the relationship. Instead of writing:
Without reading a single line of Python, someone can answer:
What does this agent do?
What information does it require?
What does it return?
What permissions does it have?
The configuration becomes a contract.
Validating Configuration
Example
Configuration is powerful.
It's also dangerous. One typo can break an entire workflow. That's why PHHM validates configuration before the application starts.
Example
class AgentConfig(BaseModel):
role: str
model: str
prompt: str
permissions: list[str]
At startup:
Example
config = AgentConfig.model_validate(raw_config)
If validation fails... The application never starts. Failing fast is far better than discovering configuration errors during production traffic.
Configuration Should Fail Loudly
One principle guided the entire platform.
Invalid configuration is a deployment problem—not a runtime problem.
Imagine discovering that an agent has no prompt configured after receiving a customer request. That's already too late. Instead, every configuration file is validated before the first request reaches the system. Startup should be boring. Production should be predictable.
Dynamic Agent Discovery
One feature we didn't initially plan for became one of our favorites. Because agents are defined in configuration, the orchestrator automatically discovers them. for agent in registry["agents"]:
Example
register(agent)
That's the entire registration process. No imports. No manual wiring. No application changes. Adding another specialist becomes a data change. Not a software project.
Permissions Live Beside the Agent
Another lesson came from authorization. Originally permissions were scattered throughout the application. Eventually we moved them next to the agent definition.
One of the biggest advantages of configuration-driven design is model independence. Suppose tomorrow you decide the Communications Agent should use a different model. You don't edit Python. You edit configuration.
The orchestrator doesn't care. It loads whatever model the registry specifies. Today it's GPT-4.1. Tomorrow it might be Claude. Six months from now it could be an entirely different provider. The architecture doesn't change. Only the configuration does. That's exactly the kind of flexibility we wanted.
Configuration Is an Abstraction Layer
Looking back, YAML wasn't really the innovation. The registry wasn't either. The important idea was abstraction. Instead of coupling orchestration to specific models, prompts, or workflows, we inserted a configuration layer between them.
Business Rules
│
▼
Configuration
│
▼
Orchestration Engine
│
▼
AI Models
Each layer has a single responsibility. Business rules describe.
Example
Configuration defines.
The orchestrator executes. The models generate. That's clean architecture applied to AI systems.
Why This Matters
The biggest surprise wasn't that configuration made the system easier to change. It was that it made the code easier to understand. The orchestration engine stopped growing. The configuration grew instead. And configuration is far easier to read, review, validate, and evolve than application logic. That trade-off compounds over time. The larger the platform becomes, the more valuable it gets.
Runtime Configuration: Building an AI Platform That Evolves Without Code Changes
Moving prompts into YAML was only the beginning. The real breakthrough came when we stopped thinking of configuration as static files. Instead, configuration became the control plane for the entire AI platform. Models. Agents. Prompts. Permissions. Feature flags.
Workflows. Routing. All of them became runtime decisions. That meant we could evolve the platform without constantly modifying Python.
The Difference Between Configuration and Configuration-Driven
There's an important distinction. Many applications have configuration. Very few are configuration-driven. For example, this application has configuration.
Example
model: gpt-4.1
temperature: 0.2
Useful? Yes.
Example
Configuration-driven?
Not really. A configuration-driven platform allows the configuration itself to influence application behaviour. For example:
Now the orchestration engine doesn't decide which agents exist.
Example
Configuration does.
That's a fundamentally different architecture.
Part 5
Feature Flags for AI Agents
One of the first capabilities we introduced was feature flags. Suppose we're developing a new Follow-Up Agent. We don't want every production workflow using it immediately. Instead, we register it but leave it disabled.
The agent exists. The orchestrator knows about it. But no production traffic reaches it. Turning it on becomes a configuration change. Not a deployment.
Controlled Rollouts
When the agent is ready, we don't expose it to everyone at once. We enable it gradually.
Example
follow_up:
enabled: true
rollout:
percentage: 10
The orchestration layer checks the rollout policy before assigning work. A simplified implementation might look like this:
Example
if rollout_enabled("follow_up"):
execute("follow_up")
As confidence grows, increasing adoption becomes a matter of changing one value. 10%. 25%. 50%. 100%. No application release required.
Environment-Specific Configuration
Production isn't the only environment. PHHM runs different configurations depending on where it's deployed. For example:
Development prioritizes experimentation. Staging mirrors production. Production prioritizes stability. The orchestration engine stays exactly the same. Only the configuration changes.
Swapping Models Without Rewriting Code
Model providers evolve quickly. Hardcoding them creates unnecessary coupling. Instead, every agent declares the model it should use.
The orchestrator simply asks the configured provider for a response. Tomorrow, if a different model performs better, the migration is a configuration update—not a refactor. That flexibility protects the platform from vendor lock-in.
Runtime Prompt Selection
Not every workflow benefits from the same prompt. Sometimes an agent needs different instructions depending on context. Rather than embedding conditional logic into Python, prompt selection also lives in configuration.
The orchestrator chooses the appropriate prompt based on the workflow. The application doesn't need additional if statements. The configuration already describes the behaviour.
Configuration-Based Permissions
Permissions evolve just as quickly as workflows. Keeping them close to the agent definition avoids duplication.
Now authorization becomes data. Reviewing permissions becomes as simple as reading one configuration file.
Experimenting Safely
One unexpected advantage of configuration-driven architecture is experimentation. Suppose we want to compare two prompts. Instead of modifying code, the registry defines the experiment.
The orchestrator routes a percentage of requests to the candidate version while collecting metrics. Experiments become repeatable. And just as importantly... They're reversible.
Runtime Decisions Replace Compile-Time Decisions
Looking back, this was the biggest architectural shift. Originally, most decisions were made when we wrote the code. Now they're made when the application runs. Examples include:
Before
After
Which model to use
Configuration
Which prompt to load
Configuration
Which agents are enabled
Configuration
Which workflow to execute
Configuration
Which experiment is active
Configuration
Which permissions apply
Configuration
The Python code became increasingly generic. The platform became increasingly flexible.
The Orchestrator Became Smaller
One surprising outcome of moving behaviour into configuration was that the orchestrator itself became simpler. It no longer needed to know:
specific models
prompt locations
workflow details
routing exceptions
experimental features
Its responsibility narrowed to one thing:
Execute the configuration faithfully.
That's exactly what orchestration should do.
The Trade-Offs
Example
Configuration-driven architecture isn't free.
It introduces new responsibilities. More configuration means:
stronger validation requirements
better documentation
versioned configuration
change management
ownership
Poorly managed configuration becomes just as dangerous as poorly written code. The goal isn't to eliminate complexity. It's to move it somewhere it's easier to understand and maintain. For PHHM, configuration was that place.
The Biggest Lesson
Looking back, YAML wasn't the hero of this architecture. It could have been JSON. A database. A service registry. Even a remote configuration service. The important idea wasn't the file format. It was the separation of responsibilities.
Business behaviour should be described, not hardcoded.
That's the principle that made PHHM adaptable.
Five Principles of Configuration-Driven AI
If I were building another multi-agent platform tomorrow, I'd follow the same five principles.
1. Keep orchestration generic
The engine shouldn't know business rules. It should execute them.
2. Keep business logic declarative
Describe behaviour through configuration instead of embedding it in code.
3. Validate everything before startup
Example
Configuration errors should stop deployments—not customer requests.
4. Design for replacement
Models, prompts, and workflows should all be interchangeable.
5. Optimize for change
The architecture should make common changes easy and uncommon changes possible.
Conclusion
When we first started PHHM, YAML felt like an implementation detail. By the end of the project, we realized it represented something much larger. It represented a philosophy. The philosophy that software should execute decisions—not contain them.
Example
Configuration describes the platform.
The orchestrator executes the platform. The agents perform the work. Each layer has one responsibility. That separation is what allowed PHHM to evolve from a small experiment into a production-ready AI system without the orchestration engine growing more complex every month.