> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fliiq.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authoring Skills

> Build custom skills with 3 files

Creating a Fliiq skill is straightforward: write 3 files, drop them in a directory, and Fliiq discovers them automatically.

## The 3-File Format

Every skill needs three files (plus an optional test file):

```
.fliiq/skills/weather/
  SKILL.md      # Name, description, LLM instructions
  fliiq.yaml    # Input/output schema, credentials
  main.py       # Async handler
  test_main.py  # Pytest tests (optional)
```

## Step-by-Step: Build a Weather Skill

### 1. Create the directory

```bash theme={null}
mkdir -p ~/.fliiq/skills/weather
```

### 2. Write SKILL.md

```markdown theme={null}
---
name: weather
description: "Get current weather for a city using OpenWeatherMap API."
---

Use this tool to get the current weather for a given city.
Returns temperature, conditions, humidity, and wind speed.
Requires OPENWEATHER_API_KEY environment variable.
```

The YAML frontmatter (`name`, `description`) is used by the tool registry. The markdown body is injected into the LLM prompt as instructions.

### 3. Write fliiq.yaml

```yaml theme={null}
input_schema:
  type: object
  properties:
    city:
      type: string
      description: "City name (e.g., 'San Francisco', 'London')"
    units:
      type: string
      enum: ["metric", "imperial"]
      description: "Temperature units. Defaults to metric."
  required:
    - city

output_schema:
  type: object
  properties:
    temperature:
      type: number
      description: "Current temperature"
    conditions:
      type: string
      description: "Weather conditions (e.g., 'Clear', 'Rain')"
    humidity:
      type: integer
      description: "Humidity percentage"
    wind_speed:
      type: number
      description: "Wind speed"

credentials:
  - OPENWEATHER_API_KEY
```

**`input_schema`** — JSON Schema defining what the LLM passes to the handler. Must be `type: object`.

**`output_schema`** — Documents the return format. Used by the LLM to understand the response.

**`credentials`** — List of environment variable names. Fliiq loads these from `.env` before calling the handler.

### 4. Write main.py

```python theme={null}
import os
import httpx


async def handler(params: dict) -> dict:
    """Get current weather for a city."""
    city = params["city"]
    units = params.get("units", "metric")

    api_key = os.environ.get("OPENWEATHER_API_KEY")
    if not api_key:
        raise ValueError(
            "OPENWEATHER_API_KEY not set. "
            "Get one at https://openweathermap.org/api"
        )

    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(
            "https://api.openweathermap.org/data/2.5/weather",
            params={"q": city, "appid": api_key, "units": units},
        )
        resp.raise_for_status()
        data = resp.json()

    return {
        "temperature": data["main"]["temp"],
        "conditions": data["weather"][0]["main"],
        "humidity": data["main"]["humidity"],
        "wind_speed": data["wind"]["speed"],
    }
```

### 5. Add the API key

```bash .env theme={null}
OPENWEATHER_API_KEY=your-key-here
```

### 6. Test it

```bash theme={null}
fliiq run "what's the weather in San Francisco?"
```

Fliiq auto-discovers the skill from `~/.fliiq/skills/weather/` and makes it available to the agent.

## Handler Contract

The handler function must follow these rules:

| Rule              | Detail                                                           |
| ----------------- | ---------------------------------------------------------------- |
| **Function name** | Must be `handler`                                                |
| **Signature**     | `async def handler(params: dict) -> dict`                        |
| **Async**         | Must be an async function                                        |
| **Params**        | Dict matching `input_schema` from fliiq.yaml                     |
| **Return**        | Dict matching `output_schema` from fliiq.yaml                    |
| **HTTP client**   | Use `httpx` (not `requests`) for async compatibility             |
| **Errors**        | Raise `ValueError` with a clear message — never fail silently    |
| **Credentials**   | Read from `os.environ` (loaded from `.env` by the skill runtime) |

## Real Example: web\_search

Here's the actual `web_search` skill that ships with Fliiq:

```python theme={null}
import json
import os

import httpx

BRAVE_API_URL = "https://api.search.brave.com/res/v1/web/search"


async def handler(params: dict) -> dict:
    """Search the web using Brave Search API."""
    query = params["query"]
    count = min(params.get("count") or 5, 20)

    api_key = os.environ.get("BRAVE_API_KEY")
    if not api_key:
        raise ValueError(
            "BRAVE_API_KEY not set. Get one at https://brave.com/search/api/"
        )

    headers = {
        "Accept": "application/json",
        "Accept-Encoding": "gzip",
        "X-Subscription-Token": api_key,
    }

    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(
            BRAVE_API_URL,
            params={"q": query, "count": count},
            headers=headers,
        )
        resp.raise_for_status()
        data = resp.json()

    web_results = data.get("web", {}).get("results", [])
    results = [
        {
            "title": r.get("title", ""),
            "url": r.get("url", ""),
            "snippet": r.get("description", ""),
        }
        for r in web_results[:count]
    ]

    return {"results": json.dumps(results, indent=2)}
```

## Promoting to Core

Once you've validated a local skill, promote it so it ships with your project:

```bash theme={null}
fliiq skill-promote weather
```

This moves it from `.fliiq/skills/weather/` to `skills/core/weather/`. See [Promoting Skills](/skills/promoting) for details.
