ClawMart AI
← Back to Blog
August 28, 20268 min readClaw Mart Team

How to Create Your First OpenClaw Skill from Scratch

How to Create Your First OpenClaw Skill from Scratch

How to Create Your First OpenClaw Skill from Scratch

Most people overthink their first OpenClaw skill. They read the docs, watch a couple of YouTube walkthroughs, browse through Discord threads, and then sit there staring at an empty file wondering where to actually start.

I get it. I was there six months ago. The concept of a "skill" sounds abstract until you've built one, and then it clicks and you wonder why you waited so long.

Here's the thing: an OpenClaw skill is just a self-contained unit of capability you give to an agent. That's it. It's a function (or set of functions) with some metadata that tells the agent what it does, when to use it, and what inputs it needs. Once you understand that, everything else is just configuration.

Let me walk you through building your first one from absolute zero.

What Exactly Is an OpenClaw Skill?

Before we write a single line of code, let's get the mental model right.

An OpenClaw agent is only as useful as its skills. Without skills, it's just an LLM sitting there generating text. Skills are what let it do things — search the web, query a database, send an email, hit an API, transform data, whatever you need.

A skill consists of three parts:

  1. The function itself — the actual Python code that does the work
  2. The input schema — a structured definition of what the function expects
  3. The metadata — description, examples, and constraints that help the agent understand when and how to use it

That's the whole anatomy. If you can write a Python function and describe what it does, you can build a skill.

Setting Up Your Environment

First, let's get OpenClaw installed and a project scaffolded. Open your terminal:

pip install openclaw

Now create a project directory:

mkdir my-first-skill
cd my-first-skill

OpenClaw doesn't force a rigid project structure on you, but I'd recommend something like this from the start:

my-first-skill/
ā”œā”€ā”€ skills/
│   └── weather.py
ā”œā”€ā”€ tests/
│   └── test_weather.py
ā”œā”€ā”€ agent.py
└── requirements.txt

That's it. No boilerplate generators, no massive config files, no YAML hell. Just Python files in folders.

Building a Real Skill: Weather Lookup

Let's build something concrete — a skill that fetches current weather for a given location. It's simple enough to understand but hits all the important concepts.

Step 1: Define the Input Schema

This is where most people either skip ahead (bad idea) or over-engineer (also bad). OpenClaw uses Pydantic for input validation, which means your agent gets clear, typed inputs every time.

# skills/weather.py
from pydantic import BaseModel, Field

class WeatherInput(BaseModel):
    location: str = Field(
        description="City name (e.g., 'San Francisco') or ZIP code (e.g., '94103')"
    )
    units: str = Field(
        default="fahrenheit",
        description="Temperature units: 'fahrenheit' or 'celsius'"
    )

Notice the Field descriptions. These aren't just for your fellow developers — OpenClaw feeds these descriptions to the LLM so it knows exactly what format to provide. The difference between a vague description and a specific one with examples is the difference between an agent that works and one that hallucinates garbage inputs.

This is one of the most common pain points I see in agent development forums. People write tool descriptions that are either three words long ("Gets weather") or a 500-word essay. Neither works well. OpenClaw's structured approach with Pydantic fields forces you into the sweet spot: concise, specific, with examples baked into the field descriptions.

Step 2: Write the Function

Now the actual skill logic:

import requests
from openclaw import tool

WEATHER_API_KEY = "your-api-key-here"  # Use env vars in production

@tool(
    description="Get current weather conditions for a location",
    examples=[
        ("What's the weather in NYC?", {"location": "New York City"}),
        ("Temperature in 94103?", {"location": "94103", "units": "fahrenheit"}),
        ("How cold is it in London in Celsius?", {"location": "London", "units": "celsius"}),
    ]
)
def get_weather(input: WeatherInput) -> dict:
    """Fetch current weather from the weather API."""
    base_url = "https://api.weatherapi.com/v1/current.json"
    
    response = requests.get(base_url, params={
        "key": WEATHER_API_KEY,
        "q": input.location,
        "aqi": "no"
    })
    
    data = response.json()
    
    if input.units == "celsius":
        temp = data["current"]["temp_c"]
        unit_label = "°C"
    else:
        temp = data["current"]["temp_f"]
        unit_label = "°F"
    
    return {
        "location": data["location"]["name"],
        "temperature": f"{temp}{unit_label}",
        "condition": data["current"]["condition"]["text"],
        "humidity": f"{data['current']['humidity']}%",
        "wind": f"{data['current']['wind_mph']} mph"
    }

