Back to blog
PHHM Journal • Configuration

Configuration-Driven AI: Why YAML Beats Hardcoded Prompts in Production

How we designed PHHM so new AI agents, models, prompts, and workflows can be added without rewriting application code.

Focus
Configuration
Read time
8 min
Series
PHHM Journal
Theme
Production AI

One of the most common patterns I see in AI projects looks something like this:

Example
if request.type == "care":
    return CareAgent()

elif request.type == "analysis":
    return AnalystAgent()

elif request.type == "newsletter":
    return CommunicationsAgent()

It works. Until someone asks:

Example
"Can we add another agent?"

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?

Example
agents = {
    "analyst": Analyst(),
    "care": Care(),
    "welcome": Welcome()
}

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.

Example
agents:

  analyst:

    role: analysis

    model: gpt-4.1

    prompt: prompts/analyst.md

    temperature: 0.2

    permissions:

      - reports

      - summaries

      - recommendations

Now Python doesn't "know" about the Analyst. It discovers it.

The Registry Pattern

Every startup follows the same process.

Application Starts
↓
Load Configuration
↓
Register Agents
↓
Validate Configuration
↓
Ready

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.

Example
routes:

  member_report:

    agents:
      - analyst
      - care

  onboarding:

    agents:
      - welcome

  newsletter:

    agents:
      - communications

  devotional:

    agents:
      - gospel

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.

Example
def get_workflow(intent):

    return registry["routes"][intent]["agents"]

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:

Example
if member.is_new:
    assign_welcome_agent()

we describe the workflow.

Example
workflow:

  trigger: new_member

  agents:

    - welcome

    - analyst

Python executes.

Example
Configuration decides.
Part 4

Configuration as Contracts

Another unexpected benefit emerged.

Example
Configuration became documentation.

Anyone opening the YAML could understand the platform. For example:

Example
agent:

  analyst

purpose:

  Generate structured analysis.

inputs:

  Member profile

outputs:

  Analysis object

permissions:

  reports

  summaries

  recommendations

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.

Example
analyst:

  permissions:

    - reports

    - summaries

    - recommendations

care:

  permissions:

    - follow_ups

    - interventions

Now authorization travels with the agent itself.

Example
Configuration stays cohesive.

Responsibilities remain obvious.

Swapping Models Without Code Changes

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.

Example
communications:

  model: claude-4

analyst:

  model: gpt-4.1

care:

  model: gpt-4.1

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:

Example
agents:

  analyst:
    enabled: true

  gospel:
    enabled: false

  communications:
    enabled: true

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.

Example
agents:

  follow_up:

    enabled: false

    model: gpt-4.1

    prompt: prompts/follow_up/system.md

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:

Example
config/

    development.yaml

    staging.yaml

    production.yaml

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.

Example
analyst:

  provider: openai

  model: gpt-4.1

communications:

  provider: anthropic

  model: claude-4

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.

Example
analyst:

  prompts:

    default: analyst/system.md

    emergency: analyst/emergency.md

    onboarding: analyst/onboarding.md

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.

Example
care:

  permissions:

    - care_plan

    - follow_up

    - intervention

  roles:

    - pastor

    - administrator

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.

Example
experiments:

  analyst_prompt:

    control: v2.4.1

    candidate: v2.5.0

    traffic: 20

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:

BeforeAfter
Which model to useConfiguration
Which prompt to loadConfiguration
Which agents are enabledConfiguration
Which workflow to executeConfiguration
Which experiment is activeConfiguration
Which permissions applyConfiguration

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.