# How to Use `typing.Annotated` with `needle.Field` for Tool Argument Constraints in Python

> Learn to use typing Annotated with needle Field to add JSON Schema constraints to Python function parameters for LLM tool definitions. Enhance your tool definitions with structured constraints.

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

---

**Use `typing.Annotated` wrapped with `needle.Field` to attach JSON Schema constraints directly to function parameters, which `needle` then converts into structured tool definitions for LLM agents.**

The `needle` library automatically generates JSON Schema descriptions for Python functions exposed as tools to LLM agents. According to the `cactus-compute/needle` source code, this process centers on [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), where the `Field` class captures validation metadata and the `tool` decorator processes `Annotated` type hints to build runtime schemas.

## How `needle.Field` Stores Validation Metadata

The `Field` class in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) acts as a container for JSON Schema validation keywords. Its `__init__` method (lines 18–31) accepts standard schema constraints including `ge`, `le`, `min_length`, `max_length`, and `pattern`:

```python

# From needle/agent/tools.py

class Field:
    def __init__(self, *, ge=None, le=None, min_length=None, max_length=None, pattern=None, **kwargs):
        self.ge = ge
        self.le = le
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern
        # ... additional schema keywords

```

When attached to a parameter via `typing.Annotated`, these values persist until schema generation. The `Field.apply` method (lines 36–49) later writes these constraints into the final JSON Schema object by mapping each attribute to its corresponding schema key.

## Extracting Field Data from `Annotated` Types

The `_field_of` function (lines 89–101) handles the extraction logic. When `needle` encounters a parameter annotated as `Annotated[int, needle.Field(ge=0, le=100)]`, `_field_of` inspects the `__metadata__` tuple of the `Annotated` type, locates the `Field` instance, and returns it for processing.

If no `Field` is present, the function returns `None`, and the schema generator proceeds with only the base type information.

## Building the Complete Tool Schema

The `build_schema` function orchestrates the conversion from Python function to JSON Schema. It executes four critical steps according to the source:

1. **Type Inspection**: Uses Python's `inspect` module to retrieve type hints, including metadata from `Annotated` wrappers.

2. **Type Mapping**: The `_json_type` helper (lines 57–86) translates Python types to JSON Schema types, handling special cases like `Literal`, `Enum`, `list`, and `dict`.

3. **Constraint Application**: If `_field_of` returns a `Field` instance, `build_schema` calls `Field.apply` to inject constraint keys (e.g., `minimum`, `maximum`, `pattern`) into the parameter's schema definition.

4. **Required Field Logic**: Parameters without default values and not wrapped in `typing.Optional` are marked as required in the schema's `required` array (lines 38–44).

Finally, the `tool` decorator (lines 73–76) attaches the resulting schema to the function via the `_needle_tool` attribute, making it discoverable by the Needle runtime.

## Practical Examples of Annotated Constraints

### Basic Numeric Constraints

Define integer bounds using `ge` (greater than or equal) and `le` (less than or equal):

```python
import needle
from typing import Annotated

@needle.tool
def set_volume(level: Annotated[int, needle.Field(ge=0, le=100)]) -> str:
    """Set the speaker volume."""
    return f"Volume set to {level}%"

# Inspect the generated schema

print(set_volume._needle_tool)

```

**Output:**

```json
{
  "name": "set_volume",
  "description": "Set the speaker volume.",
  "parameters": {
    "type": "object",
    "properties": {
      "level": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      }
    },
    "required": ["level"]
  }
}

```

The schema includes `minimum` and `maximum` keys populated from the `needle.Field` arguments.

### Optional Parameters with Defaults

Parameters with default values are automatically excluded from the `required` array:

```python
@needle.tool
def log_water_intake(
    amount_ml: Annotated[int, needle.Field(ge=1, le=5000)] = 250
) -> str:
    """Log water intake in millilitres."""
    return f"Logged {amount_ml} ml"

```

Because `amount_ml` has a default value of `250`, the generated schema omits it from the `required` list while retaining the `minimum` (1) and `maximum` (5000) constraints.

### String Constraints

String parameters support `min_length`, `max_length`, and `pattern` as implemented in [`needle/environments/productivity.py`](https://github.com/cactus-compute/needle/blob/main/needle/environments/productivity.py):

```python
@needle.tool
def create_reminder(
    title: Annotated[str, needle.Field(min_length=1, max_length=100)],
    description: Annotated[str, needle.Field(min_length=0)] = ""
) -> str:
    """Create a new reminder."""
    return f"Reminder '{title}' created"

```

## Key Implementation Files

- **[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)**: Core implementation containing `Field.__init__`, `_field_of`, `build_schema`, and the `tool` decorator.
- **[`needle/environments/smart_home.py`](https://github.com/cactus-compute/needle/blob/main/needle/environments/smart_home.py)**: Real-world usage demonstrating `Annotated[int, needle.Field(...)]` for thermostat controls (lines 23–24).
- **[`needle/environments/productivity.py`](https://github.com/cactus-compute/needle/blob/main/needle/environments/productivity.py)**: Examples of string constraints using `min_length` and `max_length` (lines 28–31).
- **[`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py)**: Unit tests verifying `Annotated` + `Field` schema generation (lines 64–66).

## Summary

- **`needle.Field`** captures JSON Schema validation keywords like `ge`, `le`, and `pattern` in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py).
- **`typing.Annotated`** attaches `Field` instances to type hints without altering the runtime type.
- **`_field_of`** extracts `Field` metadata from `Annotated` wrappers during schema generation.
- **The `tool` decorator** stores the final JSON Schema in `fn._needle_tool`, making functions discoverable by the Needle agent runtime.
- **Default values** automatically mark parameters as optional in the generated schema.

## Frequently Asked Questions

### What JSON Schema constraints does `needle.Field` support?

`needle.Field` supports numeric constraints (`ge`, `le`, `gt`, `lt`), string constraints (`min_length`, `max_length`, `pattern`), and array constraints through its constructor in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 18–31). These map directly to JSON Schema keywords `minimum`, `maximum`, `minLength`, `maxLength`, and `pattern`.

### How does Needle handle optional tool parameters?

Needle treats parameters as optional if they have a default value or are wrapped in `typing.Optional`. The `build_schema` function checks for defaults in the function signature (lines 38–44 of [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)) and excludes those parameter names from the schema's `required` array.

### Where is the generated schema stored after applying the `@needle.tool` decorator?

The `tool` decorator attaches the schema dictionary to the function object via the `_needle_tool` attribute (lines 73–76 of [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)). Access it directly with `function_name._needle_tool` to inspect the JSON Schema output.

### Can I use `needle.Field` without `typing.Annotated`?

No, `needle.Field` instances must be attached to type hints using `typing.Annotated`. The `_field_of` function specifically searches the `__metadata__` tuple of `Annotated` types to locate `Field` objects (lines 89–101). Without `Annotated`, the schema builder cannot associate the `Field` with a specific parameter.