# How to Apply Per-Argument Constraints to Tool Parameters Using `needle.Field`

> Learn to apply per-argument constraints to tool parameters with needle.Field and typing.Annotated. Inject JSON-Schema validation rules directly into your tool's parameter schema for robust input handling.

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

---

**Use `needle.Field` with `typing.Annotated` or as a default value to inject JSON-Schema validation rules directly into your tool's parameter schema.**

The `needle` framework provides `Field` as the core mechanism for adding validation constraints to tool parameters. By enriching function signatures with `Field` objects, you control the JSON schema that LLMs receive—enforcing limits like string length, numeric ranges, and regex patterns without writing custom validation logic.

---

## Understanding `needle.Field` Architecture

The constraint system in `needle` follows a four-stage pipeline defined in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py).

### Stage 1: Field Definition

A `Field` instance carries JSON-Schema constraints through its constructor. The source implementation at lines 18–32 accepts parameters like `ge`, `le`, `pattern`, `min_length`, and `max_length`:

- `ge` / `le` — numeric greater-than-or-equal / less-than-or-equal
- `gt` / `lt` — strict numeric inequalities
- `min_length` / `max_length` — string and array bounds
- `pattern` — regex validation for strings
- `description` — human-readable parameter documentation

### Stage 2: Schema Population via `field.apply`

Once the base type is inferred, the schema builder calls `field.apply(schema)` at lines 36–48. This method mutates the schema dictionary in-place, injecting all specified constraints.

### Stage 3: Field Discovery with `_field_of`

The internal `_field_of` function (lines 89–101) inspects each function parameter. It checks two locations:

1. Inside `typing.Annotated[type, Field(...)]` wrappers
2. As the default value for the parameter

When found, the `Field` is returned for schema enrichment.

### Stage 4: Final Schema Assembly

The `build_schema` function (lines 20–47) orchestrates the full process: type inference, constraint application, required/optional flagging, and JSON-Schema output generation.

---

## Method 1: Using `typing.Annotated` (Recommended)

The `Annotated` approach attaches constraints without interfering with runtime values. This preserves standard Python type hints while adding schema metadata.

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

@tool
def search(
    query: Annotated[str, Field(min_length=3, max_length=100)],
    top_k: Annotated[int, Field(ge=1, le=10)] = 5,
) -> str:
    """
    Perform a semantic search.

    Args:
        query: The search string (3-100 characters).
        top_k: Number of results to return (1-10).
    """
    return f"Searching for '{query}' with top_k={top_k}"

```

The generated schema stored at `search._needle_tool` contains:

```json
{
  "name": "search",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "minLength": 3,
        "maxLength": 100
      },
      "top_k": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10,
        "default": 5
      }
    },
    "required": ["query"]
  }
}

```

---

## Method 2: Using `Field` as Default Value

When backward compatibility with older Python versions matters, or when you prefer less verbose signatures, supply `Field` as the parameter default:

```python
from needle.agent.tools import Field, tool

@tool
def resize_image(
    width: int = Field(ge=1, le=4096),
    height: int = Field(ge=1, le=4096),
    keep_aspect: bool = True,
) -> bytes:
    """
    Resize an image to the given dimensions.

    Args:
        width: Desired width in pixels (1-4096).
        height: Desired height in pixels (1-4096).
        keep_aspect: Preserve the original aspect ratio.
    """
    # Implementation returns resized image bytes

    return b""

```

**Key limitation:** This approach consumes the default value slot. You cannot simultaneously provide a runtime default value and attach constraints this way.

---

## Complete Constraint Reference

| Constraint | JSON-Schema Key | Applies To | Example |
|------------|-----------------|------------|---------|
| `ge` | `minimum` | `int`, `float` | `Field(ge=0)` |
| `le` | `maximum` | `int`, `float` | `Field(le=100)` |
| `gt` | `exclusiveMinimum` | `int`, `float` | `Field(gt=0)` |
| `lt` | `exclusiveMaximum` | `int`, `float` | `Field(lt=1.0)` |
| `min_length` | `minLength` | `str`, `list` | `Field(min_length=1)` |
| `max_length` | `maxLength` | `str`, `list` | `Field(max_length=10)` |
| `pattern` | `pattern` | `str` | `Field(pattern="^[a-z]+$")` |
| `description` | `description` | All types | `Field(description="User ID")` |

---

## Validating Your Schema

The `@tool` decorator attaches the generated schema to the function object. Access it to verify your constraints:

```python
from needle.agent.tools import Field, tool

@tool
def example(x: Annotated[int, Field(ge=0, le=100)]) -> int:
    return x

print(example._needle_tool["parameters"]["properties"]["x"])

# {'type': 'integer', 'minimum': 0, 'maximum': 100}

```

For comprehensive test coverage of constraint handling, refer to [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) in the repository.

---

## Summary

- **`needle.Field`** is the container for per-argument JSON-Schema constraints in `needle`.
- **`Annotated[type, Field(...)]`** attaches constraints without affecting runtime behavior.
- **`Field` as default value** provides an alternative syntax with trade-offs.
- **`field.apply(schema)`** at [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) lines 36–48 performs the actual schema mutation.
- **`_field_of`** discovers `Field` instances from either location during parameter inspection.

---

## Frequently Asked Questions

### What happens if I specify conflicting constraints in `Field`?

The `needle` framework passes constraints directly to JSON-Schema without additional validation. Conflicting constraints like `Field(ge=10, le=5)` generate a schema that will reject all values, which the LLM will observe and respect. Test your tool schemas via `fn._needle_tool` before deployment.

### Can I use `needle.Field` without the `@tool` decorator?

No. `Field` is designed for the `needle` schema pipeline in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py). The `_field_of` inspection and `build_schema` functions are only invoked by `@tool`. For standalone schema generation, use `build_schema` directly on a function object.

### Does `Field` support custom JSON-Schema keywords?

The current implementation in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) lines 18–32 defines a fixed set of parameters. For extension, subclass `Field` and override `apply` to inject additional keys into the schema dictionary before `build_schema` completes.

### How do I add descriptions to parameters using `Field`?

Pass `description` to the `Field` constructor:

```python
Annotated[str, Field(min_length=3, description="Search query string")]

```

This populates the `description` key in the generated JSON-Schema, improving LLM understanding of the parameter's purpose.