Tool Ecosystem: Built-in Tools and Custom Tool Development
Chapter 14: Tool Ecosystem — Built-in Tools Deep Dive and Custom Tool Development
Master Dify's tool ecosystem completely — from configuring every built-in tool to building, testing, and productionizing custom tools that extend your Agent's capabilities.
Chapter Overview
An Agent's capability boundary is defined by its tools. Without tools, an Agent is merely a sophisticated chatbot; with the right tools, it can genuinely solve business problems. This chapter covers two dimensions: first, a systematic exploration of Dify's built-in tools (web search, code execution, knowledge base retrieval, etc.) with configuration and tuning guidance; second, a complete walkthrough of developing, testing, and publishing custom tools — including API wrapping, authentication handling, and error design.
After reading this chapter, you will be able to:
- Confidently use and configure all of Dify's built-in tools
- Build custom tools from scratch that conform to the Dify tool specification
- Design highly reliable tool interfaces (auth, retry, timeout)
- Publish tools to your team or the community tool marketplace
Level 1: Foundational Knowledge (1–3 Years Experience)
What Is a Dify Tool?
In Dify, a "tool" is an external capability unit that an Agent can invoke. Each tool has:
- Name: The unique identifier the Agent uses to reference the tool
- Description: Tells the LLM what this tool does (determines whether the LLM chooses it)
- Parameters: Typed, described inputs the tool accepts
- Implementation: Calls an external API or local function
The quality of a tool's description is critical. If it's unclear, the LLM may not know when to use the tool — or may misuse it.
Dify Built-in Tool Categories
Search tools:
web_search(Google/Bing): Search the internet for real-time informationduckduckgo_search: Privacy-friendly web search alternativesearxng: Self-hostable meta-search engine integration
Code execution tools:
code_interpreter: Execute Python in a sandbox; supports data analysis and chart generationjavascript_executor: Execute JavaScript snippets
Knowledge tools:
dataset_retrieval: Retrieve content from Dify knowledge baseswikipedia_search: Fetch structured knowledge from Wikipedia
Utility tools:
calculator: Precise math calculations (avoids LLM arithmetic errors)current_time: Get the current date and timeweather_query: Weather lookup (requires API key)
Image generation:
dalle3: Generate images with DALL-E 3stable_diffusion: Call SD for image generation
Enabling and Configuring Built-in Tools in Dify
- Go to Dify console → Tools → Built-in Tools
- Find the target tool and click "Configure"
- Enter the required API key (e.g., SerpAPI key for web search)
- Enable the tool in your Agent application
Web Search tool example:
tool: web_search
provider: serpapi
config:
api_key: "your_serpapi_key"
gl: "us" # Country: United States
hl: "en" # Language: English
num: 5 # Return 5 results per search
Your First Custom Tool: Hello World
Custom tools in Dify are added via "Custom API" — essentially wrapping an external REST API as a tool:
# Simple custom tool: exchange rate lookup
openapi: 3.1.0
info:
title: Exchange Rate Tool
version: 1.0.0
servers:
- url: https://api.exchangerate.host
paths:
/latest:
get:
operationId: get_exchange_rate
summary: Get latest exchange rate
description: Query the latest exchange rate for a currency pair. Use this for currency conversion questions.
parameters:
- name: base
in: query
required: true
description: Base currency code, e.g. USD, CNY, EUR
schema:
type: string
- name: symbols
in: query
required: false
description: Target currency codes, comma-separated, e.g. CNY,EUR,JPY
schema:
type: string
Paste this OpenAPI spec into Dify's "Custom API Tool" configuration box, and Dify will automatically parse it and generate the tool description.
Level 2: Mechanism Deep Dive (3–5 Years Experience)
Built-in Tool Deep Dive: code_interpreter
The code interpreter is the most powerful and complex built-in tool in Dify. It executes code in an isolated Python sandbox environment, supporting:
- Mathematical calculations (more accurate than LLM arithmetic)
- Data analysis (pandas, numpy)
- Chart generation (matplotlib)
- File processing (reading uploaded CSV, Excel files)
Code interpreter example:
# Code generated by the Agent for execution in sandbox
code = """
import pandas as pd
import matplotlib.pyplot as plt
import io, base64
# Analyze sales data
data = {
'Month': ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
'Revenue': [120000, 135000, 98000, 156000, 178000]
}
df = pd.DataFrame(data)
# Calculate statistics
print(f"Total Revenue: ${df['Revenue'].sum():,}")
print(f"Monthly Average: ${df['Revenue'].mean():,.0f}")
print(f"Best Month: {df.loc[df['Revenue'].idxmax(), 'Month']}")
print(f"Growth Trend: {((df['Revenue'].iloc[-1] / df['Revenue'].iloc[0]) - 1) * 100:.1f}%")
# Generate chart
fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(df['Month'], df['Revenue'] / 1000, color='steelblue', alpha=0.8)
ax.set_title('Monthly Revenue Trend', fontsize=14)
ax.set_ylabel('Revenue (thousands $)')
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100)
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode()
print(f"[Chart]\ndata:image/png;base64,{img_b64}")
"""
Security restrictions in the sandbox:
import subprocess # ❌ Blocked: system command execution
import os # ❌ Blocked: filesystem access (partial)
import socket # ❌ Blocked: network access
import requests # ❌ Blocked: HTTP requests (in most configs)
import pandas as pd # ✅ Allowed
import numpy as np # ✅ Allowed
import matplotlib # ✅ Allowed
import scipy # ✅ Allowed
import sklearn # ✅ Allowed
Built-in Tool Deep Dive: dataset_retrieval
This tool extends Dify's RAG capability into Agent scenarios. Unlike standard Q&A apps, an Agent can actively decide when to query the knowledge base and what keywords to use:
# Agent reasoning with knowledge base retrieval
"""
User: What is our company's vacation policy?
Thought: The user is asking about internal HR policy. I should retrieve from the knowledge base.
Action: dataset_retrieval
Action Input: {"dataset_ids": ["hr-policy-kb-001"], "query": "vacation policy days request process"}
Observation: [Retrieved result]
Score 0.92: Employee Vacation Policy (2024)
Employees with 1-10 years of service receive 5 days of annual leave...
Thought: Found the relevant policy. I can answer the user now.
Final Answer: According to company HR policy, employees with 1–10 years of service receive...
"""
Advanced knowledge base retrieval configuration:
tool: dataset_retrieval
config:
top_k: 5 # Return top 5 most relevant chunks
score_threshold: 0.6 # Similarity threshold — don't return below this
search_method: hybrid # hybrid = semantic + keyword
reranking: true # Enable reranking for better precision
Custom Tool Development: Complete Walkthrough
Step 1: Design the Tool Interface
Good tool interface design follows these principles:
- Single responsibility: One tool does one thing
- Precise description: Tell the LLM exactly when and when NOT to use this tool
- Minimal parameters: Only expose necessary parameters; give defaults to optional ones
- Readable errors: Error messages should be understandable to the LLM for self-correction
# Example: Internal CRM query tool definition
tool_definition = {
"name": "query_customer_info",
"description": (
"Query customer information from the CRM system. "
"Use this when the user asks about a specific customer's basic info, "
"contact details, purchase history, or contract status. "
"NOT suitable for: product queries, inventory data, financial reports "
"(use other tools for those). "
"Query by customer ID (format: CUST-XXXXXX) or company name."
),
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Unique customer ID, format: CUST- followed by 6 digits, e.g. CUST-001234"
},
"customer_name": {
"type": "string",
"description": "Company name (alternative to customer_id)"
},
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Fields to return. Options: contact, contract, purchase_history, credit_score",
"default": ["contact", "contract"]
}
}
}
}
Step 2: Implement the Tool Backend (Python)
# tools/crm_tool.py
import httpx
from typing import Optional, List
from pydantic import BaseModel, validator
class CRMQueryInput(BaseModel):
customer_id: Optional[str] = None
customer_name: Optional[str] = None
fields: List[str] = ["contact", "contract"]
@validator("customer_id")
def validate_id(cls, v):
if v and not v.startswith("CUST-"):
raise ValueError(f"customer_id must start with CUST-, got: {v}")
return v
def model_post_init(self, *args):
if not self.customer_id and not self.customer_name:
raise ValueError("Must provide either customer_id or customer_name")
ALLOWED_FIELDS = {"contact", "contract", "purchase_history", "credit_score"}
class CRMTool:
def __init__(self, crm_base_url: str, api_key: str):
self.client = httpx.AsyncClient(
base_url=crm_base_url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=10.0
)
async def query(self, raw_params: dict) -> str:
try:
params = CRMQueryInput(**raw_params)
except ValueError as e:
return f"Parameter error: {e}\nPlease check the parameter format and retry."
valid_fields = [f for f in params.fields if f in ALLOWED_FIELDS]
try:
if params.customer_id:
resp = await self.client.get(
f"/api/customers/{params.customer_id}",
params={"fields": ",".join(valid_fields)}
)
else:
resp = await self.client.get(
"/api/customers/search",
params={"name": params.customer_name,
"fields": ",".join(valid_fields)}
)
if resp.status_code == 404:
return "Customer not found. Please verify the customer ID or name."
if resp.status_code == 403:
return "Insufficient permissions to query this customer. Contact your administrator."
resp.raise_for_status()
return self._format(resp.json())
except httpx.TimeoutException:
return "CRM system timed out (>10s). Please try again later."
except httpx.HTTPStatusError as e:
return f"CRM query failed: HTTP {e.response.status_code}"
def _format(self, data: dict) -> str:
lines = [f"Customer: {data.get('name', 'Unknown')} (ID: {data.get('id')})"]
if "contact" in data:
c = data["contact"]
lines.append(f"Contact: {c.get('name')} / {c.get('phone')} / {c.get('email')}")
if "contract" in data:
ct = data["contract"]
lines.append(f"Contract: {ct.get('status')} / Expires: {ct.get('expire_date')}")
lines.append(f"Contract Value: ${ct.get('amount'):,}")
if "credit_score" in data:
lines.append(f"Credit Score: {data['credit_score']}/100")
return "\n".join(lines)
Step 3: Write the OpenAPI Specification
openapi: 3.1.0
info:
title: CRM Customer Query Tool
description: Query customer information from the enterprise CRM system
version: 1.2.0
servers:
- url: https://crm-api.company.internal
security:
- BearerAuth: []
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
paths:
/api/customers/{customer_id}:
get:
operationId: query_customer_by_id
summary: Query customer by ID
description: |
Exact lookup of customer info by unique ID.
Use for: precise queries when you know the customer ID.
parameters:
- name: customer_id
in: path
required: true
schema:
type: string
pattern: '^CUST-\d{6}$'
- name: fields
in: query
schema:
type: string
responses:
'200':
description: Success
'404':
description: Customer not found
'403':
description: Insufficient permissions
/api/customers/search:
get:
operationId: search_customer_by_name
summary: Search customer by name
parameters:
- name: name
in: query
required: true
schema:
type: string
Step 4: Register the Tool in Dify
- Dify Console → Tools → Custom Tools → Create Tool
- Fill in basic info (name, description)
- Select authentication method (Bearer Token, API Key, OAuth2, etc.)
- Paste the OpenAPI specification
- Click "Test" to validate the tool works
- Save and enable in your Agent application
# Register tool programmatically via Dify API (for CI/CD)
import httpx
def register_tool(spec: dict, dify_url: str, admin_token: str) -> dict:
resp = httpx.post(
f"{dify_url}/console/api/workspaces/tools",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": spec["name"],
"description": spec["description"],
"openapi": spec["openapi_yaml"],
"auth_type": "bearer",
"auth_config": {"token": spec["api_key"]}
}
)
resp.raise_for_status()
return resp.json()
Level 3: Source Code and Principles (5+ Years Experience)
Dify Tool System Internal Architecture
api/core/tools/
├── tool.py # Tool abstract base class
├── tool_engine.py # Tool execution engine
├── tool_manager.py # Tool registry, discovery, permissions
├── builtin/ # Built-in tool implementations
│ ├── web_search/
│ │ ├── tools/
│ │ │ └── google_search.py
│ │ └── web_search.yaml
│ ├── code_interpreter/
│ └── ...
├── api_tools/ # Custom API tools
│ ├── api_tool.py
│ └── api_tool_bundle.py
└── provider/ # Tool provider management
Tool base class core design:
from abc import ABC, abstractmethod
from typing import Any, Generator, Union
class ToolInvokeMessage:
def __init__(self, type: str, message: Any, meta: dict = None):
self.type = type # "text" | "image" | "link" | "blob"
self.message = message
self.meta = meta or {}
class Tool(ABC):
@property
@abstractmethod
def identity(self) -> "ToolIdentity":
raise NotImplementedError
@property
@abstractmethod
def parameters(self) -> list:
raise NotImplementedError
@abstractmethod
def _invoke(self, user_id: str, tool_parameters: dict) -> Union[
ToolInvokeMessage, list, Generator
]:
raise NotImplementedError
def invoke(self, user_id: str, tool_parameters: dict) -> list:
"""Public entry point with validation and error handling"""
validated = self._validate_parameters(tool_parameters)
result = self._invoke(user_id, validated)
if isinstance(result, Generator):
return list(result)
return result if isinstance(result, list) else [result]
def _validate_parameters(self, params: dict) -> dict:
validated = {}
for p in self.parameters:
value = params.get(p.name)
if p.required and value is None:
raise ValueError(f"Missing required parameter: {p.name}")
if value is not None:
validated[p.name] = p.validate_value(value)
elif p.default is not None:
validated[p.name] = p.default
return validated
Built-in tool implementation (Google Search):
# api/core/tools/builtin/web_search/tools/google_search.py
from serpapi import GoogleSearch as SerpApiSearch
from core.tools.tool.builtin_tool import BuiltinTool
class GoogleSearchTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict):
query = tool_parameters.get("query", "")
num = tool_parameters.get("num_results", 5)
gl = tool_parameters.get("gl", "us")
hl = tool_parameters.get("hl", "en")
try:
results = SerpApiSearch({
"q": query,
"num": num,
"gl": gl,
"hl": hl,
"api_key": self.runtime.credentials.get("serpapi_api_key"),
}).get_dict()
except Exception as e:
return self.create_text_message(f"Search failed: {e}")
organic = results.get("organic_results", [])
if not organic:
return self.create_text_message("No results found")
lines = []
for i, r in enumerate(organic[:num], 1):
lines.append(f"{i}. **{r.get('title')}**\n{r.get('snippet')}\n{r.get('link')}")
return self.create_text_message("\n\n".join(lines))
Tool Execution Engine: Concurrency Control
# api/core/tools/tool_engine.py — parallel execution logic
import asyncio
class ToolEngine:
TOOL_TIMEOUT = 30 # seconds per tool
async def invoke_tools_parallel(
self, tool_calls: list, user_id: str
) -> list:
"""Execute multiple tool calls in parallel with timeout and error isolation"""
async def invoke_one(tc) -> dict:
try:
tool = self.get_tool(tc.tool_name)
result = await asyncio.wait_for(
asyncio.get_event_loop().run_in_executor(
None, tool.invoke, user_id, tc.parameters
),
timeout=self.TOOL_TIMEOUT
)
return {"id": tc.id, "output": result, "success": True}
except asyncio.TimeoutError:
return {"id": tc.id, "output": "Tool timed out", "success": False}
except Exception as e:
return {"id": tc.id, "output": f"Tool error: {e}", "success": False}
return await asyncio.gather(*[invoke_one(tc) for tc in tool_calls])
Custom Tool Authentication Internals
class APIToolAuthManager:
def inject_auth(self, request: "httpx.Request", auth_config: dict):
auth_type = auth_config.get("type")
if auth_type == "api_key":
location = auth_config.get("in", "header")
name = auth_config.get("name", "X-API-Key")
value = auth_config.get("value")
if location == "header":
request.headers[name] = value
else: # query param
request = request.copy_with(
url=request.url.copy_with(params={**dict(request.url.params), name: value})
)
elif auth_type == "bearer":
request.headers["Authorization"] = f"Bearer {auth_config['token']}"
elif auth_type == "oauth2":
token = self._get_oauth2_token(auth_config)
request.headers["Authorization"] = f"Bearer {token}"
elif auth_type == "basic":
import base64
creds = base64.b64encode(
f"{auth_config['username']}:{auth_config['password']}".encode()
).decode()
request.headers["Authorization"] = f"Basic {creds}"
return request
def _get_oauth2_token(self, cfg: dict) -> str:
cache_key = f"oauth2:{cfg['client_id']}"
cached = self.token_cache.get(cache_key)
if cached:
return cached["access_token"]
resp = httpx.post(cfg["token_url"], data={
"grant_type": "client_credentials",
"client_id": cfg["client_id"],
"client_secret": cfg["client_secret"],
})
resp.raise_for_status()
data = resp.json()
self.token_cache.set(cache_key, data,
ttl=data.get("expires_in", 3600) - 60)
return data["access_token"]
Level 4: Production Pitfalls and Decision-Making (Expert Perspective)
Pitfall 1: Vague Tool Descriptions Causing Misuse
Tool descriptions are the LLM's only basis for deciding which tool to call and when. Vague descriptions lead to:
- LLM calling a tool when not needed (over-invocation)
- LLM failing to call a tool when needed (missed invocation)
- LLM calling the wrong tool entirely
Bad example (too vague):
name: data_query
description: Query data
Good example (includes scope, use cases, and exclusions):
name: crm_customer_query
description: |
Query customer basic info, contact details, and contract status from the CRM system.
Use when: user asks about a specific customer, needs to review contracts,
or wants to check purchase history.
Do NOT use for: product information, inventory data, financial reports.
Query by: customer ID (format CUST-XXXXXX) or company name (exact match only).
Pitfall 2: Tool Output Too Long — Overflowing Context
When a tool returns large volumes of data (20 search results, 100 records), the LLM's context window fills up rapidly, causing performance degradation or outright errors.
Fix: Output truncation and summarization
class OutputTruncator:
MAX_CHARS = 2000
def truncate(self, output: str, strategy: str = "smart") -> str:
if len(output) <= self.MAX_CHARS:
return output
if strategy == "tail":
return output[:self.MAX_CHARS] + f"\n...[Truncated. Original length: {len(output)} chars]"
elif strategy == "smart":
half = self.MAX_CHARS // 2
return (
output[:half]
+ f"\n\n...[{len(output) - self.MAX_CHARS} chars omitted]...\n\n"
+ output[-half:]
)
elif strategy == "summarize":
return self._summarize_with_llm(output)
def _summarize_with_llm(self, text: str) -> str:
prompt = f"Extract the key information from the following (max 500 words):\n\n{text[:5000]}"
return cheap_llm.call(prompt) # Use a small/cheap model for summarization
Pitfall 3: Non-Idempotent Tools Getting Retried
When an Agent encounters an error, it may retry the tool call. If the tool is not idempotent (sending emails, writing to a database), this causes duplicate operations.
import hashlib, json
class IdempotentToolWrapper:
"""Add idempotency protection to non-idempotent tools"""
def __init__(self, tool, store, ttl: int = 3600):
self.tool = tool
self.store = store # Redis or database
self.ttl = ttl
def invoke(self, user_id: str, params: dict) -> str:
key_data = json.dumps(
{"user": user_id, "tool": self.tool.name, "params": params},
sort_keys=True
)
idem_key = f"idem:{hashlib.sha256(key_data.encode()).hexdigest()}"
cached = self.store.get(idem_key)
if cached:
return f"[Deduplicated] {cached}"
result = self.tool.invoke(user_id, params)
self.store.setex(idem_key, self.ttl, str(result))
return str(result)
Tool Performance Benchmark and Optimization
Production environment measurements:
| Tool Type | Avg Latency | P99 Latency | Main Bottleneck | Optimization |
|---|---|---|---|---|
| Web Search (SerpAPI) | 1.2s | 3.5s | External API | Cache hot queries |
| Code Interpreter | 2.8s | 8s | Container boot + execution | Pre-warm container pool |
| Knowledge Base Retrieval | 0.3s | 0.8s | Vector DB query | Pre-load hot KBs |
| Custom API Tool | 0.5–5s | Variable | Target API performance | Set reasonable timeout |
| Calculator | <10ms | <50ms | Local compute | No optimization needed |
Tool result caching:
import functools, hashlib, json
def cacheable_tool(ttl: int = 300):
def decorator(fn):
@functools.wraps(fn)
async def wrapper(self, user_id: str, params: dict) -> str:
key = f"tool:{self.name}:{hashlib.md5(json.dumps(params, sort_keys=True).encode()).hexdigest()}"
cached = await redis.get(key)
if cached:
return cached.decode()
result = await fn(self, user_id, params)
await redis.setex(key, ttl, str(result))
return result
return wrapper
return decorator
class WeatherTool(Tool):
@cacheable_tool(ttl=600) # Cache weather for 10 minutes
async def _invoke(self, user_id: str, params: dict) -> str:
city = params["city"]
resp = await weather_api.get(city)
return f"{city}: {resp['weather']}, {resp['temperature']}°C"
Chapter Summary
This chapter provided a comprehensive view of the Dify tool ecosystem, from built-in tool configuration to the complete custom tool development pipeline:
Key takeaways:
- Tool descriptions are the LLM's sole basis for tool selection — descriptions must include applicable scenarios and explicit exclusions
- The code interpreter is the most powerful tool for data analysis tasks — safely executes Python in a sandbox with pandas/matplotlib
- Four-step custom tool development: Design interface → Implement backend → Write OpenAPI spec → Register in Dify
- Tool authentication supports API Key, Bearer Token, OAuth2, and Basic Auth — all automatically injected by Dify's auth manager
Production essentials:
- Tool output must have a length cap (recommend 2000 chars) to prevent context overflow
- Non-idempotent tools (send email, write database) must use idempotency keys
- Cache results for hot tools (weather/exchange rate: 10min, search: 5min)
- Set per-tool timeouts to 30s max to avoid stalling the entire Agent response
Key numbers:
- Code interpreter average latency: 2.8s, P99: 8s — suited for async tasks, not real-time chat
- Knowledge base retrieval average: 0.3s — the fastest tool in the ecosystem
- SerpAPI: ~$0.001/search, 1000 queries/day ≈ $1 — factor this into cost accounting
Next chapter: Chapter 15 enters the world of multi-Agent collaboration, exploring how Dify Workflow orchestrates multiple Agents to tackle complex tasks together.