How to Define Tools Using Manual JSON Schema for the Needle Agent

Bypass automatic schema generation by assigning a custom OpenAI-compatible JSON schema to function._needle_tool to control validation constraints, enum values, and parameter descriptions.

The Needle agent from cactus-compute/needle exposes Python functions as tools that large language models can invoke. By default, the @tool decorator in needle/agent/tools.py introspects type hints and docstrings to build an OpenAI-compatible JSON schema automatically. When you need finer control—custom validation rules, precise descriptions, or enum constraints—you can define the schema manually.

When to Use a Manual JSON Schema

Automatic schema generation covers most cases, but manual construction becomes essential when you need to:

  • Add JSON Schema keywords that build_schema() does not infer, such as format, pattern, or multipleOf
  • Override required field detection to make parameters optional even without default values
  • Supply richer documentation with structured descriptions per property
  • Define enum constraints or numeric ranges with specific validation semantics

Step-by-Step: Attaching a Manual Schema

Follow these three steps to supply your own schema for a tool function.

1. Construct the JSON Schema Dictionary

Build a dictionary following the OpenAI "function calling" format. The structure requires name, description, and a parameters object with properties and required arrays.

manual_schema = {
    "name": "fetch_weather",
    "description": "Retrieve current weather conditions for a location.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "City name, e.g. 'Berlin'."
            },
            "units": {
                "type": "string",
                "enum": ["metric", "imperial"],
                "description": "Temperature unit system."
            }
        },
        "required": ["city"]
    }
}

2. Assign the Schema via _needle_tool

Attach the dictionary to your function using the private attribute _needle_tool. Needle's agent discovery mechanism checks for this attribute before falling back to automatic generation.

def fetch_weather(city: str, units: str = "metric") -> str:
    """Fetch weather data for the specified city."""
    # Implementation omitted

    return f"Weather in {city}: 22°C"

# Override automatic schema with manual definition

fetch_weather._needle_tool = {
    "name": "fetch_weather",
    "description": "Retrieve current weather conditions for a location.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "City name, e.g. 'Berlin'."
            },
            "units": {
                "type": "string",
                "enum": ["metric", "imperial"],
                "default": "metric",
                "description": "Temperature unit system."
            }
        },
        "required": ["city"]
    }
}

3. Expose the Tool to the Agent

Return the function from needle.agent.fetch.get_tools() or your agent's tool collection method. The agent will serialize your exact schema when constructing LLM prompts.


# needle/agent/fetch.py

from .custom_tools import fetch_weather

def get_tools():
    return [fetch_weather]  # Agent receives the manual schema

Complete Working Example

Here is a production-ready pattern combining manual schema definition with proper module organization.


# needle/agent/weather_tools.py

def summarize_forecast(
    days: int,
    location: str,
    include_humidity: bool = False
) -> str:
    """Generate a human-readable weather summary."""
    # Implementation logic

    return f"Forecast for {location}: sunny for {days} days."

# Manual schema with validation constraints

summarize_forecast._needle_tool = {
    "name": "summarize_forecast",
    "description": "Generate a human-readable multi-day weather summary.",
    "parameters": {
        "type": "object",
        "properties": {
            "days": {
                "type": "integer",
                "minimum": 1,
                "maximum": 14,
                "description": "Number of days to forecast (1-14)."
            },
            "location": {
                "type": "string",
                "minLength": 2,
                "description": "City or geographic location."
            },
            "include_humidity": {
                "type": "boolean",
                "default": False,
                "description": "Whether to include humidity percentages."
            }
        },
        "required": ["days", "location"]
    }
}

Registration in the agent's tool fetcher:


# needle/agent/fetch.py

from .weather_tools import summarize_forecast

def get_tools():
    return [
        summarize_forecast,
        # Additional tools...

    ]

Hybrid Approach: Combining Field with Manual Overrides

The needle/agent/tools.py module provides a Field class for metadata-rich type annotations. You can use Field for automatic generation, then selectively override the resulting schema.

from typing import Annotated
from needle.agent.tools import Field, tool

def translate(
    text: str,
    target_lang: Annotated[str, Field(
        description="ISO-639-1 language code",
        enum=["en", "es", "fr", "de", "ja"]
    )],
    formality: Annotated[str, Field(
        description="Register for translation",
        enum=["formal", "informal", "neutral"],
        default="neutral"
    )] = "neutral"
) -> str:
    """Translate text to the target language with formality control."""
    return f"[{target_lang}] {text}"

# Generate base schema, then patch

translate = tool(translate)

# Override specific properties manually

translate._needle_tool["parameters"]["properties"]["text"]["maxLength"] = 5000
translate._needle_tool["parameters"]["properties"]["target_lang"]["description"] = (
    "Target language code (ISO-639-1). Supported: en, es, fr, de, ja."
)

Core Components in needle/agent/tools.py

Component Purpose
Field Dataclass wrapping metadata (description, enum, ge, le, default) for use with Annotated
build_schema(fn) Generates JSON schema from function signature and Field annotations
tool(fn) Decorator that attaches generated schema to fn._needle_tool

When _needle_tool is already present on a function, the tool decorator preserves it and skips build_schema, enabling seamless manual override.

Summary

  • Manual JSON schema definition for Needle tools requires constructing an OpenAI-compatible dictionary and assigning it to function._needle_tool
  • The schema must include name, description, parameters.type="object", parameters.properties, and parameters.required
  • Register tools through needle.agent.fetch.get_tools() for agent discovery
  • Combine Field annotations with manual patches for a hybrid automation approach
  • Source implementation resides in needle/agent/tools.py with discovery logic in needle/agent/fetch.py

Frequently Asked Questions

How does Needle detect whether to use a manual or automatic schema?

The tool decorator and agent discovery code check for the _needle_tool attribute before invoking build_schema. If present, the existing dictionary is used unchanged; otherwise, automatic generation proceeds from type hints and docstrings.

Can I mix manual schema properties with Field metadata?

Yes. Apply the @tool decorator first to generate a base schema from Field annotations, then mutate function._needle_tool directly to add or override specific properties such as pattern, format, or complex anyOf structures.

What happens if my manual schema omits the required field?

The LLM receives a schema with no mandatory arguments, treating all parameters as optional. This can lead to ambiguous tool calls if your function implementation expects certain arguments. Always explicitly specify required based on your runtime needs.

Where does Needle store the schema for registered tools?

The schema dictionary lives on the function object itself as the _needle_tool attribute. This design keeps schema and implementation colocated, simplifying introspection in needle/agent/fetch.py without requiring external registries.

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 →