Skip to main content
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

mkdir -p ~/.fliiq/skills/weather

2. Write SKILL.md

---
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

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

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

.env
OPENWEATHER_API_KEY=your-key-here

6. Test it

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:
RuleDetail
Function nameMust be handler
Signatureasync def handler(params: dict) -> dict
AsyncMust be an async function
ParamsDict matching input_schema from fliiq.yaml
ReturnDict matching output_schema from fliiq.yaml
HTTP clientUse httpx (not requests) for async compatibility
ErrorsRaise ValueError with a clear message — never fail silently
CredentialsRead from os.environ (loaded from .env by the skill runtime)
Here’s the actual web_search skill that ships with Fliiq:
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:
fliiq skill-promote weather
This moves it from .fliiq/skills/weather/ to skills/core/weather/. See Promoting Skills for details.