---
question: How do I write an agent class in a module?
kind: task
via_web: available
via_cli: available
via_python: available
via_api: available
via_config: na
ref_web: Settings › Agents
ref_cli: abi new agent
ref_python: Agent
ref_api: POST /api/agents/
prereq: The CLI and Python steps need a module listed under `modules:` in `config.yaml`. No module yet? See [Creating a Module](/v2/capabilities/build/modules/creating-a-module).
next:
  - /v2/capabilities/build/agents/give-it-tools
  - /v2/capabilities/build/agents/enable-in-a-workspace
  - /v2/capabilities/build/agents/test-an-agent
---

> **You are reading the v2.x docs (engine modules). New projects use v3.** [Read this page in v3](/capabilities/build/agents/creating-an-agent) · [Migrate from 2.x to 3.0](/capabilities/upgrade/migrate-to-3)

# Create an agent

An agent binds a chat model to tools (workflows, integrations) and to other agents. In a module, an agent is a class with a `New()` class method: the name, system prompt, tools and sub-agents are all in one place, and the engine builds it at startup.

A Nexus workspace can also hold agents of its own: a name, a system prompt and a model, stored in the workspace. The Web UI and the REST API create those. They have no tools or sub-agents. For those, write the class with the CLI or Python.

## Web UI

This creates a workspace agent. It does not write a class.

### Open the agent list

In the workspace, open Settings, then Agents.

### Add the agent

Click **Add Agent**. Fill in **Name** (required), **Icon**, **Description** and **System Prompt**, then click **Add Agent**.

![Workspace Settings on Agents: the Add New Agent form](/abi/Screenshot_Add_Agent.png)

*Settings › Agents › Add Agent: name, icon, description and system prompt.*

### Turn it on

The new agent starts off. Check **Enabled** on its row: see [Enable in a workspace](/v2/capabilities/build/agents/enable-in-a-workspace).

### Set its model

Click the row to open **Edit Agent**, fill in **Model ID** with the provider's own model id (for OpenRouter, `google/gemma-4-26b-a4b-it`), then click **Save**. See [Edit prompt & model](/v2/capabilities/build/agents/edit-prompt-and-model).

:::note Which provider answers
A workspace agent answers through the provider stored on its record (`openrouter`, `openai`, `anthropic`, `mistral`, `google` or `xai`), with that provider's key from Settings › Secrets (`OPENROUTER_API_KEY`, ...). The form and **Save** do not store a provider: set `provider` when you create the agent with the REST API. Without one, the chat falls back to a local Ollama server when one runs.
:::

## CLI

### Scaffold the agent

From the project root:

```bash
abi new agent Reviewer src/my_module/agents
```

