huxley
Build a Skill

Tools and Parameters

How to design tool descriptions and JSON Schema parameters the model picks correctly.

The model decides when to call your tool based on its name, description, and parameter schema. Get these right and the model uses your skill naturally; get them wrong and you'll see the model make obvious mistakes (calling tools at the wrong time, passing weird arguments, ignoring tools that should fire).

This page is the practical art of tool design.

A tool definition, in full

ToolDefinition(
    name="play_audiobook",
    description=(
        "Start playing an audiobook by ID. Use this AFTER calling "
        "search_audiobooks — never invent IDs."
    ),
    parameters={
        "type": "object",
        "properties": {
            "book_id": {
                "type": "string",
                "description": "The book's ID, returned by search_audiobooks.",
            },
            "start_position": {
                "type": "number",
                "description": "Seconds from start. Default 0 (begin from start) or last bookmark if known.",
            },
        },
        "required": ["book_id"],
    },
)

Three fields. Each does specific work.

Naming tools

Conventions that pay off:

  • verb_nounplay_book, set_volume, get_news, start_workout. Reads like English.
  • Avoid generic verbs. do_thing, handle_request, run — too vague for the model to map.
  • Prefix by domain when collisions are possible. Two skills both wanting a play() tool should ship play_book and play_radio. Tool names are global within a loaded persona; collisions fail fast.
  • Match the persona's mental model. If your persona says "leer un libro" instead of "play book," the user might say "léeme un libro" — your tool name doesn't need to be in Spanish, but the description should be.

Writing descriptions

The description is for the model, not for the user. The model uses it to decide when to call. Three rules:

Rule 1: state the trigger

Open with a clear "use this when..." or "call this when...":

"Start playing an audiobook by ID. Use this after the user picks one from a search result."

vs the much weaker:

"Plays an audiobook."

The first tells the model when to fire. The second tells it what the function does, which it can guess from the name.

Rule 2: rule out ambiguity

If your tool has a similar name to another tool, disambiguate explicitly:

"Set the speaker volume from 0 (silent) to 100 (max). Do NOT use this for music playback volume — use control_radio for that."

The model gets confused when two tools have overlapping use cases. Spell it out.

Rule 3: write it in the persona's language

If your persona speaks Spanish, the model picks tools much better when descriptions are in Spanish. The shape:

persona.yaml
skills:
  audiobooks:
    i18n:
      es:
        tools:
          play_audiobook:
            description: |
              Reproduce un audiolibro por ID. Úsalo SOLO después de llamar
              search_audiobooks; nunca inventes IDs.
      en:
        tools:
          play_audiobook:
            description: |
              Start playing an audiobook by ID. Use this AFTER calling
              search_audiobooks — never invent IDs.

Skills can read these overrides at setup and use them in their tools property. The audiobooks skill is the canonical example — read it for the pattern.

Parameters: JSON Schema

Tool parameters are a JSON Schema object. The framework passes the schema directly to OpenAI Realtime, which uses it to validate (and shape) the model's tool calls.

Common shapes:

No parameters

parameters={"type": "object", "properties": {}}

Used for tools like get_current_time or pause_music. Even with no params, the parameters key is required — leave the properties dict empty.

One required string

parameters={
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "description": "What to search for.",
        },
    },
    "required": ["query"],
}

Always describe each property. The model uses the description to decide what to put there.

Enum-like fields

parameters={
    "type": "object",
    "properties": {
        "action": {
            "type": "string",
            "enum": ["pause", "resume", "stop", "skip_forward", "skip_backward"],
            "description": "The control action to take.",
        },
    },
    "required": ["action"],
}

Use enum whenever the field has a fixed set of values. The model does much better with enums than with free-form strings.

Numbers with bounds

parameters={
    "type": "object",
    "properties": {
        "level": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Volume from 0 to 100.",
        },
    },
    "required": ["level"],
}

The model respects minimum and maximum. The framework also enforces them — out-of-range calls get rejected before reaching your skill.

Optional with default behavior

parameters={
    "type": "object",
    "properties": {
        "book_id": {"type": "string", "description": "Book to play."},
        "start_position": {
            "type": "number",
            "description": "Seconds from start. Defaults to last bookmark or 0.",
        },
    },
    "required": ["book_id"],
}

Don't list optional fields in required. Document the default in the description; the model will sometimes provide values, sometimes not.

How the model "sees" your tools

Each ToolDefinition becomes part of the system context OpenAI Realtime gets:

You have these tools available:

  search_audiobooks(query: string) — Search the user's library by title or author.
  play_audiobook(book_id: string, start_position?: number) — Start playing.
  audiobook_control(action: pause|resume|stop|skip_forward|skip_backward) — Control playback.
  ...

The model uses this list at every turn. It picks tools based on:

  1. The user's request (what they said).
  2. The tool's name (does it sound like the right action?).
  3. The tool's description (does the trigger phrase match?).
  4. The conversation context (what's already been called this turn?).

The art is making 1-3 line up cleanly so the model picks correctly without reasoning.

Common pitfalls

Iterating on tool design

The fastest loop:

Make the change

Edit your tool's name, description, or schema.

Restart the server

# Ctrl-C the running server, then
uv run huxley

Skills don't hot-reload. Restarts are fast (~2 seconds).

Try the conversation

Hold the PTT button. Say what a real user would say. See if the right tool fires.

Read the dispatch log

coord.tool_dispatch turn=t-7f3 tool=play_audiobook skill=audiobooks args={"book_id":"el_principito"}

This is what the model decided. If it picked the wrong tool, the description is misleading. If the args are weird, the schema is misleading.

After ~10 iterations on a new tool, you'll have a feel for what the model needs. Tool design is the most prompt-engineering-flavored part of skill authoring — don't be surprised that it takes some tuning.

Next

On this page