Agent Prompt Overrides
Overview
The Mastra Agent Framework uses a database-driven approach for managing agent prompts and configurations. Instead of hardcoding prompts in source files, agent instructions live in the database, enabling runtime updates without redeployment.
**Using the Unified Agent Platform?** The Unified Agent Platform handles all profile loading, caching, and override logic automatically. You just declare your agent's needs using an AgentDescriptor and the platform manages the rest.
How Prompt Overrides Work
1. Profile-Based System
All agents use the Agent Profiles Service to load their system prompts and configurations from the Core.AgentProfiles table:
- Default Prompts: Each agent has a default prompt defined in code as a fallback
- Database Overrides: When a profile exists in the database with
IsCurrent = 1, it overrides the default - Runtime Loading: Agents fetch their active profile at initialization
2. The Core.AgentProfiles Table
The Core.AgentProfiles table is the central store for all agent configurations:
CREATE TABLE Core.AgentProfiles (
AgentProfileSK BIGINT IDENTITY(1,1) PRIMARY KEY,
AgentId NVARCHAR(255) NOT NULL, -- Unique agent identifier
AgentAlias NVARCHAR(255), -- Human-friendly name
SystemPrompt NVARCHAR(MAX), -- The actual prompt text
LlmOverridesJson NVARCHAR(MAX), -- JSON for temperature, max tokens, etc.
MetadataJson NVARCHAR(MAX), -- Additional metadata
ProfileHash CHAR(64) NOT NULL, -- Content hash for deduplication
IsCurrent BIT NOT NULL DEFAULT 1, -- Active profile flag
VersionNumber INT NOT NULL DEFAULT 1, -- Version tracking
CreatedAtUtc DATETIME2 DEFAULT GETUTCDATE(),
UpdatedAtUtc DATETIME2 DEFAULT GETUTCDATE(),
EffectiveEndUtc DATETIME2,
CreatedBy NVARCHAR(255),
UpdatedBy NVARCHAR(255)
);
Key Fields:
- AgentId - Stable identifier matching the agent's registry ID (e.g.,
k12-safety-agent,tap-caption-tagging-agent) - SystemPrompt - The full system prompt text that overrides the default
- LlmOverridesJson - Optional JSON for model parameters:
{
"temperature": 0.7,
"maxTokens": 2000,
"topP": 0.9,
"deploymentName": "gpt-5.1"
} - IsCurrent - Only one profile per agent can have
IsCurrent = 1(enforced by unique filtered index) - ProfileHash - SHA-256 hash of content to prevent duplicate updates
3. Agent Loading Flow
With Unified Agent Platform (Recommended)
Most agents use the Unified Agent Platform which handles loading automatically:
import { getOrCreateFactory, AgentDescriptor } from '../shared';
const MY_AGENT_DESCRIPTOR: AgentDescriptor = {
profileId: 'my-agent',
registryId: 'my-agent',
defaultName: 'My Agent',
defaultPrompt: DEFAULT_PROMPT,
tools: [myTool],
seedConfig: { shouldSeed: true },
};
// Factory automatically:
// 1. Loads current profile from Core.AgentProfiles
// 2. Seeds default if missing (optional)
// 3. Resolves model with database validation
// 4. Caches agent with profile hash
// 5. Invalidates cache on profile changes
const agent = await getOrCreateFactory(MY_AGENT_DESCRIPTOR).getAgent();
Manual Loading (Legacy)
If not using the unified platform, agents load profiles manually:
// 1. Agent attempts to load profile from database
const profile = await agentProfilesService.getCurrent(agentId);
// 2. If profile exists and is current, use it
if (profile && profile.IsCurrent) {
agent.instructions = profile.SystemPrompt;
agent.model = profile.LlmOverridesJson?.deploymentName || defaultModel;
// Apply other overrides...
}
// 3. Otherwise, use default prompt from code
else {
agent.instructions = DEFAULT_PROMPT;
agent.model = defaultModel;
}
Manual profile loading is error-prone and requires custom caching logic. Consider migrating to the Unified Agent Platform which handles all of this automatically.
4. Versioning System
The profile system maintains a complete version history:
- Each update creates a new row with incremented
VersionNumber - Previous version is marked
IsCurrent = 0and gets anEffectiveEndUtctimestamp - All historical versions remain for audit purposes
- The
ProfileHashprevents duplicate content from creating unnecessary versions
Updating Agent Prompts
Recommended Method: Admin Tools
Use the provided admin tools instead of manual SQL to maintain versioning integrity:
Via HTTP API:
curl -X POST "$BASE_URL/admin/tools/update-agent-profile" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"agentId": "k12-safety-agent",
"systemPrompt": "You are Eddy, the assistant that helps edit EOPs...",
"llmOverrides": {
"temperature": 0.7,
"maxTokens": 2000
},
"updatedBy": "admin@example.com"
}
}'
Via Admin UI:
- Navigate to Admin → Agent Profiles
- Select the agent from the dropdown
- Edit the prompt and configuration
- Save (automatically handles versioning)
What Happens During Update
- Hash Calculation: System calculates SHA-256 hash of new content
- Duplicate Check: If hash matches current profile, update is skipped
- Version Creation: New row created with
VersionNumber + 1 - Current Flag: Previous row marked
IsCurrent = 0,EffectiveEndUtcset - Agent Reload: Running agents detect hash change and reload configuration
Agent-Specific Override Examples
K12 Safety Agent (EOP)
// Default prompt in code
const DEFAULT_PROMPT = `You are Eddy, the assistant that helps to edit and create EOPs...`;
// Override in database (Core.AgentProfiles)
AgentId: "k12-safety-agent"
SystemPrompt: "Enhanced version: You are Eddy, an expert in K12 emergency planning..."
LlmOverridesJson: '{"temperature": 0.5, "maxTokens": 3000}'
QRG Agents
Each QRG agent can be individually configured:
// Category Classification Agent
AgentId: "qrg-category-classification-agent"
SystemPrompt: "Custom prompt for category classification..."
// Content Generation Agent
AgentId: "qrg-content-generation-agent"
SystemPrompt: "Custom prompt for QRG content generation..."
LlmOverridesJson: '{"deploymentName": "gpt-5.1"}'
TAP Agents
// TAP Tagging Suggestions Agent
AgentId: "tap-caption-tagging-agent"
SystemPrompt: "Return tagging suggestions and optional captions as JSON..."
LlmOverridesJson: '{"temperature": 0.2, "maxTokens": 1200}'
Dynamic Prompt Placeholders
What Are Prompt Placeholders?
In addition to static prompt overrides, the Mastra framework supports dynamic prompt placeholders that are replaced with real data at runtime. This enables prompts to include contextual information fetched from external APIs or databases.
Think of placeholders as variables in a template. When you write an agent's system prompt in the database, you can include placeholders like {district_name} or {list_of_roles}. At runtime, the workflow fetches the actual district name and roles from an external API, then replaces those placeholders with the real values before sending the prompt to the LLM.
Placeholder Format: {placeholder_name}
Why This Matters: This approach allows a single agent prompt to work across thousands of different schools or districts, each with their own specific information. The prompt adapts to the context without requiring separate database entries for every organization.
How Placeholder Replacement Works
The replacement process follows these steps:
-
Load Agent Profile - The agent's system prompt is loaded from
Core.AgentProfiles(or uses the default if no override exists). This prompt contains placeholder tokens like{district_name}. -
Fetch External Data - The workflow calls specialized tools to fetch contextual data from external sources. For K12, this means calling the MOEOP API to get the school district's information, emergency contact details, and role assignments.
-
Build Replacement Map - The fetched data is transformed into a mapping of placeholder tokens to actual values. For example, the organization name "Springfield School District" is mapped to the
{district_name}placeholder. -
Apply Replacements - The system performs a find-and-replace operation on the prompt template, substituting each
{placeholder_name}with its corresponding value. Any placeholders without data become empty strings. -
Send to LLM - The final prompt, now containing real organization-specific information, is sent to the agent for processing.
This entire process happens automatically within the workflow, ensuring agents always have the most current and relevant context for their specific task.
K12 Annex Workflow Example
The K12 Annex workflow demonstrates how placeholders work in practice. When a user requests to edit an Emergency Operation Plan section, the workflow needs to provide the agent with specific information about that school or district.
The workflow uses the EOP snapshot tool to fetch organization and plan data from the MOEOP (Missouri Emergency Operation Plan) API. This tool retrieves everything from the district's name and address to the roles involved in emergency planning (like Principal, Safety Coordinator, etc.). All of this contextual information then becomes available for placeholder replacement.
Step 1: Fetch EOP Context Data
The first step is fetching the complete context for the emergency operation plan. The workflow calls the snapshot tool with just the plan ID, and receives back a comprehensive dataset about the organization.
// Tool: k12-get-emergency-operation-plan-snapshot
const planContext = await k12EmergencyOperationPlanSnapshotTool.execute({
emergencyOperationPlanId: 'plan-abc-123'
});
// Returns:
// {
// planId: 'plan-abc-123',
// snapshot: { ... },
// organization: {
// name: 'Springfield School District',
// type: 'DISTRICT',
// address: { city: 'Springfield', state: 'US_MO', ... },
// contactDetails: { phone: '555-1234', emergencyPhone: '911', ... }
// },
// generalUserTags: [
// { id: 'tag-1', name: 'Principal' },
// { id: 'tag-2', name: 'Safety Coordinator' }
// ]
// }
Step 2: Build Placeholder Replacements
Now that we have the organization data, the workflow transforms it into a format suitable for placeholder replacement. The buildPlaceholderReplacements function takes the raw API data and creates a mapping where each placeholder token is associated with its corresponding value.
This transformation handles several important tasks:
- Formatting addresses - Combines street, city, state, and zip into a human-readable format
- Extracting contact details - Pulls phone numbers, fax, and website from the organization's contact information
- Building role lists - Converts the array of role tags into a bulleted list format
- Distinguishing district vs facility - Determines whether to populate district or facility placeholders based on the organization type
// From: packages/domain-k12/src/k12-annex-prompt-replacements.ts
const replacements = buildPlaceholderReplacements({
planId: planContext.planId,
organization: planContext.organization,
generalUserTags: planContext.generalUserTags,
block: 'Section 3',
section: 'Emergency Contacts',
text: '<p>Current emergency contact information...</p>'
});
// Returns replacement map:
// {
// block: 'Section 3',
// section: 'Emergency Contacts',
// text: '<p>Current emergency contact information...</p>',
// list_of_roles: '- Principal\n- Safety Coordinator',
// district_name: 'Springfield School District',
// district_full_address: '123 Main St, Springfield, MO, 65801',
// district_emergency_contact_phone_number: '911',
// district_contact_phone: '555-1234',
// facility_name: '', // Empty if org type is DISTRICT
// ... // other placeholders
// }
Step 3: Apply Replacements to Prompt
With the replacement map built, the final step is applying these replacements to the agent's system prompt. The prompt stored in the database contains placeholder tokens surrounded by curly braces. The applyPlaceholderReplacements function performs a straightforward find-and-replace operation for each token.
Here's what makes this powerful: The same prompt template works for any school district. Whether it's Springfield School District in Missouri or any other organization using the system, the placeholders get filled in with that organization's specific details. The agent receives a fully contextualized prompt without anyone having to manually write custom prompts for each district.
// Agent system prompt from database (with placeholders)
const systemPrompt = `You are Eddy, the assistant for {district_name}.
Current editing context:
- Block: {block}
- Section: {section}
- Current text: {text}
Available roles in this organization:
{list_of_roles}
District information:
- Address: {district_full_address}
- Emergency Phone: {district_emergency_contact_phone_number}`;
// Apply replacements
const resolvedPrompt = applyPlaceholderReplacements(systemPrompt, replacements);
// Final prompt sent to agent:
// "You are Eddy, the assistant for Springfield School District.
//
// Current editing context:
// - Block: Section 3
// - Section: Emergency Contacts
// - Current text: <p>Current emergency contact information...</p>
//
// Available roles in this organization:
// - Principal
// - Safety Coordinator
//
// District information:
// - Address: 123 Main St, Springfield, MO, 65801
// - Emergency Phone: 911"
Supported Placeholders (K12)
The K12 Annex system provides 18 different placeholders organized into four categories. Each placeholder pulls specific information from either the MOEOP API or the incoming edit request.
Understanding these placeholders is crucial for writing effective agent prompts. When crafting a prompt in the Core.AgentProfiles table, you can reference any of these placeholders knowing they'll be replaced with real data at runtime.
Code Location: packages/domain-k12/src/k12-annex-prompt-replacements.ts
Annex Editing Context
These placeholders provide information about the specific section of the EOP being edited. They come from the user's edit request rather than the MOEOP API.
| Placeholder | Description | Source |
|---|---|---|
{block} | Current block/group name being edited (e.g., "Section 3", "Introduction") | Request form data |
{section} | Current section/field name being edited (e.g., "Emergency Contacts", "Procedures") | Request form data |
{text} | Current text/content being edited (the actual HTML or text content of the field) | Request form data |
Roles
This placeholder provides the list of roles involved in the emergency plan, such as Principal, Safety Coordinator, School Nurse, etc. The system formats this as a bulleted markdown list.
| Placeholder | Description | Source |
|---|---|---|
{list_of_roles} | Bulleted list of role titles (e.g., "- Principal\n- Safety Coordinator") | MOEOP API → generalUserTags |
District-Level Information
These placeholders contain information about the school district. They're populated when the organization type is "DISTRICT" or when a facility has a parent district organization.
| Placeholder | Description | Source |
|---|---|---|
{district_name} | District organization name (e.g., "Springfield School District") | MOEOP API → organization.name |
{district_website} | District website URL (e.g., "https://springfield.k12.mo.us") | MOEOP API → organization.contactDetails.website |
{district_full_address} | Full formatted address (e.g., "123 Main St, Springfield, MO, 65801") | MOEOP API → organization.address (formatted) |
{district_emergency_contact_phone_number} | Emergency contact phone number | MOEOP API → organization.contactDetails.emergencyPhone |
{district_contact_phone} | General contact phone number | MOEOP API → organization.contactDetails.phone |
{district_contact_fax} | Fax number | MOEOP API → organization.contactDetails.fax |
Facility-Level Information
These placeholders contain information about a specific school facility. They're populated when the organization type is "FACILITY" or "SCHOOL".
| Placeholder | Description | Source |
|---|---|---|
{facility_name} | Facility/school name (e.g., "Lincoln Elementary School") | MOEOP API → organization.name (if type=FACILITY) |
{facility_website} | Facility website URL | MOEOP API → organization.contactDetails.website |
{facility_full_address} | Full formatted address | MOEOP API → organization.address (formatted) |
{facility_emergency_contact_phone_number} | Emergency contact phone for the facility | MOEOP API → organization.contactDetails.emergencyPhone |
{facility_contact_phone} | General contact phone for the facility | MOEOP API → organization.contactDetails.phone |
{facility_contact_fax} | Fax number for the facility | MOEOP API → organization.contactDetails.fax |
How District vs. Facility Works: The system automatically determines which placeholders to populate based on the organization's type:
-
District Organizations (
type === 'DISTRICT'): The district placeholders are filled with the organization's information, while facility placeholders remain empty. This is typical for district-level EOPs. -
Facility Organizations (
type === 'FACILITY'or'SCHOOL'): The facility placeholders are filled with the organization's information. If the facility has a parent district, the district placeholders are filled from theparentOrganizationdata. Otherwise, district placeholders remain empty.
This allows prompts to intelligently reference the appropriate organizational level without needing separate prompt templates for districts vs. schools.
Empty Placeholder Handling
The system gracefully handles missing data by replacing placeholders with empty strings rather than showing errors or leaving the placeholder tokens visible.
Why This Matters: Not all organizations have complete information. A school might not have a website listed, or a district-level EOP might not have facility information. The prompt needs to work regardless of which fields are populated.
When a placeholder has no corresponding data, it simply disappears from the final prompt:
// If no roles exist in the plan
{list_of_roles} → ""
// The prompt will have an empty line where roles would be listed
// If organization has no website
{district_website} → ""
// The website field will be blank
The system also removes any placeholders that aren't recognized (placeholders that don't exist in the official list):
// If a prompt contains {unknown_placeholder}
applyPlaceholderReplacements(prompt, replacements);
// Result: {unknown_placeholder} is removed (replaced with empty string)
This behavior ensures prompts always work, even if someone adds a placeholder token that doesn't exist. It fails gracefully rather than breaking the agent.
Example: Creating a Prompt with Placeholders
Database Profile (Core.AgentProfiles):
AgentId: k12-safety-agent
SystemPrompt: 'You are Eddy, the safety planning assistant for {district_name}.
You are helping to edit the Emergency Operation Plan.
Current Editing Context:
- Section: {section}
- Block: {block}
The following roles are involved in this plan:
{list_of_roles}
District Contact Information:
- Phone: {district_contact_phone}
- Emergency: {district_emergency_contact_phone_number}
- Address: {district_full_address}
Provide clear, actionable guidance appropriate for K12 school safety planning.'
Runtime Execution:
// 1. Workflow fetches EOP snapshot
const snapshot = await k12EmergencyOperationPlanSnapshotTool.execute({
emergencyOperationPlanId: planId
});
// 2. Build placeholder context
const context = {
planId: snapshot.planId,
organization: snapshot.organization,
generalUserTags: snapshot.generalUserTags,
block: request.form.block,
section: request.form.section,
text: request.form.text
};
// 3. Load agent profile
const profile = await agentProfilesService.getCurrent('k12-safety-agent');
// 4. Resolve placeholders in system prompt
const { prompt: resolvedPrompt } = resolvePromptPlaceholders(
profile.SystemPrompt,
context
);
// 5. Create agent with resolved prompt
const agent = new Agent({
instructions: resolvedPrompt,
model: profile.LlmOverridesJson?.deploymentName || defaultModel
});
Implementation Details
Core Functions (packages/domain-k12/src/k12-annex-prompt-replacements.ts):
// Build all replacements from context
buildPlaceholderReplacements(context: PlaceholderContext): Record<PlaceholderToken, string>
// Apply replacements to prompt string
applyPlaceholderReplacements(prompt: string, replacements: Record<PlaceholderToken, string>): string
// Convenience function (build + apply)
resolvePromptPlaceholders(prompt: string, context: PlaceholderContext): {
prompt: string;
replacements: Record<PlaceholderToken, string>;
}
Data Source (packages/domain-k12/src/emergency-operation-plan-snapshot-tool.ts):
// Fetches EOP context from MOEOP API
k12EmergencyOperationPlanSnapshotTool.execute({
emergencyOperationPlanId: string
}): Promise<{
planId: string;
snapshot: SnapshotRecord | null;
configuration: ConfigurationRecord | null;
organization: OrganizationRecord | null;
generalUserTags: { id: string; name: string }[];
}>
Best Practices for Placeholders
-
Always Provide Fallbacks: Design prompts to work even when placeholders are empty
// Good: Works with or without roles
Available roles: {list_of_roles}
// Bad: Breaks if list_of_roles is empty
You must assign this to one of these roles: {list_of_roles} -
Test with Missing Data: Verify prompt quality when external APIs return null/empty values
-
Document Custom Placeholders: If adding new placeholders, update
PLACEHOLDER_TOKENSconstant -
Use Descriptive Names: Placeholder names should clearly indicate their source and purpose
{district_name} // Good: Clear hierarchy
{name} // Bad: Ambiguous -
Validate Before Replacement: Ensure context data is properly sanitized to avoid injection issues
Caching and Performance
Agent-Level Caching
Agents cache their loaded profiles and only reload when the ProfileHash changes:
let cachedProfileHash = null;
let cachedAgent = null;
export async function getK12SafetyAgent() {
const currentProfile = await agentProfilesService.getCurrent('k12-safety-agent');
// Check if profile changed
if (currentProfile?.ProfileHash !== cachedProfileHash) {
// Rebuild agent with new profile
cachedAgent = new Agent({
instructions: currentProfile.SystemPrompt,
model: currentProfile.LlmOverridesJson?.deploymentName,
// ...
});
cachedProfileHash = currentProfile.ProfileHash;
}
return cachedAgent;
}
Database-Level Performance
- Unique filtered index on
(AgentId, IsCurrent)makes lookups fast - Only current rows are queried during agent initialization
- Historical rows don't impact runtime performance
Troubleshooting
Agent Not Using Override
Symptom: Agent uses default prompt despite database profile existing
Checklist:
-
Verify profile has
IsCurrent = 1:SELECT AgentId, IsCurrent, VersionNumber, ProfileHash
FROM Core.AgentProfiles
WHERE AgentId = 'your-agent-id'; -
Check schema name matches environment:
- Confirm
DB_CORE_SCHEMAenv var (default:Core) - Ensure admin tools, runtime, and database use same schema
- Confirm
-
Verify agent ID matches exactly:
- Check agent's registry ID in code
- Ensure no typos in
AgentIdcolumn
-
Enable debug logging:
AGENT_PROFILES_DEBUG=true npm run dev
Admin UI Shows Empty Dropdown
Symptom: Agent profile dropdown is empty
Cause: No profiles with IsCurrent = 1 exist
Solution:
-
Run DB health check:
curl -X POST "$MASTRA_BASE_URL/admin/tools/db-health-check" \
-H "Content-Type: application/json" \
-d '{"data":{"schema":"Core","tables":["Core.AgentProfiles"]}}' -
Bootstrap initial profiles:
curl -X POST "$MASTRA_BASE_URL/admin/tools/update-agent-profile" \
-H "Content-Type: application/json" \
-d '{"data":{"agentId":"k12-safety-agent","systemPrompt":"Initial prompt","updatedBy":"admin@example.com"}}' -
Verify schema configuration matches across all services
Profile Updates Not Taking Effect
Symptom: Updated profile in database but agent still uses old prompt
Possible Causes:
- Cache not invalidated: Restart the application
- Multiple instances: If running multiple instances, each must reload
- Hash unchanged: If content is identical, no update occurs (by design)
- Wrong environment: Confirm you updated the correct database
Debugging:
# Enable agent override debugging
K12_DEBUG_AGENT_OVERRIDES=true npm run dev
# Check current profile hash
SELECT AgentId, ProfileHash, SystemPrompt
FROM Core.AgentProfiles
WHERE IsCurrent = 1 AND AgentId = 'your-agent-id';
Best Practices
1. Use Tools, Not Manual SQL
Always use admin tools for updates to ensure:
- Proper versioning
- Hash calculation
- Current flag management
- Audit trail
2. Test in Non-Production First
Before updating production profiles:
- Test prompt changes in dev/staging
- Verify agent behavior with new configuration
- Check for unintended side effects
3. Document Prompt Changes
Include meaningful UpdatedBy and use version control for major prompt revisions:
git commit -m "Update k12-safety-agent prompt to improve EOP formatting"
4. Monitor Profile Performance
Track metrics after profile updates:
- Response quality
- Token usage
- Error rates
- User feedback
5. Keep Defaults as Fallback
Always maintain sensible default prompts in code as fallback when database is unavailable.
Environment Variables
Key environment variables for agent profile system:
# Core schema name (default: Core)
DB_CORE_SCHEMA=Core
# Enable debug logging for profile loading
AGENT_PROFILES_DEBUG=true
# K12-specific agent debugging
K12_DEBUG_AGENT_OVERRIDES=true
# Database connection
AZURE_SQL_CONNECTION_STRING=Server=...
Related Documentation
- Database: Agent Profiles - Database schema and table details
- Agents (Detailed) (/agents/detailed) - Complete agent registry with profile IDs
- K12 Agents - K12-specific agent documentation
- TAP Agents - TAP-specific agent documentation