# Choosing the right abstraction

> When to use prebuilt agents, the Agent class in a custom graph, or plain function nodes.

Source: https://10xgraph.com/docs/how-to/python/choose-the-right-abstraction
Last updated: 2026-05-24

10xGraph gives you three levels to work at. Each one trades flexibility for convenience.

| | Prebuilt | `Agent` + graph | Custom function node |
|---|---|---|---|
| **Setup** | 5 lines | ~30 lines | As much as you need |
| **LLM call** | Handled | Handled | You write it (optional) |
| **Graph topology** | Fixed | You define it | You define it |
| **Control flow** | Not customizable | Full control | Full control |
| **Custom state fields** | Limited | Yes | Yes |
| **InjectQ services** | Not directly | Not directly | `Inject[T]` on any param |
| **Best for** | Standard patterns, prototypes | Most production agents | Non-LLM steps, custom providers, logic that wraps an Agent |

---

## Prebuilt agents

Prebuilt agents are complete, compiled graphs. One constructor call gives you a runnable `CompiledGraph`, no `StateGraph`, no edges, no `compile()`.

**Available prebuilts:**

| Class | Pattern |
|---|---|
| `ReactAgent` | Single agent with tool use (react loop) |
| `PlanActReflectAgent` | Plan → execute → reflect loop |
| `RAGAgent` | Retrieval-augmented generation |
| `StructuredOutputAgent` | Forces structured JSON output |
| `SupervisorTeamAgent` | Supervisor routes tasks to specialist workers |
| `SwarmAgent` | Peer-to-peer handoff between agents |

```python
from tenxgraph.prebuilt.agent.react import ReactAgent

app = ReactAgent(
    model="gpt-4o",
    system_prompt=[{"role": "system", "content": "You are a helpful assistant."}],
    tools=[get_weather, search_web],
).compile()

result = app.invoke(
    {"messages": [Message.text_message("What is the weather in Paris?")]},
    config={"thread_id": "t1"},
)
```

**Use a prebuilt when:**

- You need a well-known pattern (react loop, RAG, supervisor, swarm) and the default topology fits.
- You are prototyping or building a demo and do not want to wire a graph manually.
- The prebuilt's constructor arguments cover all the configuration you need.

**Do not use a prebuilt when:**

- You need a custom graph topology, extra nodes, non-standard edges, or a node before/after the agent.
- You need to inject custom services (`Inject[T]`) into graph nodes.
- You need to share a `ToolNode` across multiple agents in a single graph.

---

## `Agent` class in a custom graph

`Agent` is a node, not a full graph. You place it inside a `StateGraph` alongside other nodes and wire the edges yourself. This is the most common pattern for production agents.

```python
from tenxgraph.core.graph import StateGraph, Agent, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.utils import END

tool_node = ToolNode([get_weather, search_web])

agent = Agent(
    model="gpt-4o",
    system_prompt=[{"role": "system", "content": "You are a helpful assistant."}],
    tool_node=tool_node,
    retry_config=True,
    trim_context=True,
)

def should_use_tools(state: AgentState) -> str:
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and getattr(last, "tools_calls", None):
        return "TOOL"
    return END

graph = StateGraph()
graph.add_node("MAIN", agent)
graph.add_node("TOOL", tool_node)
graph.set_entry_point("MAIN")
graph.add_conditional_edges("MAIN", should_use_tools, {"TOOL": "TOOL", END: END})
graph.add_edge("TOOL", "MAIN")

app = graph.compile()
```

`Agent` owns the LLM call. It handles message conversion, tool call detection, retries, context trimming, reasoning config, and multimodal output through constructor arguments.

**Use `Agent` in a graph when:**

- The node's job is to call an LLM on a standard provider (OpenAI, Google, or any OpenAI-compatible API).
- You need built-in tool call detection, retry logic, fallback models, or reasoning config.
- You want to add custom nodes around the LLM call (pre-processing, post-processing, routing), something a prebuilt cannot do.
- You have multiple agents in one graph (supervisor, swarm, pipeline).

**Do not use `Agent` when:**

- The node does not call an LLM (use a custom function node instead).
- You are calling an unsupported LLM provider or need full control over the prompt (use a custom function node + `ModelResponseConverter`).

---

## Custom function node

A plain Python function, sync or async, registered as a graph node. The framework auto-injects `state` and `config` by name; everything else comes via `Inject[T]`.

```python
from injectq import Inject
from tenxgraph.core.state import AgentState, Message
from tenxgraph.storage.store import BaseStore

async def load_profile(
    state: AgentState,
    config: dict,
    store: BaseStore = Inject[BaseStore],
) -> dict:
    profile = await store.aget(
        namespace=("profiles", config["user_id"]),
        key="data",
    )
    return {"profile": profile.value if profile else {}}
```

Return types: `str`, `Message`, `list[Message]`, `AgentState`, or `Command`.

**Use a custom function node when:**

- The node does not need an LLM, loading data, logging, routing, calling an external API, running a calculation.
- You need to run something before or after an `Agent` node in the graph.
- You are calling a custom or unsupported LLM provider, call it yourself and return a `str`, `Message`, or `ModelResponseConverter`.
- The routing logic is dynamic and depends on side effects inside the node, return `Command`.
- You need direct access to framework services (checkpointer, store, publisher) via `Inject[T]`.

---

## Decision guide

```
Do you need a standard pattern (react loop, RAG, supervisor, swarm)?
│
├── Yes, and the default topology is enough  →  Prebuilt
│
└── No, or you need to customize the graph
    │
    ├── Does the node call an LLM on OpenAI / Google / compatible API?
    │   │
    │   ├── Yes  →  Agent class in a custom graph
    │   │
    │   └── No (or exotic provider / full prompt control)  →  Custom function node
    │
    └── Do you need non-LLM nodes (pre-processing, logging, routing)?
        └── Yes  →  Custom function nodes alongside Agent nodes in a graph
```

---

## Mixing all three

Prebuilts, `Agent`, and custom function nodes are fully composable. A realistic production graph often looks like this:

```python
from tenxgraph.prebuilt.agent.react import ReactAgent
from tenxgraph.core.graph import StateGraph, Agent, ToolNode
from tenxgraph.core.state import AgentState
from tenxgraph.utils import END
from injectq import Inject
from tenxgraph.storage.store import BaseStore

# Custom node: enriches state before the agent runs
async def load_profile(state: AgentState, config: dict, store: BaseStore = Inject[BaseStore]) -> dict:
    ...

# Agent node: handles the LLM call
agent = Agent(model="gpt-4o", tool_node=ToolNode([get_weather]))

# Custom node: logs after the agent responds
def audit(state: AgentState, config: dict) -> dict:
    print(f"[{config['thread_id']}] {len(state.context)} turns")
    return {}

graph = StateGraph()
graph.add_node("LOAD", load_profile)
graph.add_node("MAIN", agent)
graph.add_node("AUDIT", audit)

graph.set_entry_point("LOAD")
graph.add_edge("LOAD", "MAIN")
graph.add_edge("MAIN", "AUDIT")
graph.add_edge("AUDIT", END)

app = graph.compile()
```

---

## Related docs

- [Prebuilt agents](/docs/how-to/python/use-prebuilt-agents)
- [Configure Agent](/docs/how-to/python/configure-agent)
- [Custom nodes](/docs/how-to/python/use-custom-nodes)
- [Build a graph](/docs/how-to/python/build-a-graph)