Let me highlight a few things about the @tool decorator.

The description is short and action-oriented. Not "This function retrieves meteorological data from an external third-party API service and returns structured weather information including temperature, humidity, and wind conditions." Just "Get current weather conditions for a location." The agent doesn't need a novel; it needs clarity.

The examples are gold. Each one shows a natural language query mapped to the expected input parameters. OpenClaw uses these to generate optimized prompts that dramatically improve tool selection accuracy. I cannot stress enough how much of a difference this makes. Three good examples beat a paragraph of description every single time.

Step 3: Add Error Handling

Here's where most tutorials stop, and where most production agents break. What happens when the API is down? When the user provides a nonsense location? When you hit a rate limit?

In most frameworks, an unhandled exception kills the entire agent run. That's absurd for production use. OpenClaw has built-in retry and fallback mechanisms:

from openclaw import tool
from openclaw.retry import RetryStrategy

@tool(
    description="Get current weather conditions for a location",
    examples=[
        ("What's the weather in NYC?", {"location": "New York City"}),
        ("Temperature in 94103?", {"location": "94103"}),
    ],
    retry=RetryStrategy(
        max_attempts=3,
        backoff="exponential",
        retry_on=[TimeoutError, ConnectionError, requests.exceptions.RequestException],
        fallback="The weather service is temporarily unavailable. Please try again in a moment."
    )
)
def get_weather(input: WeatherInput) -> dict:
    response = requests.get(base_url, params={
        "key": WEATHER_API_KEY,
        "q": input.location,
    }, timeout=5)
    
    if response.status_code == 404:
        return {"error": f"Location '{input.location}' not found. Try a different city name or ZIP code."}
    
    response.raise_for_status()
    data = response.json()
    
    return {
        "location": data["location"]["name"],
        "temperature": f"{data['current']['temp_f']}°F",
        "condition": data["current"]["condition"]["text"],
    }

When the API hiccups, OpenClaw retries with exponential backoff. After three failures, instead of crashing, the agent receives that fallback message and adapts — maybe it tells the user to try again, or attempts an alternative approach. The agent stays alive and responsive. That's the kind of resilience that separates a demo from a product.

Step 4: Wire It Into an Agent

Now let's connect the skill to an actual agent:

# agent.py
from openclaw import Agent
from openclaw.guardrails import MaxToolCalls, PreventRepeats
from skills.weather import get_weather

agent = Agent(
    tools=[get_weather],
    guardrails=[
        MaxToolCalls(limit=10),
        PreventRepeats(window=3),
    ],
    trace=True
)

result = agent.run("What's the weather like in Chicago right now?")
print(result.output)

Those guardrails aren't optional fluff — they're what prevent your agent from calling the weather API 47 times in a row because the LLM got stuck in a reasoning loop. If you've spent any time in the LangChain subreddit, you've seen dozens of posts about agents stuck in infinite loops. OpenClaw's MaxToolCalls and PreventRepeats are built-in precisely because this is such a common failure mode.

The trace=True flag is something I leave on during development. It gives you a full execution trace:

ExecutionTrace:
  Step 1: Reasoning
    "The user wants current weather in Chicago. I'll use get_weather."
  Step 2: Tool Call - get_weather(location="Chicago")
    Input: {"location": "Chicago", "units": "fahrenheit"}
    Output: {"location": "Chicago", "temperature": "34°F", "condition": "Cloudy", ...}
    Duration: 0.8s
  Step 3: Reasoning
    "I have the weather data. I'll format a response."
  
  Total steps: 3
  Total duration: 1.2s

