Designing Tool Schemas That Models Won't Misuse
Design tool schemas that prevent LLM misuse. Use clear descriptions, strict type validation, and defensive patterns to keep agents on track.
Published on • September 8, 2026
AI Assistant

The schema is the contract between your tool and the LLM. A poorly designed schema invites misuse — wrong parameter types, missing required fields, ambiguous descriptions that lead to incorrect tool calls. A well-designed schema guides the model toward correct usage and rejects invalid inputs before they cause problems.
Why Schema Design Matters
LLMs don’t read your code. They read your schema. When deciding which tool to call and with what parameters, the model relies entirely on:
- Tool name and description
- Parameter names, types, and descriptions
- Required vs optional fields
- Enum constraints
A vague description leads to vague usage. A missing constraint leads to invalid calls. Schema design is agent safety.
Principle 1: Descriptive Names and Descriptions
# Bad: Vague
@tool
def search(q: str) -> str:
"""Searches stuff."""
# Good: Specific
@tool
def search_knowledge_base(
query: str,
category: str = "all",
max_results: int = 5
) -> str:
"""Search the internal knowledge base for relevant documentation.
Use this tool when the user asks questions about:
- Product features or capabilities
- How-to guides and tutorials
- API documentation
- Troubleshooting steps
Do NOT use this tool for:
- Current events or news (use web_search instead)
- Personal opinions or advice
- Tasks that don't require information retrieval
"""
Principle 2: Strict Type Validation with Pydantic
from pydantic import BaseModel, Field, validator
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class CreateTaskInput(BaseModel):
title: str = Field(
...,
min_length=1,
max_length=200,
description="Clear, actionable task title"
)
description: str = Field(
default="",
max_length=2000,
description="Detailed description of what needs to be done"
)
priority: Priority = Field(
default=Priority.MEDIUM,
description="Task priority level"
)
assignee_email: str = Field(
...,
pattern=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
description="Email of the person responsible for this task"
)
due_date: str = Field(
...,
pattern=r'^\d{4}-\d{2}-\d{2}$',
description="Due date in YYYY-MM-DD format"
)
@validator('title')
def title_must_be_actionable(cls, v):
if not any(v.lower().startswith(w) for w in ['create', 'update', 'fix', 'implement', 'review', 'test']):
raise ValueError('Title should start with an action verb (create, update, fix, implement, review, test)')
return v
@tool(args_schema=CreateTaskInput)
def create_task(
title: str,
description: str,
priority: Priority,
assignee_email: str,
due_date: str
) -> str:
"""Create a new task in the project management system."""
# Tool implementation
pass
Principle 3: Constrained Enums
from enum import Enum
class EmailTone(str, Enum):
"""The tone for the email."""
PROFESSIONAL = "professional"
FRIENDLY = "friendly"
FORMAL = "formal"
URGENT = "urgent"
class EmailLength(str, Enum):
"""Desired email length."""
SHORT = "short" # 2-3 sentences
MEDIUM = "medium" # 1 paragraph
DETAILED = "detailed" # Multiple paragraphs
@tool
def compose_email(
recipient: str,
subject: str,
key_points: list[str],
tone: EmailTone = EmailTone.PROFESSIONAL,
length: EmailLength = EmailLength.MEDIUM
) -> str:
"""Compose a professional email.
Args:
recipient: Email address of the recipient
subject: Clear, concise email subject line
key_points: List of main points to cover in the email
tone: Desired tone (professional, friendly, formal, urgent)
length: Desired length (short, medium, detailed)
"""
pass
Principle 4: Clear Error Messages
from pydantic import ValidationError
@tool
def safe_create_task(title: str, priority: str) -> str:
"""Create a task with validation."""
try:
validated = CreateTaskInput(title=title, priority=priority)
except ValidationError as e:
# Return structured error that helps the model correct itself
return json.dumps({
"error": "validation_failed",
"details": [
{
"field": err["loc"][0],
"message": err["msg"],
"suggestion": get_correction_suggestion(err)
}
for err in e.errors()
]
})
def get_correction_suggestion(error: dict) -> str:
"""Generate helpful correction suggestions."""
suggestions = {
"value_error": "Check the format and try again",
"type_error": f"Expected type: {error.get('type', 'unknown')}",
"string_too_long": f"Maximum length is {error.get('ctx', {}).get('max_length', 'unknown')} characters",
"string_too_short": "This field cannot be empty",
}
return suggestions.get(error.get("type", ""), "Please check your input")
Principle 5: Prevent Dangerous Combinations
from pydantic import model_validator
class SendEmailInput(BaseModel):
recipient: str
subject: str
body: str
send_immediately: bool = False
requires_approval: bool = False
@model_validator(mode='after')
def check_dangerous_combinations(self):
# Prevent immediate sending without approval
if self.send_immediately and not self.requires_approval:
if any(word in self.body.lower() for word in ['contract', 'agreement', 'legal', 'payment']):
raise ValueError(
'Emails containing legal/financial terms require approval. '
'Set requires_approval=true'
)
return self
Principle 6: Tool Naming Conventions
# Consistent naming helps models understand tool purposes
tools = [
search_knowledge_base, # Verb + Noun
create_task, # Verb + Noun
update_user_profile, # Verb + Noun + Context
delete_inactive_sessions, # Verb + Adjective + Noun
generate_report, # Verb + Noun
validate_email_address, # Verb + Noun
]
# Avoid ambiguous names
# Bad: process_data (what kind of processing?)
# Good: encrypt_pii_data (specific action and target)
Principle 7: Documentation as Contract
@tool
def migrate_database(
source_env: str,
target_env: str,
tables: list[str],
dry_run: bool = True
) -> str:
"""Migrate database tables between environments.
IMPORTANT: This tool makes irreversible changes to production data.
Always use dry_run=true first to preview changes.
Only set dry_run=false after reviewing the preview.
Args:
source_env: Source environment (dev, staging, production)
target_env: Target environment (staging, production)
tables: List of table names to migrate
dry_run: If true, show what would change without making changes
Returns:
JSON with migration preview or confirmation
Raises:
ValidationError: If environments are invalid or same
PermissionError: If migrating to production without approval
"""
if source_env == target_env:
return json.dumps({"error": "Source and target must be different"})
if target_env == "production" and not dry_run:
return json.dumps({
"error": "Production writes require explicit approval",
"hint": "Set dry_run=true first, then call again with dry_run=false"
})
Testing Tool Schemas
import pytest
from pydantic import ValidationError
def test_create_task_validation():
# Valid input
valid = CreateTaskInput(
title="Implement user authentication",
priority="high",
assignee_email="dev@company.com",
due_date="2026-09-15"
)
assert valid.priority == Priority.HIGH
# Invalid: no action verb
with pytest.raises(ValidationError):
CreateTaskInput(
title="authentication thing",
priority="high",
assignee_email="dev@company.com",
due_date="2026-09-15"
)
# Invalid: bad email
with pytest.raises(ValidationError):
CreateTaskInput(
title="Fix login bug",
priority="high",
assignee_email="not-an-email",
due_date="2026-09-15"
)
def test_dangerous_combination_blocked():
# Should block immediate send with legal terms
email = SendEmailInput(
recipient="client@example.com",
subject="Contract Review",
body="Please review the attached contract agreement",
send_immediately=True,
requires_approval=False
)
# Validation should fail
Conclusion
Tool schema design is agent safety engineering. Use Pydantic for strict validation, descriptive names and enums for clarity, clear error messages for self-correction, and model validators to prevent dangerous combinations. Treat your schemas as contracts — test them, version them, and document them thoroughly.