It writes `ReviewerAgent.py`. The arguments are `<name> [path]`; the path defaults to the current directory. (`abi new module` already scaffolds a first agent in the new module.) The command is [`abi new agent`](/user-guide/reference/cli#abi-new-agent).

### Chat with it from a terminal

```bash
abi chat my_module ReviewerAgent    # <module> <agent class name>
```

`<module>` is the module's entry under `modules:`. `abi chat` loads the engine in the terminal. On ABI 3.x in a NATS project, it needs the broker running first (`abi dev up` or `abi stack start`). The full command list is the [CLI reference](/user-guide/reference/cli).

## Python

### Write the class

The scaffold is a subclass of `Agent` (from `naas_abi_core.services.agent.Agent`) with class attributes and a `New(...)` constructor:

```python title="agents/ReviewerAgent.py"
from __future__ import annotations

from typing import Optional

from naas_abi_core.services.agent.Agent import (
    Agent,
    AgentConfiguration,
    AgentSharedState,
)


class ReviewerAgent(Agent):
    name: str = "Reviewer"
    description: str = "Reviews documents and answers questions about them."
    avatar_url: str = "https://example.com/reviewer.png"
    system_prompt: str = """<role>
You are ReviewerAgent, a careful reviewer.
</role>

<objective>
Help the user review their documents.
</objective>

<tools>
[TOOLS]
</tools>

<constraints>
- Only use the provided tools; do not fabricate data.
</constraints>
"""

    @classmethod
    def New(
        cls,
        agent_shared_state: Optional[AgentSharedState] = None,
        agent_configuration: Optional[AgentConfiguration] = None,
    ) -> "ReviewerAgent":
        from naas_abi_core.engine.context import get_default_model_registry

        # The workspace's default chat model, from the model registry.
        registry = get_default_model_registry()
        assert registry is not None, "ModelRegistryService not initialized"
        chat_model = registry.get_default_chat_model()

        tools: list = []   # workflow and integration tools go here
        agents: list = []  # sub-agents this agent can delegate to

        if agent_configuration is None:
            tools_section = "\n".join(f"- {t.name}: {t.description}" for t in tools)
            agent_configuration = AgentConfiguration(
                system_prompt=cls.system_prompt.replace("[TOOLS]", tools_section)
            )
        if agent_shared_state is None:
            agent_shared_state = AgentSharedState(thread_id="0")

        return cls(
            name=cls.name,
            description=cls.description,
            chat_model=chat_model,
            tools=tools,
            agents=agents,
            memory=None,
            state=agent_shared_state,
            configuration=agent_configuration,
        )
```

The model is the registry's default (`services.model_registry.default_chat_model` in `config.yaml`). For a specific model, ask the registry for its canonical id: `registry.get_chat_model("gemma-4-26b-a4b-it")`. A module that needs its own settings reads them from its configuration: `ABIModule.get_instance().configuration`.

The scaffold also has two optional hooks, `onHumanMessage(message)` and `onAImessage(message, agent_name)`, called on every user message and every AI message (also from sub-agents). They are for observation: what they return is ignored, an exception in them is logged and swallowed, and they run inline, so keep them quick.

### Let the engine load it

The engine imports every `.py` file directly under the module's `agents/` folder (not its subfolders, and not `*test.py`) and calls `New()` on each agent class at startup. A module-level `create_agent()` function in the file is used as `New`. No registration is needed. Make sure the module is listed under `modules:` in `config.yaml`.

:::warning Import inside New()
The engine takes every `Expose` class it finds in an `agents/` file for an agent, imported ones included, and calls its `New()`. Import workflows and pipelines inside `New()`, as below, not at the top of the file.
:::

## REST API

There is no HTTP call that writes the agent class. The Nexus API creates a workspace agent, and the engine API calls an agent it has loaded.

### Create a workspace agent

```http
POST /api/agents/
```

Body: `workspace_id` and `name`, plus optional `description`, `system_prompt`, `model_id`, `provider`, `logo_url` and `enabled` (off by default). Auth: a Nexus user session with access to the workspace.

### Turn it on

```http
PATCH /api/agents/{id}
```

Send `enabled: true`. Its prompt and model are on [Edit prompt & model](/v2/capabilities/build/agents/edit-prompt-and-model).

### Call an existing agent

Every agent the engine loads is exposed at:

```http
POST /agents/{agent_name}/completion
POST /agents/{agent_name}/stream-completion
```

`{agent_name}` is the agent's `name`, with characters other than letters, digits, `_` and `-` replaced by `_` (`Multi Models` becomes `Multi_Models`).

```json title="Request body"
{
  "prompt": "Your question here",
  "thread_id": 1
}
```

Auth: `Authorization: Bearer` with `ABI_API_KEY`. The full process list is the [API](/api).

## Config

Not applicable. `config.yaml` does not define the agent class: it lists the module under `modules:`, and [which agents a workspace shows](/v2/capabilities/build/agents/enable-in-a-workspace?via=config).

## Adding tools from workflows

Workflows expose themselves as tools with `as_tools()`. Instantiate one with its configuration and add its tools:

```python
# ... inside New():
from my_module.workflows.MyWorkflow import MyWorkflow, MyWorkflowConfiguration

workflow = MyWorkflow(MyWorkflowConfiguration())
tools += workflow.as_tools()
```

See [Creating a Workflow](/v2/capabilities/build/agents/creating-a-workflow).

## Adding tools from integrations

By convention an integration module exposes a module-level `as_tools(configuration)` function that returns LangChain tools:

```python
# ... inside New():
from my_module import ABIModule
from my_module.integrations.MyIntegration import MyIntegrationConfiguration, as_tools

api_key = ABIModule.get_instance().configuration.my_api_key
if api_key:
    tools += as_tools(MyIntegrationConfiguration(api_key=api_key))
```

See [Creating an Integration](/v2/capabilities/build/agents/creating-an-integration).

## Using an agent as a tool in another agent

Pass agents in the `agents=[]` list:

```python
# ... inside New():
from naas_abi.agents.OntologyEngineerAgent import OntologyEngineerAgent

ontology_agent = OntologyEngineerAgent.New()

return cls(
    name=cls.name,
    ...
    agents=[ontology_agent],   # this agent can now delegate to OntologyEngineerAgent
)
```

## Persistent memory

Memory is automatic for agents built with `memory=None`. Where the engine keeps it depends on the ABI line:

| ABI line | Where conversations are kept |
|---|---|
| ABI 2.x | In Postgres when `POSTGRES_URL` is set, else in memory. |
| ABI 3.x, project without NATS | In Postgres when `POSTGRES_URL` is set. Otherwise in the Document Service when the engine loads it (`services.document` in `config.yaml`, needs the `naas-abi-core[nats]` extra), else in memory. |
| ABI 3.x, NATS project | In the Document Service. History survives restarts and appears in the Nexus System app under the `naas_abi_core.services.agent` namespace. |

Outside an engine (scripts, tests), set `POSTGRES_URL` for persistent memory:

```bash
POSTGRES_URL=postgresql://abi_user:abi_password@localhost:5432/abi_memory
```

Without it, the agent falls back to in-memory storage (history lost on restart).

On ABI 3.x, memory written to `POSTGRES_URL` moves into the Document Service with `abi agent migrate-memory` (a dry run until you add `--apply`). The command does not exist on ABI 2.x.