No more black-box debugging. You can see exactly what the agent thought, what it called, what it received, and how long each step took. When something goes wrong (and it will), this is how you figure out why in thirty seconds instead of thirty minutes.

Testing Your Skill

Here's something that separates serious OpenClaw developers from weekend tinkerers: actually testing your skills before deploying them.

The problem with testing agents is non-determinism. The LLM might phrase things differently each run. OpenClaw gives you tools to test behavior rather than exact outputs:

# tests/test_weather.py
from openclaw import Agent
from openclaw.testing import MockTool, AgentTest
from skills.weather import get_weather

def test_weather_skill_is_called():
    mock_response = {
        "location": "Chicago",
        "temperature": "34°F",
        "condition": "Cloudy",
        "humidity": "65%",
        "wind": "12 mph"
    }
    
    with MockTool("get_weather", return_value=mock_response):
        agent = Agent(tools=[get_weather])
        test = AgentTest(agent)
        
        result = test.run("What's the weather in Chicago?")
        
        test.assert_tool_called("get_weather")
        test.assert_output_contains("34")
        test.assert_output_contains("Chicago")
        test.assert_steps_count(max=5)

def test_weather_handles_bad_location():
    mock_error = {"error": "Location 'asdfghjkl' not found."}
    
    with MockTool("get_weather", return_value=mock_error):
        agent = Agent(tools=[get_weather])
        test = AgentTest(agent)
        
        result = test.run("Weather in asdfghjkl")
        
        test.assert_tool_called("get_weather")
        test.assert_output_contains("not found")

No real API calls. No LLM costs in CI/CD. You're testing that the agent selects the right tool, passes reasonable inputs, and handles edge cases. Run these with pytest like any other test suite.

Adding a Second Skill (and Seeing the Magic)

One skill is useful. Two skills is where agents start to feel powerful. Let's add a quick unit conversion skill:

# skills/convert.py
from openclaw import tool
from pydantic import BaseModel, Field

class ConvertInput(BaseModel):
    value: float = Field(description="The numeric value to convert")
    from_unit: str = Field(description="Source unit (e.g., 'fahrenheit', 'miles', 'kg')")
    to_unit: str = Field(description="Target unit (e.g., 'celsius', 'kilometers', 'lbs')")

@tool(
    description="Convert a value between units of measurement",
    examples=[
        ("Convert 72°F to Celsius", {"value": 72, "from_unit": "fahrenheit", "to_unit": "celsius"}),
        ("How many km is 5 miles?", {"value": 5, "from_unit": "miles", "to_unit": "kilometers"}),
    ]
)
def convert_units(input: ConvertInput) -> dict:
    conversions = {
        ("fahrenheit", "celsius"): lambda v: (v - 32) * 5/9,
        ("celsius", "fahrenheit"): lambda v: v * 9/5 + 32,
        ("miles", "kilometers"): lambda v: v * 1.60934,
        ("kilometers", "miles"): lambda v: v / 1.60934,
        ("kg", "lbs"): lambda v: v * 2.20462,
        ("lbs", "kg"): lambda v: v / 2.20462,
    }
    
    key = (input.from_unit.lower(), input.to_unit.lower())
    if key not in conversions:
        return {"error": f"Conversion from {input.from_unit} to {input.to_unit} not supported"}
    
    result = conversions[key](input.value)
    return {"result": round(result, 2), "from": input.from_unit, "to": input.to_unit}

Now update your agent:

from skills.weather import get_weather
from skills.convert import convert_units

agent = Agent(
    tools=[get_weather, convert_units],
    guardrails=[MaxToolCalls(limit=10), PreventRepeats(window=3)],
    trace=True
)

result = agent.run("What's the weather in Tokyo in Celsius? If it's under 15°C, tell me what that is in Fahrenheit.")

The agent now chains skills together — fetching weather, evaluating a condition, and converting units — all without you writing orchestration logic. OpenClaw's reasoning engine handles the sequencing. The trace shows you every decision it made and why.

Wrapping Existing Code as Skills

One thing I really appreciate about OpenClaw is that it doesn't force you to rewrite your existing codebase. If you already have a library or SDK you use, you can wrap it directly:

from openclaw import Agent, tool

# Your existing class, untouched
class InventorySystem:
    def check_stock(self, product_id: str) -> int:
        """Check current stock level for a product."""
        # existing database logic
        pass
    
    def reorder(self, product_id: str, quantity: int) -> str:
        """Place a reorder for a product."""
        # existing procurement logic
        pass

inventory = InventorySystem()

# Auto-wrap the entire class
agent = Agent(
    tools=tool.from_class(InventorySystem, instance=inventory),
    prompt_template=PromptTemplate.REACT_OPTIMIZED
)

OpenClaw reads your docstrings and type hints to auto-generate descriptions. If your existing code has decent documentation, this takes about thirty seconds. If it doesn't, well, now you have two reasons to write better docstrings.

The Shortcut (If You Want One)

I'll be honest: building your first skill from scratch is a great learning exercise, and I think everyone should do it at least once. It demystifies what agents actually do and gives you the confidence to debug when things go sideways.

But if you're past the learning phase and just want to ship something — or if you'd rather start with working examples and modify them instead of writing boilerplate — Felix's OpenClaw Starter Pack on Claw Mart is genuinely worth the $29. It includes pre-configured skills for the most common use cases (API integrations, data lookups, transformations, notifications), all with proper error handling, retry strategies, and tests already written. I picked it up when I was building my third agent and it saved me probably two full days of setup. The weather skill we built above? There's a more production-ready version in the pack with caching, rate limiting, and multi-provider fallback already wired up. You can tear the skills apart, learn from the patterns, and adapt them to your specific needs. It's like having a senior OpenClaw dev's boilerplate library on day one.

Common Mistakes to Avoid

After building a dozen skills and helping others build theirs, here's what trips people up:

Vague tool descriptions. "Does stuff with data" tells the LLM nothing. Be specific and action-oriented. "Search a product catalog by name or SKU and return price, availability, and description" is what works.

Missing examples in the @tool decorator. I know it feels redundant when you've already written a description. It's not. Examples are the single highest-leverage thing you can add to improve tool selection accuracy.

No error handling. Your skill will encounter bad inputs, network failures, and API quirks. Handle them gracefully or your agent falls over at the worst possible moment.

Over-scoping a single skill. If your skill does five different things, split it into five skills. The agent is better at choosing between focused tools than navigating a Swiss Army knife.

Skipping tests. "It works when I run it manually" is not a testing strategy. Use OpenClaw's MockTool and AgentTest utilities. Your future self will thank you when something breaks at 2 AM.

Where to Go Next

You've got your first skill built, tested, and connected to an agent. Here's what I'd tackle next:

  1. Add checkpointing for multi-step tasks so you don't lose progress on failures. OpenClaw's Checkpoint system with Redis or disk backends takes about five lines to set up.

  2. Enable streaming so users see progress in real-time instead of waiting for the full agent run to complete.

  3. Set up observability. OpenClaw's tracing is great for development, but for production you'll want to pipe execution data into proper monitoring.

  4. Build a multi-skill agent where three or four skills work together on complex tasks. This is where the framework really shines.

  5. Explore prompt templates. OpenClaw's PromptTemplate.REACT_OPTIMIZED works great out of the box, but customizing the role and constraints for your specific use case can significantly improve agent behavior.

The hardest part of building with OpenClaw is getting started. Once you've built that first skill and seen the trace of your agent reasoning through a problem, calling your tool, and returning a useful result — it stops being abstract and starts being addictive.

Now go build something.

Recommended for this post

Generate production-ready SKILL.md templates with proper structure, frontmatter, and guardrails.

All platformsEngineering5 sold
SpookyJuice.aiSpookyJuice.ai
$0Buy

Claw Mart Daily

Get one AI agent tip every morning

Free daily tips to make your OpenClaw agent smarter. No spam, unsubscribe anytime.

More From the Blog