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

> Learn to define custom JSON schemas for the Needle agent by assigning a JSON schema to function._needle_tool. Control validation, enums, and descriptions for your tools.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: how-to-guide
- Published: 2026-09-06

---

**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](https://github.com/cactus-compute/needle) exposes Python functions as **tools** that large language models can invoke. By default, the `@tool` decorator in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/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.

```python
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.

```python
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.

```python

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

```python

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

```python

# 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`](https://github.com/cactus-compute/needle/blob/main/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.

```python
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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) with discovery logic in [`needle/agent/fetch.py`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/needle/agent/fetch.py) without requiring external registries.