Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automatically applies when creating AI tool functions. Ensures proper schema design, input validation, error handling, context access, and comprehensive testing.
.claude/skills/majiayu000-tool-design-pattern/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 180% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 247% | 0% |
When creating tools for AI agents (LangChain, function calling, etc.), follow these design patterns.
pythonfrom langchain.tools import tool from pydantic import BaseModel, Field from typing import Optional import logging logger = logging.getLogger(__name__) # 1. Define input schema class SearchInput(BaseModel): """Input schema for search tool.""" query: str = Field(..., description="Search query string") max_results: int = Field( default=10, ge=1, le=100, description="Maximum number of results to return" ) filter_type: Optional[str] = Field( None, description="Optional filter type (e.g., 'recent', 'popular')" ) # 2. Implement tool function @tool(args_schema=SearchInput) def search_database(query: str, max_results: int = 10, filter_type: Optional[str] = None) -> str: """ Search database for relevant information. Use this tool when user asks to find, search, or look up information. Returns JSON string with search results. Args: query: Search query string max_results: Maximum number of results (1-100) filter_type: Optional filter (recent, popular) Returns: JSON string with results or error message """ request_id = str(uuid.uuid4()) try: # Log tool invocation logger.info( f"TOOL_CALL: search_database | " f"query={query[:50]} | " f"request_id={request_id}" ) # Validate inputs if not query or not query.strip(): return json.dumps({ "error": "Query cannot be empty", "request_id": request_id }) # Execute search results = _execute_search(query, max_results, filter_type) # Return structured response return json.dumps({ "results": results, "total": len(results), "request_id": request_id }) except Exception as e: logger.error(f"Tool error | request_id={request_id}", exc_info=True) return json.dumps({ "error": "Search failed", "request_id": request_id, "timestamp": datetime.now().isoformat() }) # 3. Helper implementation def _execute_search(query: str, max_results: int, filter_type: Optional[str]) -> List[dict]: """Internal search implementation.""" # Actual search logic pass
pythonfrom pydantic import BaseModel, Field, field_validator from typing import Literal, Optional class EmailToolInput(BaseModel): """Well-designed tool input schema.""" recipient: str = Field( ..., description="Email address of recipient (e.g., user@example.com)" ) subject: str = Field( ..., description="Email subject line", min_length=1, max_length=200 ) body: str = Field( ..., description="Email body content", min_length=1 ) priority: Literal["low", "normal", "high"] = Field( default="normal", description="Email priority level" ) attach_invoice: bool = Field( default=False, description="Whether to attach invoice PDF" ) @field_validator('recipient') @classmethod def validate_email(cls, v: str) -> str: if '@' not in v: raise ValueError('Invalid email address') return v.lower() class Config: json_schema_extra = { "example": { "recipient": "customer@example.com", "subject": "Order Confirmation", "body": "Thank you for your order!", "priority": "normal", "attach_invoice": True } }
pythonimport uuid from datetime import datetime import json @tool def robust_tool(param: str) -> str: """Tool with comprehensive error handling.""" request_id = str(uuid.uuid4()) # Input validation if not param: return json.dumps({ "error": "Parameter is required", "error_code": "INVALID_INPUT", "request_id": request_id, "timestamp": datetime.now().isoformat() }) try: # Main logic result = process_data(param) return json.dumps({ "success": True, "data": result, "request_id": request_id }) except ValidationError as e: return json.dumps({ "error": str(e), "error_code": "VALIDATION_ERROR", "request_id": request_id, "timestamp": datetime.now().isoformat() }) except ExternalAPIError as e: logger.error(f"External API failed | request_id={request_id}", exc_info=True) return json.dumps({ "error": "External service unavailable", "error_code": "SERVICE_ERROR", "request_id": request_id, "timestamp": datetime.now().isoformat() }) except Exception as e: logger.error(f"Unexpected error | request_id={request_id}", exc_info=True) return json.dumps({ "error": "An unexpected error occurred", "error_code": "INTERNAL_ERROR", "request_id": request_id, "timestamp": datetime.now().isoformat() })
pythonfrom typing import Any @tool def context_aware_tool(query: str, context: Optional[dict] = None) -> str: """ Tool that uses conversation context. Args: query: User query context: Optional context from agent (user_id, session_id, etc.) Returns: JSON string with results """ # Extract context safely user_id = context.get("user_id") if context else None session_id = context.get("session_id") if context else None logger.info( f"Tool called | user_id={user_id} | " f"session_id={session_id} | query={query[:50]}" ) # Use context in logic if user_id: # Personalized response results = fetch_user_data(user_id, query) else: # Generic response results = fetch_generic_data(query) return json.dumps({"results": results})
pythonfrom langchain.tools import tool import httpx @tool async def async_api_tool(query: str) -> str: """ Async tool for external API calls. Use async for I/O-bound operations to improve performance. """ request_id = str(uuid.uuid4()) try: async with httpx.AsyncClient() as client: response = await client.get( f"https://api.example.com/search", params={"q": query}, timeout=10.0 ) response.raise_for_status() return json.dumps({ "results": response.json(), "request_id": request_id }) except httpx.TimeoutException: return json.dumps({ "error": "Request timed out", "request_id": request_id }) except httpx.HTTPStatusError as e: return json.dumps({ "error": f"API error: {e.response.status_code}", "request_id": request_id })
pythonimport pytest from unittest.mock import patch, Mock def test_search_tool_success(): """Test successful search.""" result = search_database(query="test query", max_results=5) data = json.loads(result) assert "results" in data assert "request_id" in data assert data["total"] >= 0 def test_search_tool_empty_query(): """Test validation error.""" result = search_database(query="", max_results=10) data = json.loads(result) assert "error" in data assert data["error"] == "Query cannot be empty" @patch('module.httpx.get') def test_async_tool_timeout(mock_get): """Test timeout handling.""" mock_get.side_effect = httpx.TimeoutException("Timeout") result = async_api_tool(query="test") data = json.loads(result) assert "error" in data assert "timed out" in data["error"].lower() @pytest.mark.asyncio async def test_async_tool_success(): """Test async tool success path.""" with patch('module.httpx.AsyncClient') as mock_client: mock_response = Mock() mock_response.json.return_value = {"data": "test"} mock_response.raise_for_status = Mock() mock_client.return_value.__aenter__.return_value.get.return_value = mock_response result = await async_api_tool("test query") data = json.loads(result) assert "results" in data
python# ❌ No input schema @tool def bad_tool(param): # No type hints, no schema! pass # ❌ Returning plain strings @tool def bad_tool(param: str) -> str: return "Error: something went wrong" # Not structured! # ❌ No error handling @tool def bad_tool(param: str) -> str: result = external_api_call(param) # What if this fails? return result # ❌ Exposing sensitive data @tool def bad_tool(user_id: str) -> str: logger.info(f"Processing user {user_id}") # PII leak! return json.dumps({"user_id": user_id}) # ❌ No validation @tool def bad_tool(email: str) -> str: send_email(email) # What if email is invalid? return "Sent"
When creating tools:
@tool decorator with args_schema| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,477 | 23,690 | +22% | 1 | 1 | 0% | 4,094 | 6,689 | +63% | 0 | 0 | — |
case-02 | fail→pass | 26,652 | 22,310 | -16% | 1 | 1 | 0% | 4,449 | 7,496 | +68% | 0 | 0 | — |
case-03 | pass→pass | 11,640 | 12,833 | +10% | 1 | 1 | 0% | 1,362 | 5,281 | +288% | 0 | 0 | — |
case-04 | pass→pass | 17,989 | 18,740 | +4% | 1 | 1 | 0% | 2,610 | 5,569 | +113% | 0 | 0 | — |
case-05 | pass→fail | 18,923 | 21,312 | +13% | 1 | 1 | 0% | 2,208 | 6,616 | +200% | 0 | 0 | — |
case-06 | pass→pass | 11,502 | 19,594 | +70% | 1 | 1 | 0% | 2,074 | 5,032 | +143% | 0 | 0 | — |
case-12 | pass→pass | 13,510 | 17,352 | +28% | 1 | 1 | 0% | 1,551 | 5,830 | +276% | 0 | 0 | — |
case-07 | pass→pass | 13,735 | 8,465 | -38% | 1 | 1 | 0% | 1,550 | 4,481 | +189% | 0 | 0 | — |
case-08 | fail→pass | 11,360 | 25,808 | +127% | 1 | 1 | 0% | 2,190 | 5,551 | +153% | 0 | 0 | — |
case-09 | pass→pass | 11,049 | 17,729 | +60% | 1 | 1 | 0% | 990 | 6,190 | +525% | 0 | 0 | — |
case-10 | fail→pass | 19,049 | 16,239 | -15% | 1 | 1 | 0% | 2,173 | 6,094 | +180% | 0 | 0 | — |
case-11 | fail→fail | 19,118 | 42,794 | +124% | 1 | 1 | 0% | 3,502 | 7,258 | +107% | 0 | 0 | — |
case-13 | fail→pass | 33,609 | 18,812 | -44% | 1 | 1 | 0% | 1,801 | 6,245 | +247% | 0 | 0 | — |
case-14 | pass→pass | 32,335 | 16,115 | -50% | 1 | 1 | 0% | 2,663 | 6,210 | +133% | 0 | 0 | — |
case-15 | pass→pass | 17,288 | 16,897 | -2% | 1 | 1 | 0% | 2,342 | 6,308 | +169% | 0 | 0 | — |
case-16 | pass→pass | 18,415 | 24,295 | +32% | 1 | 1 | 0% | 2,337 | 6,928 | +196% | 0 | 0 | — |
case-17 | fail→pass | 10,008 | 13,216 | +32% | 1 | 1 | 0% | 794 | 4,150 | +423% | 0 | 0 | — |
case-18 | pass→pass | 13,763 | 21,605 | +57% | 1 | 1 | 0% | 2,737 | 7,547 | +176% | 0 | 0 | — |
case-19 | pass→pass | 12,069 | 16,496 | +37% | 1 | 1 | 0% | 2,076 | 4,995 | +141% | 0 | 0 | — |
case-20 | pass→pass | 12,484 | 16,198 | +30% | 1 | 1 | 0% | 2,211 | 5,835 | +164% | 0 | 0 | — |
case-21 | pass→pass | 25,204 | 23,518 | -7% | 1 | 1 | 0% | 3,846 | 6,513 | +69% | 0 | 0 | — |
case-22 | fail→pass | 17,897 | 15,115 | -16% | 1 | 1 | 0% | 2,387 | 5,661 | +137% | 0 | 0 | — |
case-23 | fail→pass | 11,205 | 12,694 | +13% | 1 | 1 | 0% | 1,535 | 4,161 | +171% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +30 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.