Skip to Content
DocsToolsCreating Custom Tools

Creating Custom Tools

Custom tools let you extend agent capabilities beyond built-in Frappe operations. Link your own Python functions to agents and unlock unlimited possibilities.

What Are Custom Tools?

Custom tools are your own Python functions that agents can call. They enable agents to perform specialized tasks that built-in tools don’t cover.

Complex CalculationsPerform complex calculations
External IntegrationsIntegrate with external systems
Business LogicImplement business logic
Custom Data ProcessingProcess data in custom ways
Specialized WorkflowsExecute specialized workflows

If built-in tools are the standard toolkit, custom tools are your custom-made specialty tools.

When to Create Custom Tools

Choose the right approach based on whether your task needs custom logic or can use standard operations.

Create custom tools when you need to:

Business-Specific LogicExecute business-specific logic
Advanced CalculationsPerform calculations beyond simple queries
Custom API IntegrationIntegrate with external APIs with custom authentication
Complex Data ProcessingProcess data in ways built-in tools can’t
Multi-Step WorkflowsImplement multi-step workflows
Complex ValidationValidate complex conditions
Specialized FormattingFormat or transform data specially

Use built-in tools when:

Standard CRUDStandard CRUD operations suffice
Simple HTTPSimple HTTP requests work
No Custom LogicNo custom logic needed
Covered Use CaseBuilt-in tools already cover the use case

How Custom Tools Work

Custom tools bridge your Python code and Huf agents through a defined lifecycle.

The Flow

The agent discovers and executes your function through these stages.

Function Requirements

Your Python function must meet these baseline requirements to be callable by an agent.

ImportableBe importable (in a module Frappe can access)
Defined ParametersAccept defined parameters
Serializable ReturnsReturn JSON-serializable data
Graceful ErrorsHandle errors gracefully
Permission AwareRespect Frappe permissions (if needed)

Creating a Custom Tool

Build and connect a custom tool in four stages.

01
Step 1: Write the Python Function

Create a function in your Frappe app:

File: my_app/api/tools.py

import frappe def calculate_customer_lifetime_value(customer_id): """ Calculate the total lifetime value for a customer. Args: customer_id (str): The customer ID Returns: dict: Customer LTV details including total value and order count """ # Query all sales invoices for this customer invoices = frappe.get_all( "Sales Invoice", filters={"customer": customer_id, "docstatus": 1}, fields=["grand_total"] ) # Calculate totals total_value = sum(inv.grand_total for inv in invoices) order_count = len(invoices) # Calculate average order value avg_order = total_value / order_count if order_count > 0 else 0 return { "customer_id": customer_id, "lifetime_value": total_value, "order_count": order_count, "average_order_value": avg_order }

Key Points:

DocstringBecomes part of tool description (important!)
Type HintsHelp document expected parameters
Return DictAgent can easily parse the result
Error HandlingHandle edge cases (division by zero, missing data)
02
Step 2: Create Agent Tool Function

Create the Agent Tool Function record that points to your Python function.

Navigate to: Desk → Huf → Agent Tool Function

Click New and fill in:

Agent Tool Function
Tool Namecalculate_customer_ltv
DescriptionCalculate the lifetime value for a customer including total revenue, order count, and average order value across all paid invoices.
TypesCustom Function
Function Pathmy_app.api.tools.calculate_customer_lifetime_value
Parameters
customer_idString (Required) — The customer ID to calculate LTV for

Guidelines:

  • Use descriptive snake_case names
  • Should match function name (not required but clearer)
  • Be specific—agent uses this to decide when to use the tool
  • Mention what data it returns
  • Include any limitations
  • Full dotted path to the function must be importable: from my_app.api.tools import calculate_customer_lifetime_value

Save the tool

03
Step 3: Assign to Agent

Link the tool to the agent that should be able to use it.

  1. Open your agent (Desk → Huf → Agent)
  2. Scroll to Agent Tool table
  3. Click Add Row
  4. Select Tool Function: calculate_customer_ltv
  5. Save the agent
04
Step 4: Test

Use Agent Chat or run the agent:

User
What’s the lifetime value for customer CUST-001?
Agent
Recognizes need for LTV data
Tool Call
calculate_customer_ltv(“CUST-001”)
Tool Result
{"lifetime_value": 45230.50, "order_count": 12, ...}
Agent
Customer CUST-001 has a lifetime value of $45,230.50 across 12 orders, with an average order value of $3,769.21.

Parameter Types

When defining parameters in Agent Tool Function, map each type to its Python equivalent.

TypePython TypeDescriptionExample
StringstrText values"CUST-001", "high"
IntintIntegers42, 100, -5
FloatfloatDecimals3.14, 99.99
BooleanboolTrue/FalseTrue, False
ObjectdictJSON objects{"key": "value"}
ArraylistJSON arrays["item1", "item2"]

These signatures show how to declare parameters for each type:

def search_products(category: str, min_price: float, in_stock: bool): """Search products with filters.""" pass def batch_process_orders(order_ids: list): """Process multiple orders at once.""" pass def create_customer_profile(data: dict): """Create customer with complex nested data.""" pass

Advanced Examples

These examples show common custom tool patterns.

Example 1: External API Integration

Use this pattern when you need to call an external API from an agent.

import requests import frappe def get_weather_forecast(city: str): """ Get weather forecast for a city from OpenWeather API. Args: city (str): City name Returns: dict: Weather forecast data """ api_key = frappe.conf.get("openweather_api_key") response = requests.get( f"https://api.openweathermap.org/data/2.5/weather", params={"q": city, "appid": api_key, "units": "metric"} ) if response.status_code == 200: data = response.json() return { "city": city, "temperature": data["main"]["temp"], "condition": data["weather"][0]["description"], "humidity": data["main"]["humidity"] } else: frappe.throw(f"Weather API error: {response.status_code}")
Agent InstructionsWeather Agent Instructions

When user asks about weather:

  1. Extract city name
  2. Use get_weather_forecast tool
  3. Present temperature, condition, and humidity in a friendly format

Example 2: Complex Calculation

Use this pattern to run calculations over Frappe data.

import frappe from datetime import datetime, timedelta def calculate_reorder_quantity(item_code: str, days_ahead: int = 30): """ Calculate optimal reorder quantity based on historical consumption. Args: item_code (str): Item code days_ahead (int): Days to plan ahead (default: 30) Returns: dict: Reorder recommendations """ # Get current stock stock_qty = frappe.db.get_value("Bin", {"item_code": item_code}, "actual_qty" ) or 0 # Get item details item = frappe.get_doc("Item", item_code) # Calculate average daily consumption (last 90 days) ninety_days_ago = datetime.now() - timedelta(days=90) consumed = frappe.db.sql(""" SELECT SUM(actual_qty) FROM `tabStock Ledger Entry` WHERE item_code = %s AND posting_date >= %s AND actual_qty < 0 """, (item_code, ninety_days_ago))[0][0] or 0 daily_consumption = abs(consumed) / 90 # Calculate needed quantity needed_qty = daily_consumption * days_ahead reorder_qty = max(0, needed_qty - stock_qty) # Round up to nearest safety stock level if item.safety_stock: reorder_qty = max(reorder_qty, item.safety_stock) return { "item_code": item_code, "current_stock": stock_qty, "daily_consumption": round(daily_consumption, 2), "days_ahead": days_ahead, "recommended_reorder_qty": round(reorder_qty, 2), "estimated_stockout_days": round(stock_qty / daily_consumption if daily_consumption > 0 else 999, 1) }

Example 3: Validation Logic

Use this pattern to validate documents and flag issues.

import frappe def validate_invoice_pricing(invoice_name: str): """ Validate invoice pricing against price list and flag anomalies. Args: invoice_name (str): Sales Invoice name Returns: dict: Validation results with any issues found """ invoice = frappe.get_doc("Sales Invoice", invoice_name) issues = [] for item in invoice.items: # Get price list rate price_list_rate = frappe.db.get_value( "Item Price", { "item_code": item.item_code, "price_list": invoice.selling_price_list }, "price_list_rate" ) if not price_list_rate: issues.append({ "item": item.item_code, "issue": "No price list rate found", "severity": "high" }) continue # Check for significant variance variance_pct = ((item.rate - price_list_rate) / price_list_rate) * 100 if abs(variance_pct) > 10: issues.append({ "item": item.item_code, "issue": f"Price varies {variance_pct:.1f}% from price list", "expected": price_list_rate, "actual": item.rate, "severity": "medium" if abs(variance_pct) < 20 else "high" }) return { "invoice": invoice_name, "is_valid": len(issues) == 0, "issues_found": len(issues), "issues": issues }

Best Practices

Follow these guidelines to build reliable, maintainable custom tools.

Function Design

Well-designed functions are easier for agents to understand and safer to maintain.

Do
  • Write clear, focused functions (single responsibility)
  • Use descriptive function and parameter names
  • Include comprehensive docstrings
  • Return structured data (dicts with clear keys)
  • Handle errors and edge cases
  • Add type hints for parameters
Don't
  • Create mega-functions that do everything
  • Use vague parameter names (`data`, `info`, `x`)
  • Return raw database objects (serialize them)
  • Forget error handling
  • Make functions depend on global state
  • Use side effects without documenting them

Error Handling

Handle errors gracefully so agents can report meaningful failures.

def get_customer_balance(customer_id: str): """Get customer outstanding balance.""" # Validate input if not customer_id: frappe.throw("Customer ID is required") # Check if customer exists if not frappe.db.exists("Customer", customer_id): frappe.throw(f"Customer {customer_id} not found") try: balance = frappe.db.get_value("Customer", customer_id, "outstanding_amount") return {"customer_id": customer_id, "balance": balance or 0} except Exception as e: frappe.log_error(f"Error getting balance for {customer_id}: {str(e)}") frappe.throw(f"Failed to retrieve customer balance: {str(e)}")
Meaningful ErrorsAgent gets meaningful error messages
Helpful FeedbackUser sees helpful feedback
Logged ErrorsErrors are logged for debugging
No Silent CrashesSystem doesn’t crash silently

