How to Specify Argument Constraints Using needle.Field in Tool Definitions

Use typing.Annotated to wrap your parameter type with needle.Field, then pass constraint parameters like ge, le, min_length, or pattern to automatically generate JSON Schema validation rules and enforce them at runtime for LLM tool calls.

The cactus-compute/needle repository provides a lightweight framework for converting Python functions into tools that Large Language Models (LLMs) can invoke. When you specify argument constraints using needle.Field, you create self-documenting validation logic that both restricts user inputs and guides the LLM toward generating valid function calls.

The needle.Field Validation Pipeline

When you decorate a function with @needle.tool, the runtime inspects your type hints to build a JSON Schema description of the function signature. This process occurs in three distinct phases within needle/agent/tools.py.

Annotating Parameters with typing.Annotated

Python's typing.Annotated wrapper allows you to attach metadata to type hints without changing the runtime type. To apply constraints, wrap your base type with Annotated and include a needle.Field instance as the second argument.

from typing import Annotated
import needle

@needle.tool
def set_thermostat(
    temperature: Annotated[int, needle.Field(ge=10, le=30)]
):
    """Set temperature between 10°C and 30°C."""
    return {"temperature": temperature}

The Annotated[int, needle.Field(...)] syntax tells Needle that this parameter accepts integers, but with additional validation metadata attached.

Schema Extraction and Constraint Application

The schema building process relies on two key functions in needle/agent/tools.py. First, _field_of (lines 18‑23) scans the parameter annotation to detect any Field objects. If found, the Field.apply method (lines 36‑45) merges your constraints—such as ge, le, pattern, or min_length—into the generated JSON Schema dictionary.

For required parameters, the logic at lines 33‑39 checks whether the parameter has a default value or is wrapped in Optional. If neither condition is met, the parameter name is added to the schema's required list.

Constraint Types and Implementation Examples

needle.Field supports the full range of JSON Schema validation keywords. You can combine multiple constraints on a single parameter to create complex validation rules.

Numeric Range Constraints

Use ge (greater than or equal) and le (less than or equal) to define inclusive bounds for numeric arguments.

@needle.tool
def set_thermostat(
    temp: Annotated[int, needle.Field(ge=10, le=30)]
):
    """Temperature must be between 10 and 30 degrees."""
    pass

String Length and Pattern Validation

Restrict string inputs by length using min_length and max_length, or enforce formats with regex pattern.

@needle.tool
def create_reminder(
    msg: Annotated[str, needle.Field(min_length=1, max_length=120)]
):
    """Message must be 1-120 characters."""
    pass

For complex validation, use a regex pattern. This example from needle/environments/data_capture.py validates phone number formats:

from typing import Optional, Annotated

@needle.tool
def log_contact(
    phone: Optional[Annotated[str, needle.Field(pattern=r"^\+?[0-9][0-9 -]{5,17}$")]] = None
):
    """Log a contact with optional international phone number validation."""
    pass

Enumerations and Constant Values

Use Python's Literal type for static enumerations, or Field(const=...) to restrict an argument to exactly one value.

from typing import Literal

@needle.tool
def control_lights(
    room: Literal["kitchen", "living_room"],
    forced_mode: Annotated[str, needle.Field(const="auto")] = "auto"
):
    """Room must be from the list; forced_mode is always 'auto'."""
    pass

Optional Parameters with Conditional Constraints

When arguments are optional, wrap the Annotated type inside Optional. Constraints apply only when the value is provided, not when it is None.

from typing import Optional

@needle.tool
def dim_lights(
    brightness: Annotated[Optional[int], needle.Field(ge=0, le=100)] = None
):
    """Optional brightness, but if provided must be 0-100."""
    pass

Complete Smart Home Implementation

The needle/environments/smart_home.py file demonstrates the full pattern in production code (lines 19‑24). This example combines required Literal enums with optional numeric constraints:

import needle
from typing import Annotated, Literal, Optional

@needle.tool
def control_lights(
    room: Literal["kitchen", "living_room", "bedroom", "study"],
    action: Literal["on", "off", "dim"],
    brightness_percent: Annotated[Optional[int], needle.Field(ge=0, le=100)] = None,
    color: Optional[Literal["warm white", "cool white", "red", "green", "blue"]] = None,
):
    """Turn lights on/off, dim to a percentage, or set a color."""
    return {
        "ok": True,
        "room": room,
        "action": action,
        "brightness_percent": brightness_percent,
        "color": color
    }

When the LLM receives the generated schema from build_schema, it knows that room must be one of the four specified strings, brightness_percent cannot exceed 100, and color is restricted to specific literal values.

Summary

  • needle.Field attaches JSON Schema constraints directly to Python type hints using typing.Annotated.
  • The _field_of function extracts Field metadata during schema generation in needle/agent/tools.py.
  • Constraint parameters like ge, le, min_length, max_length, pattern, and const map directly to JSON Schema keywords.
  • Optional parameters use Optional[Annotated[...]] syntax to apply constraints only when values are provided.
  • Validating arguments at the schema level prevents invalid inputs from reaching your tool logic and reduces LLM hallucinations.

Frequently Asked Questions

What happens if the LLM provides a value that violates a needle.Field constraint?

The Needle runtime validates incoming arguments against the generated JSON Schema before executing your function. If a value falls outside the specified ge/le bounds, exceeds max_length, or fails a pattern match, the tool call is rejected immediately with a validation error, and the LLM receives feedback to correct its input.

Can I combine multiple constraints on a single field?

Yes. You can pass multiple keyword arguments to a single needle.Field instance. For example, needle.Field(ge=0, le=100, description="Percentage value") applies both numeric bounds and a documentation string to the schema. The Field.apply method merges all provided constraints into the final schema dictionary.

When should I use Literal instead of needle.Field(const=...)?

Use Literal["a", "b"] when the argument can be one of several specific string values, as it generates an enum in the JSON Schema. Use needle.Field(const="value") when the argument must always be exactly one specific value, effectively making it a constant that the LLM must match. The latter is useful for API compatibility where a parameter exists but has only one valid state.

How does Needle determine if a parameter is required?

According to the source code in needle/agent/tools.py (lines 33‑39), a parameter is marked as required if it has no default value and is not wrapped in typing.Optional. If you assign a default value (e.g., = None) or use Optional[...], the parameter is omitted from the schema's required array.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →