How to Add Per-Argument Constraints Using needle.Field

Attach JSON Schema constraints to function parameters using needle.Field, either as default values or via typing.Annotated, and the @needle.tool decorator automatically merges them into the generated OpenAI function schema.

The cactus-compute/needle library provides a lightweight mechanism for defining per-argument constraints through the Field class. When you wrap a function with the @tool decorator, the library inspects the signature, extracts type hints, and integrates any Field metadata into the final JSON Schema. This allows you to specify validation rules like numeric bounds, regex patterns, and enum restrictions directly in your Python code.

Understanding the Field Class

The Field class is implemented in needle/agent/tools.py (lines 18-50) and serves as a container for JSON Schema constraint parameters. When instantiated, it stores validation metadata that later gets applied to the schema dictionary during tool registration.

Key methods include:

  • __init__ – Accepts constraint parameters (e.g., ge, max_length, pattern) and stores them as instance attributes.
  • has_default – Returns True when the field was created with an explicit default value, distinguishing it from the internal _MISSING sentinel.
  • apply – Merges non-None constraint values into the target schema dictionary, converting enums to lists and conditionally adding const values.

Available Constraint Parameters

needle.Field supports the full range of JSON Schema validation keywords. When constructing a Field instance, you can combine any of the following arguments:

  • description – Human-readable description appearing in the OpenAI schema.
  • enum – List of allowed values for the parameter.
  • const – Fixed constant value.
  • ge / le – Inclusive minimum and maximum values (>=, <=).
  • gt / lt – Exclusive minimum and maximum values (>, <).
  • multiple_of – Value must be a multiple of this number.
  • min_length / max_length – String length boundaries.
  • pattern – Regular expression the string must match.
  • format – JSON Schema format hint (e.g., "email").
  • min_items / max_items – Array length boundaries.
  • unique_items – When True, requires array elements to be unique.

Schema Construction Pipeline

The integration of Field metadata into the final schema occurs through a three-stage pipeline defined in needle/agent/tools.py:

  1. Detection via _field_of (line 85) – Inspects each parameter to detect Field instances, whether provided directly as default values or wrapped inside typing.Annotated hints.

  2. Base Type Generation – The build_schema function (line 11) constructs the base JSON type for each parameter using _json_type.

  3. Constraint Merging – If a Field is detected, build_schema calls field.apply(schema) to merge constraints into the properties dictionary. The function also adjusts the required array based on whether parameters have defaults or are optional.

Usage Patterns

You can attach per-argument constraints using two primary syntaxes, depending on whether you need to preserve the Python type annotation independently of the constraints.

Direct Default Value Syntax

Provide a Field instance as the parameter default when the constraint itself serves as the default value.

from needle.agent.tools import Field, tool

@tool
def summarize(text: str, max_words: int = Field(default=100, description="Maximum words")):
    """Summarize the given text."""
    pass

In this pattern, Field(default=...) both provides the default value and attaches metadata. The has_default method returns True, and build_schema recognizes this parameter as optional in the required array.

typing.Annotated Syntax

Use typing.Annotated to keep the Python type separate from the constraint metadata, which is essential when you need to apply constraints without providing a runtime default.

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

@tool
def translate(
    text: str,
    target_lang: Annotated[
        str,
        Field(
            description="ISO-639-1 language code",
            pattern="^[a-z]{2}$",
            enum=["en", "es", "fr", "de"]
        )
    ],
) -> str:
    """Translate text to the specified language."""
    pass

Here, _field_of extracts the Field instance from the Annotated metadata, allowing apply to inject the pattern and enum constraints into the generated schema:

{
  "name": "translate",
  "description": "Translate text to the specified language.",
  "parameters": {
    "type": "object",
    "properties": {
      "text": { "type": "string" },
      "target_lang": {
        "type": "string",
        "description": "ISO-639-1 language code",
        "pattern": "^[a-z]{2}$",
        "enum": ["en", "es", "fr", "de"]
      }
    },
    "required": ["text", "target_lang"]
  }
}

Combining Multiple Constraints

Complex validation rules are created by stacking multiple constraint arguments in a single Field call.

@tool
def upload_file(
    path: Annotated[
        str,
        Field(
            description="Path to the file",
            pattern=r"^/data/.*\.csv$",
            max_length=200
        )
    ],
    size_mb: Annotated[int, Field(ge=0, le=10, description="File size in MB")],
):
    """Upload a CSV file smaller than 10 MB."""
    pass

The apply method filters out None values, so you only need to specify the constraints relevant to your use case.

Summary

  • needle.Field provides a declarative way to define JSON Schema constraints for function arguments in cactus-compute/needle.
  • Constraints are defined in needle/agent/tools.py and support validation keywords including ge, pattern, enum, and max_length.
  • The @tool decorator automatically detects Field metadata via _field_of and merges constraints using the apply method during schema generation.
  • Use Field as a default value for simple cases, or wrap it in typing.Annotated when you need to separate type hints from validation rules.

Frequently Asked Questions

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

Using Field as a default value (e.g., param: int = Field(default=5)) assigns that value at runtime and marks the parameter as optional in the schema. Using typing.Annotated (e.g., param: Annotated[int, Field(ge=0)]) attaches constraints without changing the parameter's required status or runtime default, which is necessary when you want the LLM to always provide the value but still validate it against rules.

Does needle.Field support custom JSON Schema formats?

Yes, the format parameter accepts any JSON Schema format string (e.g., "email", "uri", "date-time"). The apply method in needle/agent/tools.py passes this directly into the schema dictionary, making it compatible with validators that respect the JSON Schema specification.

How does needle handle conflicting constraints?

needle.Field performs no runtime validation of constraint combinations; it simply merges provided values into the schema via the apply method. The OpenAI API or your downstream JSON Schema validator will enforce the constraints. If you provide logically incompatible rules (e.g., gt=10 and lt=5), the resulting schema will contain both constraints, and validation will fail when the function is called.

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 →