Needle complete() vs run() vs extract(): When to Use Each API

Needle's three core methods—complete(), run(), and extract()—differ primarily in iteration depth, return type, and whether they handle tool-calling automatically. This guide breaks down exactly how each API works under the hood so you can choose the right one for your use case.

Needle is a Python library that bridges language models with native C performance via cactus-compute/needle. Understanding the distinction between these three entry points is essential for building efficient agentic applications. Let's examine each method's behavior, source implementation, and practical applications.

complete(): Single-Shot Completion

The simplest APIcomplete() performs exactly one forward pass through the model and returns the raw JSON response envelope.

How It Works Internally

In needle/__init__.py (lines 103–118), complete() is a thin wrapper around the compiled C library:

def complete(self, prompt: str, **kwargs) -> dict:
    # Serializes prompt, calls needle_complete(), deserializes JSON

    response = self._native.complete(prompt, self._tools, **kwargs)
    return json.loads(response)

No iteration occurs. No tool results are fed back to the model. You get exactly what the model generates in one shot.

When to Use complete()

  • Simple chat-style responses
  • Generation tasks where tools aren't needed
  • Scenarios where you want manual control over any tool-calling logic
from needle import Needle

agent = Needle()
resp = agent.complete("Explain quantum entanglement in one sentence.")
print(resp["text"])  # → "Quantum entanglement is..."

run(): Iterative Tool-Calling Loop

The most powerful APIrun() keeps the conversation going, automatically executing tools and feeding results back until the model produces a final response or hits a step limit.

How It Works Internally

Per needle/__init__.py (lines 19–40), run() builds on complete() with a loop:

def run(self, prompt: str, max_steps: int = 10) -> dict:
    # First completion

    response = self.complete(prompt)
    results = []
    
    for step in range(max_steps):
        calls = response.get("function_calls", [])
        if not calls:
            break  # No more tool calls needed

            
        # Execute each tool, collect results

        for call in calls:
            func = self._functions[call["name"]]
            try:
                result = func(**call["arguments"])
            except Exception as e:
                result = {"error": str(e)}
            results.append(result)
        
        # Feed results back for next completion

        response = self.complete(prompt + json.dumps(results))
    
    response["results"] = results  # Enrich final envelope

    return response

Key Characteristics

  • Multiple steps: Up to max_steps completions (default: 10)
  • Automatic tool execution: Looks up Python functions in self._functions
  • Result enrichment: Adds a "results" field with all tool-call outcomes
  • Error handling: Captures exceptions and feeds them back as results

When to Use run()

  • Multi-step reasoning with external tools
  • Scenarios where the model needs to fetch data, calculate, then reason about results
  • Agentic workflows where you want the engine to manage the conversation loop
def fetch(url: str) -> str:
    import requests
    return requests.get(url).text

agent = Needle(tools=[fetch])
result = agent.run(
    "Fetch the first paragraph of https://example.com and summarise it.",
    max_steps=5
)
print(result["results"])  # List of tool execution results

print(result["text"])     # Final summarised response

extract(): One-Shot Structured Extraction

The convenience APIextract() treats a Pydantic schema as the only available "tool" and returns a parsed Python object directly, eliminating boilerplate.

How It Works Internally

Per needle/__init__.py lines 57–68 and 141–142, extract() creates a temporary agent:

def extract(cls, text: str, schema: Type[BaseModel]) -> Optional[BaseModel]:
    # Create ephemeral agent with schema as sole tool

    agent = cls(tools=[schema])
    response = agent.complete(text)
    
    calls = response.get("function_calls", [])
    if not calls:
        return None
    
    # Parse first function call into schema instance

    args = calls[0]["arguments"]
    return schema(**args)  # Or return raw dict if instantiation fails

Key Characteristics

  • No persistent agent: Creates a temporary Needle instance internally
  • Single tool restriction: Schema is the only available function
  • Direct object return: Returns a Pydantic instance or None, not a JSON envelope
  • No iteration loop: Exactly one completion, like complete()

When to Use extract()

  • Structured data extraction from free-form text (dates, addresses, entities)
  • Quick parsing without managing a full agent lifecycle
  • One-shot transformation of unstructured → structured data
from pydantic import BaseModel

class PersonInfo(BaseModel):
    name: str
    email: str

text = "John Doe's email is john.doe@example.com."
extracted = Needle().extract(text, PersonInfo)
print(extracted)  # PersonInfo(name='John Doe', email='john.doe@example.com')

Summary: Choosing the Right API

Aspect complete() run() extract()
Steps 1 Up to max_steps 1
Tool handling None (unless prompt contains schema) Automatic loop Schema-only
Return type JSON envelope (dict) JSON envelope + "results" list Pydantic object or None
Agent lifecycle Uses existing instance Uses existing instance Creates temporary agent
Best for Simple completions, manual control Multi-step agentic workflows Quick structured extraction

Frequently Asked Questions

Can I use extract() with multiple schemas at once?

No. The extract() design intentionally restricts the tool set to a single schema, as implemented in needle/__init__.py lines 57–68. For multiple extraction targets, create a single Pydantic model with optional fields or use complete()/run() with a broader tool set.

Does run() always execute tools synchronously?

Yes. According to the source in needle/__init__.py lines 19–40, each tool is looked up in self._functions and executed via direct Python invocation. There is no built-in async support in the current implementation—each tool runs to completion before the next complete() call.

Why does extract() return None instead of raising an exception?

This is intentional defensive behavior per lines 57–68. If the model fails to generate a matching function call—due to prompt ambiguity, schema mismatch, or model refusal—extract() returns None rather than crashing. Always check the return value or wrap calls in your own validation logic.

Can I access the raw JSON from extract() if needed?

No. The extract() API deliberately discards the envelope. If you need raw response details (confidence scores, token usage, alternative calls), use complete() directly with your schema as a tool and handle parsing yourself, as shown in the extract() implementation pattern.

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 →