# How to Manually Declare a Tool Schema in Needle: 3 Methods Explained

> Learn to manually declare a tool schema in Needle with 3 methods. Assign JSON-schema, use the @tool decorator, or generate and modify a base schema for efficient tool integration.

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

---

**To manually declare a tool schema in Needle, assign a JSON-schema dictionary to your function's `_needle_tool` attribute, use the `@tool` decorator for automatic generation, or generate a base schema with `build_schema()` and modify it.**

Needle treats every **tool** as a Python callable with a JSON-schema description attached. This schema powers the OpenAI-compatible function-calling interface that lets agents discover and invoke your tools. In `cactus-compute/needle`, you have three ways to attach this schema—ranging from fully automatic to completely manual control.

## What Is a Tool Schema in Needle?

A tool schema follows the OpenAI function-calling format:

```json
{
  "name": "<function name>",
  "description": "<docstring or explicit description>",
  "parameters": {
    "type": "object",
    "properties": {
      "<arg>": { "type": "...", "description": "...", … }
    },
    "required": [ "<arg>", … ]
  }
}

```

Needle stores this schema on the function object itself under `_needle_tool`, a private attribute checked at runtime ([`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py), lines 146–148). When the library loads your tool, it first looks for `_needle_tool`; if missing, it falls back to auto-generating a schema via `build_schema(entry)`.

## Method 1: The @tool Decorator (Automatic Schema)

The **`@tool` decorator** in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 173–176) is the fastest way to declare a tool schema. It automatically extracts type hints, docstrings, and `Field` constraints:

```python
from needle import tool, Field

@tool                                   # attaches auto-generated schema to _needle_tool

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

```

Behind the scenes, `@tool` simply calls `build_schema(fn)` and assigns the result:

```python
def tool(fn: Callable) -> Callable:
    fn._needle_tool = build_schema(fn)
    return fn

```

The resulting `add._needle_tool` contains a complete JSON schema validated by the test suite ([`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py), lines 14–24).

## Method 2: Direct Assignment of a Custom Schema Dict

For **full manual control**, build the schema yourself and assign directly to `_needle_tool`:

```python
def greet(name: str) -> str:
    """Greet someone."""
    return f"Hi {name}"

# Manual schema declaration—every field under your control

greet._needle_tool = {
    "name": "greet",
    "description": "Greet someone with a warm welcome.",
    "parameters": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "Name of the person to greet",
                "maxLength": 50
            }
        },
        "required": ["name"]
    }
}

```

**Use this when:**
- You need non-standard fields (e.g., `maxLength`, `pattern`, custom metadata)
- The schema originates from external configuration or an API spec
- You want to override automatic type inference

## Method 3: Generate with build_schema(), Then Tweak

The **`build_schema(fn)` helper** ([`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), lines 20–52) extracts type hints and docstring arguments, letting you start from automation and customize selectively:

```python
from needle.agent.tools import build_schema

def download(url: str, timeout: int = 30) -> bytes:
    """Download a file from the given URL."""
    ...

# Generate base schema from type hints

download._needle_tool = build_schema(download)

# Add custom JSON-schema constraints

download._needle_tool["parameters"]["properties"]["timeout"]["minimum"] = 5
download._needle_tool["parameters"]["properties"]["url"]["format"] = "uri"

```

This pattern appears in [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) (lines 62–78), demonstrating how to layer manual adjustments on generated foundations.

## Using Field for Schema-Aware Validation

For fine-grained control without full manual declaration, use **`Field`** from [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 18–32):

```python
from needle import tool, Field

@tool
def upload(
    file_path: str,
    size: int = Field(ge=0, le=1073741824, description="File size in bytes"),
    priority: int = Field(default=1, ge=1, le=5)
) -> bool:
    """Upload a file to remote storage."""
    ...

```

`Field` parameters translate directly to JSON-schema constraints: `ge`/`le` become `minimum`/`maximum`, `description` populates property docs, and defaults are captured automatically.

## How Needle Loads Tool Schemas

The library's entry point ([`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py), lines 146–148) implements this resolution order:

```python

# Pseudocode from source analysis

if hasattr(entry, "_needle_tool"):
    schema = entry._needle_tool          # ← manual or decorator-attached

else:
    schema = build_schema(entry)         # ← fallback auto-generation

```

This design means **manual declarations always take precedence**, and you can mix decorated, manually assigned, and runtime-generated schemas in the same project.

## Summary

- **`@tool` decorator**: Fastest path—automatic schema from type hints and docstrings
- **Direct `_needle_tool` assignment**: Total control for custom or externally sourced schemas
- **`build_schema()` + modification**: Best of both worlds—automation plus selective overrides
- **`Field` class**: Declarative constraints without leaving the function signature

All three approaches store the final schema in the same location (`fn._needle_tool`), ensuring consistent behavior across Needle's agent environments and test suites.

## Frequently Asked Questions

### How do I override a single field in an auto-generated schema?

Call `build_schema(fn)` first, then modify the returned dictionary before assigning to `_needle_tool`. This preserves type-hint inference while letting you adjust specific properties, as shown in [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) (lines 62–78).

### Can I use a JSON file to define my tool schema?

Yes—load the JSON into a Python dict and assign directly to `my_function._needle_tool`. Needle does not restrict the schema source; it only validates that `_needle_tool` exists and contains valid OpenAI-compatible JSON schema.

### What happens if I forget to declare a schema?

Needle falls back to `build_schema(entry)` automatically ([`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py), lines 146–148). Your tool will still work, but you lose control over descriptions, constraints, and field ordering. Explicit declaration is recommended for production agents.

### Does the @tool decorator work with async functions?

The decorator operates on the callable object before invocation, so it attaches `_needle_tool` to async def functions the same way. The schema generation in `build_schema()` inspects type hints and docstrings, not the function's coroutine status.