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

> Understand Needle's complete() run() and extract() APIs. Learn their differences in iteration depth return type and tool calling to select the best method for your task.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: deep-dive
- Published: 2026-08-20

---

**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 API**—`complete()` performs exactly one forward pass through the model and returns the raw JSON response envelope.

### How It Works Internally

In [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) (lines 103–118), `complete()` is a thin wrapper around the compiled C library:

```python
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

```python
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 API**—`run()` 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`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) (lines 19–40), `run()` builds on `complete()` with a loop:

```python
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

```python
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 API**—`extract()` 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`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) lines 57–68 and 141–142, `extract()` creates a **temporary agent**:

```python
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

```python
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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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.