How to Define Tools Using the @needle.tool Decorator: A Complete Guide

The @needle.tool decorator exposes any Python function as an LLM-callable tool by automatically generating a JSON schema from the function's type hints, docstring, and Field metadata.

The decorator is implemented in the cactus-compute/needle repository and provides the bridge between Python code and agent-based systems. When applied, it attaches a _needle_tool attribute containing an OpenAI-compatible function schema that Needle's runtime can discover and invoke.

How the @needle.tool Decorator Works

The core implementation lives in needle/agent/tools.py. Understanding the internal mechanics helps you leverage the decorator effectively.

Schema Generation Pipeline

The build_schema(fn) function (lines 20-50) drives the conversion process:

  1. Signature inspection – extracts parameter names, type hints, and defaults
  2. Docstring parsing – pulls the function description and parameter documentation
  3. Field metadata processing – applies validation constraints from Field objects
  4. Type mapping – converts Python types to JSON Schema via _json_type (lines 57-86)

The decorator itself is minimal (lines 73-76): it simply calls build_schema(fn) and stores the result as fn._needle_tool.

Optional Parameter Handling

The _is_optional helper (lines 52-55) detects Optional[T] or T | None syntax. Optional parameters are excluded from the required array in the generated schema, matching OpenAI's function calling specification.

Basic Tool Definition

Import needle_tool from needle.agent.tools and apply it to any function:

from needle.agent.tools import needle_tool

@needle_tool
def echo(message: str) -> str:
    """Return the same text that was given."""
    return message

The decorator populates echo._needle_tool with this schema:

{
  "name": "echo",
  "description": "Return the same text that was given.",
  "parameters": {
    "type": "object",
    "properties": {
      "message": {"type": "string"}
    },
    "required": ["message"]
  }
}

The function name becomes the tool name, the docstring becomes the description, and type hints map to JSON Schema types automatically.

Adding Validation with Field

Use Field objects to specify constraints, defaults, and metadata:

from needle.agent.tools import needle_tool, Field

@needle_tool
def scale(value: float, factor: float = Field(default=1.0, ge=0.0, le=10.0)):
    """Multiply `value` by `factor`."""
    return value * factor

The generated schema includes validation rules:

"factor": {
  "type": "number",
  "default": 1.0,
  "minimum": 0.0,
  "maximum": 10.0
}

Common Field parameters that translate to JSON Schema:

  • ge / leminimum / maximum
  • gt / ltexclusiveMinimum / exclusiveMaximum
  • min_length / max_lengthminLength / maxLength
  • patternpattern
  • enumenum

Using Enums and Literals

The decorator handles both Enum classes and Literal types for constrained choices:

from enum import Enum
from typing import Literal
from needle.agent.tools import needle_tool, Field

class Color(Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@needle_tool
def paint(surface: str, colour: Color | Literal["yellow"] = Field(default=Color.RED)):
    """Paint a surface with the chosen colour."""
    return f"{surface} painted {colour}"

The schema merges all allowed values: ["red", "green", "blue", "yellow"]. Enum members serialize to their values, so Color.RED becomes "red" in the default field.

Registering Multiple Tools from a Module

Organize tools in dedicated modules and register them collectively:


# math_tools.py

from needle.agent.tools import needle_tool

@needle_tool
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

@needle_tool
def subtract(a: int, b: int) -> int:
    """Subtract b from a."""
    return a - b

# main.py

import importlib
from needle.agent import register_tools

module = importlib.import_module("math_tools")
tools = register_tools(module)

register_tools scans the module's globals, identifies all objects with _needle_tool attributes, and returns a list ready for agent initialization.

Accessing and Debugging Tool Schemas

Inspect the generated schema directly for debugging or external integration:

>>> print(add._needle_tool)
{'name': 'add',
 'description': 'Add two integers.',
 'parameters': {'type': 'object',
                'properties': {'a': {'type': 'integer'},
                               'b': {'type': 'integer'}},
                'required': ['a', 'b']}}

This attribute is available immediately after decoration—no runtime registration required.

Real-World Examples in the Repository

The cactus-compute/needle codebase demonstrates production usage:

These files illustrate patterns for naming conventions, docstring formatting, and validation design that align with LLM interpretation.

Summary

  • @needle_tool attaches a JSON schema to any function, making it discoverable by Needle agents
  • Schema generation is automatic from type hints, docstrings, and Field metadata in needle/agent/tools.py
  • Field objects add validation constraints and defaults that map directly to JSON Schema
  • Optional parameters ( T | None or Optional[T]) are excluded from the required array
  • register_tools collects all decorated functions from a module for agent initialization
  • _needle_tool attribute provides direct access to the schema for debugging and integration

Frequently Asked Questions

What Python versions support the @needle.tool decorator?

The decorator uses modern Python features including | union syntax and typing.Annotated. According to the source code, it requires Python 3.10 or newer. Earlier versions may work with adjustments to type hint syntax.

Can I use Pydantic models instead of individual parameters?

No—the current implementation in needle/agent/tools.py builds schemas from function signatures directly. For Pydantic-style validation, use Field objects within the function parameters rather than model classes.

How does the decorator handle complex return types?

Return type annotations are captured in the schema but not validated at runtime. The schema's parameters field describes inputs only; return type information is stored separately and may be used by agent orchestration layers for result formatting.

What happens if two tools have the same name?

The schema uses the Python function name as the tool name. Duplicate names in the same scope will overwrite earlier registrations when register_tools scans the module. Use distinct function names or organize tools in separate namespaces.

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 →