Skip to main content
Tutorial 11 min read

Build OpenClaw Plugins with Ollama – Custom Tools

Build production-ready OpenClaw plugins with custom tools & Ollama. Step-by-step guide with code examples.

Originally published:

YouTube by Fahd Mirza

What You'll Learn

This tutorial walks you through building a production-ready OpenClaw plugin that extends Ollama with custom tools. You'll create a plugin from scratch, register a local agent tool, integrate it with a running Ollama instance, and deploy it in a live environment. By the end, you'll have a reusable plugin architecture you can adapt for any custom AI tool.

Prerequisites

  • System Requirements: Linux, macOS, or Windows with WSL2; 4GB RAM minimum, 2GB free disk space
  • Software: Python 3.8+, pip or conda, Git, Docker (optional but recommended), Ollama 0.1.0+
  • Knowledge: Basic Python programming, REST API concepts, JSON format, command-line familiarity
  • Setup Verification: Run ollama --version and python --version to confirm installations. Ollama should be running: ollama serve in a separate terminal

How Does the OpenClaw Plugin System Work?

OpenClaw plugins are Python modules that extend Ollama's agent capabilities by registering custom tools. A tool is a discrete function the agent can call to perform actions outside its core language model—like fetching weather data, executing queries, or transforming content. The plugin system follows a registration pattern: your plugin defines a tool schema (input/output types), implements the tool logic, and registers it with the Ollama runtime.

Plugins communicate with Ollama via its REST API. When an agent decides to use your tool, Ollama sends a request to your plugin endpoint, your code executes, and you return the result. This decoupling allows multiple plugins to coexist without competing for resources.

Step 1: Set Up Your Development Environment

Create a project directory and virtual environment

Start by creating an isolated Python environment to avoid dependency conflicts:

mkdir openclaw-plugin-demo
cd openclaw-plugin-demo
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Install required dependencies

Install the core packages needed for plugin development:

pip install ollama pydantic requests flask python-dotenv

Why these packages? ollama provides the client SDK, pydantic handles data validation and schema generation, flask serves your plugin's HTTP endpoint, requests handles outbound API calls, and python-dotenv manages configuration securely.

Verify Ollama is running

In a separate terminal, start the Ollama server:

ollama serve

Confirm it's accessible by checking: curl http://localhost:11434/api/tags. You should see a JSON response listing available models. If Ollama isn't installed, download it from ollama.ai.

Step 2: Define Your Plugin Architecture

What does a plugin structure look like?

Create the following directory structure to organize your plugin:

openclaw-plugin-demo/
├── plugin/
│   ├── __init__.py
│   ├── tool.py           # Tool schema and logic
│   ├── registry.py       # Registration logic
│   └── config.py         # Configuration
├── app.py                # Flask server
├── requirements.txt
└── .env                  # API keys and config

Create the configuration file

Start with plugin/config.py to centralize settings:

import os
from dotenv import load_dotenv

load_dotenv()

OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434')
PLUGIN_NAME = 'custom-weather-tool'
PLUGIN_VERSION = '1.0.0'
PLUGIN_PORT = int(os.getenv('PLUGIN_PORT', 5000))
DEFAULT_MODEL = os.getenv('DEFAULT_MODEL', 'mistral')

Create a .env file:

OLLAMA_BASE_URL=http://localhost:11434
PLUGIN_PORT=5000
DEFAULT_MODEL=mistral
WEATHER_API_KEY=your_api_key_here

Step 3: Define the Tool Schema

What makes a valid tool schema?

Tools require a Pydantic schema that defines input parameters and output type. This schema is both validation logic and documentation. Create plugin/tool.py:

from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum

class TemperatureUnit(str, Enum):
celsius = "celsius"
fahrenheit = "fahrenheit"

class WeatherInput(BaseModel):
"""Input schema for weather tool"""
location: str = Field(..., description="City name or coordinates")
unit: TemperatureUnit = Field(default=TemperatureUnit.celsius, description="Temperature unit")
include_forecast: bool = Field(default=False, description="Include 7-day forecast")

class WeatherOutput(BaseModel):
"""Output schema for weather tool"""
location: str
temperature: float
condition: str
humidity: int
wind_speed: float
forecast: Optional[list] = None
timestamp: str

The schema enforces type safety and auto-generates documentation that the agent reads. Pydantic also validates inputs before they reach your handler function.

Step 4: Implement the Tool Handler

How do you execute tool logic?

Add the tool implementation to plugin/tool.py. This example uses a simulated weather API (replace with real API calls in production):

from datetime import datetime
import json

class WeatherTool:
def init(self, api_key: str):
self.api_key = api_key
self.name = "get_weather"
self.description = "Fetch current weather and forecast for a location"
self.input_schema = WeatherInput
self.output_schema = WeatherOutput

async def execute(self, input_data: WeatherInput) -> WeatherOutput:
    """Execute the weather tool"""
    # Simulated API call - replace with real weather API
    result = 
    return WeatherOutput(**result)

Key principle: The execute method is async-ready, accepts validated input, and returns a Pydantic model instance. This makes it composable with the agent framework.

