How to Declare Custom Tools Using the @needle.tool Decorator in Needle

The @needle.tool decorator exposes Python functions as callable tools for the Needle LLM agent by automatically generating a JSON schema from type hints, default values, and docstrings.

This guide explains how to declare custom tools using the @needle.tool decorator in the cactus-compute/needle repository. When applied to any Python function, this decorator inspects the signature and documentation to create a machine-readable schema, making the function discoverable and invocable by the LLM agent while preserving its original callable behavior.

How the @needle.tool Decorator Works

The decorator is implemented in needle/agent/tools.py. When you apply @needle.tool to a function, Needle performs three distinct operations at import time.

First, it calls build_schema(fn) (defined at line 73) to introspect the function’s type hints, default arguments, and docstring. This generates a JSON schema describing the tool’s name, parameters, required fields, and descriptions.

Second, it attaches the generated schema to the function via the private attribute _needle_tool. As seen at line 74 in tools.py, the implementation executes fn._needle_tool = build_schema(fn), marking the function as a discoverable tool.

Third, the decorator returns the original function object unchanged, allowing you to call it directly in Python code without interference from the tooling infrastructure.

Step-by-Step Guide to Declaring a Custom Tool

Follow these steps to expose a Python function as a Needle tool:

  1. Import Needle: Access the decorator via import needle.

  2. Define the function: Write a standard Python function with type hints for every parameter. Use typing.Annotated combined with needle.Field to add JSON Schema constraints like minLength or numerical ranges.

  3. Add a descriptive docstring: Include a clear description and an Args: section. Needle parses this docstring to populate the schema’s description and per-parameter documentation fields.

  4. Apply the decorator: Prefix the function with @needle.tool. This triggers schema generation immediately at import time.

  5. Export the function (optional): Add the function to a module-level TOOLS list, or ensure it is accessible through the environment’s agent property. This step allows needle._harness to discover the tool at runtime.

Practical Code Examples

Basic Tool Without Parameters

The simplest tool requires no arguments and returns a static response.

import needle

@needle.tool
def ping():
    """Return a simple health-check response."""
    return {"ok": True}

The generated schema for this function is {"name": "ping", "parameters": {"type": "object", "properties": {}}}, indicating it accepts no inputs.

Tool with Type Hints and Validation

For production use, define parameters with type hints and validation constraints using Annotated and Field.

import needle
from typing import Annotated, Literal

Priority = Literal["low", "medium", "high"]

@needle.tool
def create_task(
    title: Annotated[str, needle.Field(min_length=1, max_length=120)],
    due_days: int = 7,
    priority: Priority = "medium",
):
    """
    Create a new task in the user's to-do list.

    Args:
        title: Short description of the task.
        due_days: Number of days from now the task is due.
        priority: Desired priority level.
    """
    return {
        "created": True,
        "title": title,
        "due_in": due_days,
        "priority": priority
    }

In this example, Annotated maps Python types to JSON Schema constraints. The Literal type generates an enum constraint, while needle.Field injects minLength and maxLength validations directly into the schema.

Exporting Tools in an Environment Module

To make tools available to the test harness, export them in a TOOLS list within your environment module.


# needle/environments/smart_home.py

import needle
from needle import Field
from typing import Annotated, Literal

@needle.tool
def set_light(
    room: Literal["kitchen", "living_room"],
    brightness: Annotated[int, Field(ge=0, le=100)]
):
    """Set the brightness of a room's light.

    Args:
        room: The target room.
        brightness: Desired brightness percentage (0-100).
    """
    return {"room": room, "brightness": brightness, "status": "ok"}

TOOLS = [set_light]

The TOOLS list in needle/environments/smart_home.py signals to needle._harness which functions belong to this specific environment. The harness reads the _needle_tool attribute attached by the decorator to understand the function’s contract.

Tool Discovery and the Harness

The needle._harness module handles runtime discovery of decorated functions. When an environment loads, the harness inspects module-level TOOLS lists (as seen in needle/environments/wearable.py and smart_home.py) to identify candidate functions.

For each function in these lists, the harness checks for the _needle_tool attribute populated by the decorator. This attribute contains the complete JSON schema necessary to construct LLM-compatible tool definitions. This architecture separates schema generation (performed once at import) from runtime registration (performed by the harness).

Summary

  • @needle.tool generates JSON schemas automatically by inspecting type hints and docstrings in needle/agent/tools.py.
  • The schema is stored in the _needle_tool attribute, leaving the original function callable for direct Python use.
  • Use typing.Annotated with needle.Field to add validation constraints like string length or numeric ranges.
  • Export decorated functions via a module-level TOOLS list to enable discovery by needle._harness.
  • Clear docstrings with Args: sections improve the LLM’s understanding of tool parameters.

Frequently Asked Questions

What is the @needle.tool decorator and where is it defined?

The @needle.tool decorator is defined in needle/agent/tools.py. It wraps Python functions to automatically generate JSON schemas that describe the function’s interface, enabling the Needle LLM agent to understand and invoke them.

How does Needle convert function signatures to JSON schemas?

Needle uses the build_schema utility function (line 73 in needle/agent/tools.py) to introspect function signatures. It examines type hints, default values, and docstrings to construct a schema that includes parameter types, required fields, descriptions, and validation constraints.

Can I add validation constraints like minimum string length or numeric ranges?

Yes. Use typing.Annotated in combination with needle.Field. For example, Annotated[str, needle.Field(min_length=1, max_length=120)] generates a schema with minLength and maxLength constraints, while Annotated[int, Field(ge=0, le=100)] enforces numeric bounds.

How does the Needle harness discover my decorated functions?

The needle._harness module discovers tools by checking for the _needle_tool attribute attached by the decorator. Typically, you export decorated functions in a module-level TOOLS list (as demonstrated in needle/environments/smart_home.py), which the harness scans when loading an environment.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →