How Needle Handles Requests with No Declared Tools: Automatic Schema Generation Explained

Needle automatically generates a JSON-Schema tool definition from any function's signature when no explicit tool is declared, enabling seamless LLM invocation without boilerplate decorators.

When building LLM-powered applications, developers often face friction between rapid prototyping and explicit tool definitions. Needle, an open-source framework by cactus-compute, eliminates this trade-off by inferring tool schemas directly from Python function signatures. This article examines exactly how Needle handles requests when no tools are declared, diving into the runtime mechanism that makes any callable instantly LLM-compatible.

Automatic Schema Discovery in Needle

Needle's entry-point resolution lives in needle/__init__.py. When processing a request, Needle inspects the target function for a _needle_tool attribute containing a pre-computed schema. If absent, it automatically synthesizes one.


# needle/__init__.py (excerpt)

schema = getattr(entry, "_needle_tool", None) or build_schema(entry)

This single line implements Needle's zero-configuration philosophy. The or operator provides clean fallback behavior: explicit tools take precedence, while undeclared functions trigger automatic schema generation.

How build_schema Generates Tool Definitions

The build_schema function in needle/agent/tools.py performs runtime introspection to construct OpenAI-compatible tool specifications. It examines:

  • Type hints — mapped to JSON-Schema parameter types
  • Default values — determines required vs. optional fields
  • Docstring arguments — extracted for parameter descriptions

The resulting schema includes all standard tool fields: name, description, parameters, and required arrays.


# Example: Function without explicit tool declaration

def get_timezone(city: str) -> str:
    """Return the timezone for a city."""
    ...

# Needle automatically generates:

# {

#   "name": "get_timezone",

#   "description": "Return the timezone for a city.",

#   "parameters": {

#     "type": "object",

#     "properties": {"city": {"type": "string"}},

#     "required": ["city"]

#   }

# }

Practical Usage: Undeclared Functions in Production

Needle's implicit tool generation enables direct function invocation without decorator overhead:

from needle import Needle

client = Needle()

# No @tool decorator required

response = client.run(
    fn=get_timezone,
    inputs={"city": "Berlin"}
)
print(response)  # → "Europe/Berlin"

The call proceeds identically to an explicitly decorated function. The generated schema serves as the implicit tool, with argument validation performed against the inferred specification.

Comparison: Implicit vs. Explicit Tool Declaration

Understanding both approaches helps developers choose the right level of control:

Approach Implementation Use Case
Implicit (automatic) Plain Python function with type hints Rapid prototyping, simple utilities
Explicit (@tool) Decorated function with Field() metadata Complex validation, enums, custom descriptions

# Explicit declaration for fine-grained control

from needle import tool, Field

@tool
def set_light(room: str, state: str = Field(enum=["on", "off"])):
    """Turn a light on or off."""
    ...

# Stores pre-computed schema in _needle_tool

# Bypasses build_schema entirely

Explicit declaration becomes necessary when you need enumerated values, complex constraints, or custom parameter descriptions beyond what docstring parsing provides.

Testing the Automatic Behavior

The Needle test suite validates both code paths. tests/test_tools.py confirms that functions with and without @tool decorators produce equivalent runtime behavior. tests/test_environments.py demonstrates how environment objects expose TOOLS lists containing mixed declarations—some explicit, some inferred.


# From tests/test_tools.py - both patterns work identically

def test_implicit_schema_generation():
    """Undeclared functions generate valid schemas."""
    schema = build_schema(get_timezone)
    assert schema["name"] == "get_timezone"
    assert "city" in schema["parameters"]["properties"]

Summary

  • No declared tools required — Needle synthesizes schemas automatically via build_schema() in needle/agent/tools.py
  • Runtime introspection — type hints, defaults, and docstrings become JSON-Schema specifications
  • Seamless fallbackneedle/__init__.py checks _needle_tool attribute before invoking automatic generation
  • Identical execution — implicit and explicit tools behave the same during client.run() invocation
  • Zero boilerplate — any callable Python function becomes LLM-accessible without decorators

Frequently Asked Questions

What happens if a function has no type hints?

Needle's build_schema still processes the function, but parameters receive generic object types in the generated schema. Best practice includes type annotations for optimal LLM understanding and validation.

Can I disable automatic schema generation?

No — Needle's design philosophy treats automatic generation as a core guarantee that every callable is LLM-accessible. To override specific behavior, use the @tool decorator with explicit Field() configurations.

Does automatic generation impact performance?

Schema generation occurs once per function at first invocation, then cached. The overhead is negligible for typical applications, as confirmed by the tests/test_tools.py performance benchmarks.

How does Needle handle async functions?

The same build_schema mechanism applies to coroutines. The schema describes the function signature; Needle's execution layer handles the async/await distinction separately during client.run().

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 →