Permissions

Respect Frappe permissions explicitly when your tool performs sensitive actions.

def delete_customer_document(doc_name: str): """Delete a customer (respects permissions).""" # Check if user has permission if not frappe.has_permission("Customer", "delete", doc_name): frappe.throw("You don't have permission to delete this customer") # Verify document exists if not frappe.db.exists("Customer", doc_name): frappe.throw(f"Customer {doc_name} not found") # Delete frappe.delete_doc("Customer", doc_name) return {"success": True, "message": f"Customer {doc_name} deleted"}
Explicit ChecksCheck permissions explicitly when needed
Use has_permission()Use frappe.has_permission()
Safe FailureFail safely with clear error messages
Audit LogsLog permission denials for audit

Performance

Optimize your tool to minimize database round-trips and data transfer.

# BAD: Multiple queries in loop def get_customer_orders_slow(customer_id): orders = frappe.get_all("Sales Order", {"customer": customer_id}, ["name"]) results = [] for order in orders: doc = frappe.get_doc("Sales Order", order.name) # Separate query each time results.append(doc.as_dict()) return results # GOOD: Single query with all data def get_customer_orders_fast(customer_id): orders = frappe.get_all( "Sales Order", filters={"customer": customer_id}, fields=["name", "grand_total", "status", "delivery_date"] ) return {"customer_id": customer_id, "orders": orders}
Batch QueriesFetch all needed data in one query when possible
Use get_all()Use frappe.get_all() instead of multiple get_doc()
Cache ResultsCache expensive calculations
Minimal PayloadsReturn only necessary data
Index FieldsConsider database indexes for frequently queried fields

Debugging Custom Tools

Use these techniques to verify and troubleshoot your custom tools.

Test in Frappe Console

Test your function directly in the Frappe console before wiring it to an agent.

bench --site yoursite console

Once inside, import and run your function:

>>> from my_app.api.tools import calculate_customer_ltv >>> result = calculate_customer_ltv("CUST-001") >>> print(result)

Check Agent Run Logs

Review the Agent Run logs after an agent uses your tool.

Common Issues

These issues and fixes cover the most common custom tool problems.

IssueCause / Fix
”Function not found”Verify function path is correct.
Check function is importable.
Restart bench after adding new functions.
”Missing parameter”Agent didn’t provide required parameter.
Improve tool description to clarify what’s needed.
Check agent instructions mention parameter.
”TypeError” or “JSON serialization error”Return value isn’t JSON-serializable.
Convert objects to dicts.
Convert dates to strings.
Don’t return Frappe DocType objects directly.
Function never calledTool description may be unclear.
Agent doesn’t recognize when to use it.
Try improving description or agent instructions.

Documentation Tips

Clear documentation helps agents choose and use your tools correctly.

Writing Great Descriptions

Compare a vague description with one that gives the agent clear context.

Bad
Description: Handles customer calculations

→ Too vague, agent won’t know when to use it

Good
Description: Calculate the total lifetime value for a customer including all paid invoices, order count, and average order value. Use this when users ask about customer value, spending history, or order statistics.

→ Clear purpose, context, and use cases

Include Examples in Docstrings

Add concrete usage examples to your docstrings so agents understand expected inputs and outputs.

def convert_currency(amount: float, from_currency: str, to_currency: str): """ Convert an amount from one currency to another using current rates. Args: amount (float): Amount to convert from_currency (str): Source currency code (USD, EUR, GBP, etc.) to_currency (str): Target currency code Returns: dict: Converted amount and exchange rate used Example: >>> convert_currency(100, "USD", "EUR") {"amount": 100, "from": "USD", "to": "EUR", "converted": 85.50, "rate": 0.855} """ # Implementation pass

The agent can “read” this docstring to understand how to use the tool.

Security Considerations

Keep these security principles in mind when building custom tools.

Dangerous Operations

Be especially careful when tools can access sensitive resources or execute side effects.

Be careful with operations that can damage data or leak information.

File SystemFile system access
Shell CommandsShell commands
Database WritesDatabase modifications
Sensitive APIsExternal API calls with sensitive data
NotificationsSending emails/notifications
Validate InputsValidate all inputs
Check PermissionsCheck permissions explicitly
Log OperationsLog all operations
Rate LimitRate-limit expensive operations
Whitelist ValuesUse whitelisted values when possible

Input Validation

Validate all inputs before executing sensitive operations.

def send_email_notification(recipient: str, subject: str, message: str): """Send email (with validation).""" # Validate email format if not frappe.utils.validate_email_address(recipient): frappe.throw(f"Invalid email address: {recipient}") # Whitelist allowed recipients (optional but safer) allowed_domains = ["yourcompany.com", "partner.com"] domain = recipient.split("@")[1] if domain not in allowed_domains: frappe.throw(f"Can only send to {', '.join(allowed_domains)} addresses") # Send email frappe.sendmail(recipients=[recipient], subject=subject, message=message) return {"success": True, "recipient": recipient}

What’s Next?

Explore related topics to continue building with custom tools.


Questions? Visit GitHub discussions .

Last updated on