# How to Define Constraints for Tool Parameters Using needle.Field and Annotated

> Learn how to define constraints for tool parameters using needle.Field and Annotated. Easily add JSON Schema constraints to your function parameters for tool schema generation.

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

---

**Use `needle.Field` either as a default argument value or inside `typing.Annotated` to attach JSON Schema constraints directly to function parameters; the `@tool` decorator automatically extracts these constraints in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) and injects them into the generated OpenAI-compatible tool schema.**

The **cactus-compute/needle** library provides a lightweight framework for building LLM-compatible tools in Python. Defining constraints for tool parameters using `needle.Field` and `Annotated` ensures that your functions receive validated data that matches specific formats, ranges, or patterns before execution. This approach leverages standard JSON Schema vocabulary while maintaining clean, readable Python type hints.

## Understanding needle.Field and JSON Schema Constraints

`needle.Field` is a lightweight descriptor class that attaches validation metadata to function parameters. When you decorate a function with `@tool`, the `build_schema` function in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) inspects the signature, extracts `Field` instances via the `_field_of` helper, and translates arguments like `ge`, `le`, `min_length`, and `pattern` into corresponding JSON Schema keywords.

The `Field.apply` method (lines 18‑31 in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)) handles the actual mapping. For example, `ge=10` becomes `minimum: 10` in the schema, while `pattern=r"^\+?[0-9]+"` becomes `pattern: "^\\+?[0-9]+"`. This generated schema is stored in the function's `._needle_tool` attribute, making the constraints available to LLM runtimes.

## Declaring Constraints with Field Defaults

You can attach constraints by using `needle.Field` as a default value for a parameter. This pattern works best when the parameter is required but needs validation rules.

### Basic Syntax Using Default Values

```python
from needle import tool, Field

@tool
def set_thermostat(
    temperature: int = Field(description="Target temperature in °C", ge=10, le=30)
) -> None:
    """Set the thermostat to a specific temperature."""
    ...

```

In this example, `build_schema` extracts the `Field` metadata and produces a JSON Schema fragment specifying that `temperature` must be an integer between 10 and 30. The resulting schema is accessible via `set_thermostat._needle_tool`:

```json
{
  "name": "set_thermostat",
  "description": "Set the thermostat to a specific temperature.",
  "parameters": {
    "type": "object",
    "properties": {
      "temperature": {
        "type": "integer",
        "description": "Target temperature in °C",
        "minimum": 10,
        "maximum": 30
      }
    },
    "required": ["temperature"]
  }
}

```

## Using Annotated for Cleaner Type Hints

The `typing.Annotated` pattern separates the type hint from the default value, which is essential when you need constraints on a parameter that has an actual default value or when you prefer explicit metadata attachment.

### The Annotated Pattern

```python
from typing import Annotated
from needle import tool, Field

@tool
def create_reminder(
    title: Annotated[str, Field(min_length=1, max_length=120)],
    when: str,
) -> None:
    """Create a reminder with a validated title."""
    ...

```

When `build_schema` processes the `create_reminder` signature (lines 23‑38 in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)), it detects the `Field` instance inside the `Annotated` wrapper and applies `minLength` and `maxLength` constraints to the `title` property. This method avoids conflating default values with validation logic.

## Combining Multiple Validation Rules

Complex validation scenarios require stacking multiple constraints on a single parameter. You can mix numeric bounds, string length limits, and pattern matching within a single `Field` definition.

```python
from typing import Annotated
from needle import tool, Field

@tool
def log_water_intake(
    amount_ml: Annotated[int, Field(ge=1, le=5000)],
    note: Annotated[str, Field(max_length=200)] = ""
) -> None:
    """Log a water-intake event."""
    ...

```

Here, `amount_ml` enforces a valid range for milliliters (1‑5000), while `note` limits the optional comment length. The `@tool` decorator processes both parameters through the same `_field_of` extraction logic, merging all constraints into the final schema.

## Pattern and Format Validation

For string parameters requiring specific formats—such as email addresses or phone numbers—use the `format` and `pattern` arguments to inject regex-based validation directly into the JSON Schema.

```python
from typing import Annotated
from needle import tool, Field

@tool
def register_user(
    email: Annotated[str, Field(format="email")],
    phone: Annotated[str, Field(pattern=r"^\+?[0-9][0-9 -]{5,17}$")] = None,
) -> None:
    """Register a new user with validated contact info."""
    ...

```

The `format="email"` constraint hints to the LLM that the input should match the email format, while the `pattern` argument provides a strict regex for international phone numbers. These constraints are applied in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) during the schema assembly phase.

## How Schema Generation Works Under the Hood

The constraint injection happens in three stages inside [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py):

1. **Signature Inspection**: The `build_schema` function (lines 15‑46) introspects the decorated function using Python's `inspect` module.
2. **Metadata Extraction**: The `_field_of` helper extracts `Field` instances whether they appear as default values or inside `Annotated` wrappers.
3. **Schema Assembly**: The `Field.apply` method (lines 18‑31) translates Python arguments into JSON Schema keywords and merges them into the parameter definition.

After processing, the complete schema dictionary is bound to the function as `func_def._needle_tool`, allowing the runtime to present constrained parameters to the LLM.

## Summary

- **`needle.Field`** acts as a bridge between Python type hints and JSON Schema validation keywords.
- Use `Field(...)` as a default value for required parameters, or wrap it in **`Annotated[<type>, Field(...)]`** to avoid interfering with actual default values.
- Supported constraints include `ge`/`le` (numeric ranges), `min_length`/`max_length` (string bounds), `pattern` (regex), `enum` (allowed values), and `format` (semantic types).
- The **`@tool`** decorator in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) automatically extracts these fields via `build_schema` and stores the final schema in `._needle_tool`.
- Refer to [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) for additional usage examples and edge cases.

## Frequently Asked Questions

### What is the difference between using Field as a default and using Annotated?

Using `Field` as a default value (`param: int = Field(...)`) attaches constraints but requires the parameter to have that specific default, which can be problematic if you need a different default value or want the parameter to be required. Using **`Annotated[int, Field(...)]`** separates the type and metadata from the default value, allowing you to specify constraints while keeping the parameter required or assigning it a separate default like `= None`.

### Which JSON Schema keywords does needle.Field support?

According to the `apply` method in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 18‑31), `needle.Field` supports `ge`/`le` (mapped to `minimum`/`maximum`), `min_length`/`max_length` (mapped to `minLength`/`maxLength`), `pattern` (mapped to `pattern`), `enum` (mapped to `enum`), and `format` (mapped to `format`). Additional Pydantic-compatible arguments are passed through to the schema generator.

### How do I make a parameter optional when using Annotated?

To make a parameter optional when using `Annotated`, simply assign a default value after the annotation: `param: Annotated[str, Field(...)] = ""` or `= None`. The presence of the default value in the function signature marks it as optional in the generated JSON Schema, while the `Field` constraints inside `Annotated` still apply to any provided value.

### Where is the generated schema stored after using the @tool decorator?

The `@tool` decorator stores the complete JSON Schema in the **`._needle_tool`** attribute of the decorated function. You can inspect this dictionary at runtime to see the exact constraints and types that will be sent to the LLM, as demonstrated in the `set_thermostat._needle_tool` example.