Step 5: Create the Plugin Registry

How does the plugin register with Ollama?

The registry is the glue between your tool and the Ollama runtime. Create plugin/registry.py:

import json
from typing import Dict, Any
from .tool import WeatherTool, WeatherInput, WeatherOutput
from .config import PLUGIN_NAME, PLUGIN_VERSION

class PluginRegistry:
def init(self):
self.tools: Dict[str, WeatherTool] = {}
self.metadata = {
"name": PLUGIN_NAME,
"version": PLUGIN_VERSION,
"description": "Custom weather plugin for OpenClaw",
"author": "Your Name"
}

def register_tool(self, api_key: str):
    """Register the weather tool"""
    tool = WeatherTool(api_key)
    self.tools[tool.name] = tool
    return tool

def get_tool_schema(self, tool_name: str) -> Dict[str, Any]:
    """Return OpenAPI-compatible schema for a tool"""
    if tool_name not in self.tools:
        raise ValueError(f"Tool {tool_name} not found")
    
    tool = self.tools[tool_name]
    return 

def list_tools(self) -> list:
    """Return list of registered tools"""
    return list(self.tools.keys())

async def execute_tool(self, tool_name: str, input_data: dict):
    """Execute a registered tool with validation"""
    if tool_name not in self.tools:
        raise ValueError(f"Tool {tool_name} not found")
    
    tool = self.tools[tool_name]
    # Validate input against schema
    validated_input = tool.input_schema(**input_data)
    result = await tool.execute(validated_input)
    return result.model_dump()

Step 6: Build the Flask Server

How do you expose your plugin to Ollama?

Create app.py to define HTTP endpoints that Ollama will call:

from flask import Flask, request, jsonify
import asyncio
import os
from plugin.registry import PluginRegistry
from plugin.config import PLUGIN_PORT

app = Flask(name)
registry = PluginRegistry()

Register tools on startup

api_key = os.getenv('WEATHER_API_KEY', 'demo_key')
registry.register_tool(api_key)

@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({"status": "healthy", "plugin": registry.metadata}), 200

@app.route('/tools', methods=['GET'])
def list_tools():
"""List all registered tools"""
return jsonify({
"tools": registry.list_tools(),
"metadata": registry.metadata
}), 200

@app.route('/tools//schema', methods=['GET'])
def get_tool_schema(tool_name):
"""Get schema for a specific tool"""
try:
schema = registry.get_tool_schema(tool_name)
return jsonify(schema), 200
except ValueError as e:
return jsonify({"error": str(e)}), 404

@app.route('/tools//execute', methods=['POST'])
def execute_tool(tool_name):
"""Execute a tool with provided input"""
try:
input_data = request.get_json()
# Run async function in event loop
result = asyncio.run(registry.execute_tool(tool_name, input_data))
return jsonify({"result": result, "success": True}), 200
except ValueError as e:
return jsonify({"error": str(e), "success": False}), 400
except Exception as e:
return jsonify({"error": f"Execution failed: {str(e)}", "success": False}), 500

@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Endpoint not found"}), 404

if name == 'main':
print(f"Starting OpenClaw plugin on port {PLUGIN_PORT}")
print(f"Available tools: {registry.list_tools()}")
app.run(host='0.0.0.0', port=PLUGIN_PORT, debug=True)

Step 7: Register the Plugin with Ollama

How does Ollama discover your plugin?

Start your plugin server in one terminal:

python app.py

You should see output like: Starting OpenClaw plugin on port 5000. Verify it's working:

curl http://localhost:5000/health

Now register the plugin with Ollama. In another terminal, use the Ollama CLI or API to register:

ollama plugin register http://localhost:5000 --name weather-plugin

Verify registration:

ollama plugin list

If the CLI command isn't available, use the REST API directly:

curl -X POST http://localhost:11434/api/plugins/register \
  -H "Content-Type: application/json" \
  -d '{"url": "http://localhost:5000", "name": "weather-plugin"}'

Step 8: Test the Plugin with a Live Agent

How do you invoke your tool from an agent?

Create test_agent.py to interact with the agent and trigger tool use:

from ollama import Client
import json

client = Client(base_url='http://localhost:11434')

Prompt that should trigger tool use

prompt = "What's the weather in London? Give me the temperature in Celsius."

response = client.generate(
model='mistral',
prompt=prompt,
tools=['weather-plugin'],
stream=False
)

print("Agent Response:")
print(json.dumps(response, indent=2))

Check if tool was called

if 'tool_calls' in response:
print("\nTool Calls Made:")
for call in response['tool_calls']:
print(f" - {call['name']}: {call['input']}")

Run the test:

python test_agent.py

Watch your Flask server logs—you should see incoming POST requests to /tools/get_weather/execute with the extracted location parameter.

Step 9: Add Error Handling and Logging

How do you handle failures gracefully?

Update app.py to add robust error handling and logging:

import logging
from flask import Flask, request, jsonify
from datetime import datetime

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('plugin.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(name)

@app.route('/tools//execute', methods=['POST'])
def execute_tool(tool_name):
"""Execute a tool with error handling"""
try:
logger.info(f"Executing tool: {tool_name}")
input_data = request.get_json()

    if not input_data:
        logger.warning(f"Empty input for tool {tool_name}")
        return jsonify({"error": "Input data required", "success": False}), 400
    
    logger.debug(f"Input data: {input_data}")
    result = asyncio.run(registry.execute_tool(tool_name, input_data))
    
    logger.info(f"Tool {tool_name} executed successfully")
    return jsonify({"result": result, "success": True, "timestamp": datetime.utcnow().isoformat()}), 200
    
except ValueError as e:
    logger.error(f"Validation error for {tool_name}: {str(e)}")
    return jsonify({"error": str(e), "success": False}), 400
except Exception as e:
    logger.exception(f"Unexpected error in {tool_name}: {str(e)}")
    return jsonify({"error": f"Execution failed: {str(e)}", "success": False}), 500

Troubleshooting Common Issues

Plugin not discovered by Ollama

Problem: ollama plugin list shows empty or registration fails.

Solutions: (1) Verify your Flask server is running and accessible: curl http://localhost:5000/health. (2) Check that Ollama can reach localhost:5000—if running in Docker, use the container's IP or Docker network. (3) Ensure the plugin URL is absolute and reachable from Ollama's perspective. (4) Check Ollama's logs for detailed error messages.

Tool schema validation errors

Problem: Requests fail with "Validation error" even though input looks correct.

Solution: Pydantic is strict about types. If your schema expects an integer but receives a string, it will reject it. Verify the JSON sent matches the schema exactly. Print the incoming data: logger.debug(request.get_json()) and compare to WeatherInput.model_json_schema().

Async execution timeout

Problem: Requests hang or timeout after 30 seconds.

Solution: Long-running external API calls block the event loop. Use timeout parameters: requests.get(url, timeout=10). Alternatively, move blocking I/O to a thread pool with asyncio.to_thread().

Plugin works locally but fails in production

Problem: Different behavior when deployed vs. local testing.

Solutions: (1) Environment variables differ—verify .env is loaded in production. (2) Network access: ensure the production Ollama instance can reach your plugin endpoint. (3) Port conflicts: production may use different ports. (4) Run with explicit logging to capture production errors.

Best Practices for Production Plugins

Security

Always validate inputs strictly. Use environment variables for API keys, never hardcode them. Add authentication if your plugin is exposed beyond localhost: require API keys in the Authorization header.

from functools import wraps

def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key != os.getenv('PLUGIN_API_KEY'):
return jsonify({"error": "Unauthorized"}), 401
return f(*args, **kwargs)
return decorated

@app.route('/tools//execute', methods=['POST'])
@require_api_key
def execute_tool(tool_name):
# ... your code

Performance

Use async I/O for external calls. Cache tool schemas since they don't change frequently. Consider rate limiting to prevent abuse.

Monitoring

Log all tool executions with timestamps and results. Use prometheus or similar to track execution time, success rates, and errors. Monitor disk space if your plugin creates files.

Versioning

Include semantic versioning in your plugin metadata. Maintain backward compatibility—if you change a tool's input schema, support both old and new versions or fail gracefully with clear error messages.

Next Steps

  • Integrate real APIs: Replace the simulated weather API with calls to OpenWeatherMap, WeatherAPI, or another service.
  • Add multiple tools: Extend your plugin to register 5+ tools (e.g., weather, news, calculations, data fetching).
  • Deploy to Docker: Package your plugin in a Docker container for easy deployment across environments.
  • Explore tool chaining: Build tools that call other plugins, creating composite workflows.
  • Review OpenClaw documentation: Learn advanced features like streaming responses, background tasks, and tool dependencies.
  • ollama-integrations – Discover community plugins and integrations.

Summary

You've built a complete OpenClaw plugin from scratch: defined tool schemas with Pydantic, implemented async handlers, registered with Ollama's runtime, and tested live tool execution. The architecture you've created is modular—the registry pattern, async handlers, and schema-driven approach scale to dozens of tools without refactoring.

The key takeaway is that OpenClaw plugins are simple HTTP services that follow a contract: expose tool schemas, validate inputs strictly, and return structured results. Start with a single tool, validate it works end-to-end, then add complexity. Use logging extensively in production—it's your primary debugging tool when issues arise in live environments.

Key Takeaways

  • OpenClaw plugins are HTTP services that register tools with Ollama using a schema-driven architecture—Pydantic models define input/output contracts that both validate data and auto-generate documentation.
  • The three-layer pattern (schema definition → handler implementation → registry) separates concerns and makes plugins composable; this structure supports scaling from 1 to 100+ tools.
  • Test plugins locally before production deployment; use logging extensively and verify your Flask server is reachable from Ollama's perspective (critical in Docker/Kubernetes environments).
  • Security requires strict input validation, environment-based configuration, and optional API key authentication; never expose secrets in code or logs.
  • Async execution with proper timeout handling prevents blocking; external API calls should always have explicit timeout parameters to fail fast under latency.
Share:

Original Source

https://www.youtube.com/watch?v=48Sy_yV6aMo

View Original

Last updated: