---
question: Can I scaffold an integration in a remote module?
kind: task
via_web: na
via_cli: available
via_python: available
via_api: na
via_config: na
ref_cli: abi new integration
ref_python: Integration
prereq: You need an in-engine module listed under `modules:` in `config.yaml`. `abi new integration` refuses to run inside a remote module. See [Creating a Module](/user-guide/build/modules/creating-a-module).
next:
  - /capabilities/build/integrations/add-credentials
  - /capabilities/build/integrations/use-in-an-agent
  - /capabilities/build/knowledge-graph/creating-a-pipeline
---

# Create an integration

Integrations are the external API adapters in ABI. They are the smallest, most atomic units - they talk to a specific external service and nothing else. They do not depend on pipelines, workflows, or agents.

Integrations are `naas_abi_core` code, so they belong in an [in-engine module](/user-guide/build/modules/creating-a-module#remote-or-in-engine). `abi new integration` refuses to run inside a remote module; there, call the third-party API from plain Python (`httpx`, `requests`) in the module, with credentials read through `self.engine.services.secret`.

## Role in the stack

```text
External API  →  Integration  →  Pipeline (transforms to RDF)
                              →  Workflow (business logic)
                              →  Agent tool (direct invocation by the model)
```

An integration should:
- Handle authentication and HTTP communication.
- Return raw data from the external service.
- Offer LangChain tools through a module-level `as_tools()` function.

An integration must NOT:
- Write to the triple store directly.
- Depend on other ABI modules, or on services beyond what it needs for itself.
- Contain business logic.

## Web UI

Not applicable. Nexus has no screen that writes an integration: it is a Python class in an in-engine module. Use the CLI or Python on this page.

## CLI

### Scaffold the integration

From the project root:

```bash
abi new integration My src/my_module/integrations
```

It writes `MyIntegration.py` with an empty configuration dataclass and an `Integration` subclass to fill in (the name is converted to PascalCase and `Integration` is appended). The arguments are `<name> [path]`; the path defaults to the current directory. The command is [`abi new integration`](/user-guide/reference/cli#abi-new-integration). The full command list is the [CLI reference](/user-guide/reference/cli).

## Python

### Write the class

The scaffold holds an empty configuration and the class. Filled in, it looks like this:

```python title="integrations/MyIntegration.py"
from naas_abi_core.integration.integration import Integration, IntegrationConfiguration
from dataclasses import dataclass
from typing import Any

@dataclass
class MyIntegrationConfiguration(IntegrationConfiguration):
    """Configuration for MyIntegration.

    Attributes:
        api_key: API key for MyService.
        base_url: Base URL for the API (default: https://api.myservice.com).
    """
    api_key: str
    base_url: str = "https://api.myservice.com"

class MyIntegration(Integration):
    __configuration: MyIntegrationConfiguration

    def __init__(self, configuration: MyIntegrationConfiguration):
        super().__init__(configuration)
        self.__configuration = configuration

    def _make_request(
        self,
        endpoint: str,
        method: str = "GET",
        params: dict | None = None,
        json: dict | None = None,
    ) -> dict:
        import requests

        response = requests.request(
            method=method,
            url=f"{self.__configuration.base_url}{endpoint}",
            headers={"Authorization": f"Bearer {self.__configuration.api_key}"},
            params=params,
            json=json,
        )
        response.raise_for_status()
        return response.json()

    def list_items(self, filter: str | None = None) -> list[dict]:
        """List items from MyService.

        Args:
            filter: Optional filter expression.

        Returns:
            List of item dicts from the API.
        """
        params = {"filter": filter} if filter else None
        return self._make_request("/items", params=params)

    def get_item(self, item_id: str) -> dict:
        """Get a single item by ID."""
        return self._make_request(f"/items/{item_id}")
```


### Expose it as tools

By convention, an integration file also defines an `as_tools(configuration)` function at module level (most marketplace integrations do), which an agent calls to get the tools:

```python
def as_tools(configuration: MyIntegrationConfiguration) -> list:
    """Convert MyIntegration into LangChain tools."""
    from langchain_core.tools import StructuredTool
    from pydantic import BaseModel, Field

    integration = MyIntegration(configuration)

    class ListItemsSchema(BaseModel):
        filter: str | None = Field(None, description="Optional filter expression")

    class GetItemSchema(BaseModel):
        item_id: str = Field(..., description="The ID of the item to retrieve")

    return [
        StructuredTool(
            name="list_myservice_items",
            description="List items from MyService. Use when the user asks to see or search items.",
            func=lambda **kwargs: integration.list_items(**kwargs),
            args_schema=ListItemsSchema,
        ),
        StructuredTool(
            name="get_myservice_item",
            description="Get a specific item from MyService by its ID.",
            func=lambda **kwargs: integration.get_item(**kwargs),
            args_schema=GetItemSchema,
        ),
    ]
```

## REST API

Not applicable. There is no REST call that scaffolds an integration, and the API process does not mount integrations as routes. The full process list is the [API](/api).

## Config

Not applicable for the class. Credentials for the external API are on [Add credentials](/user-guide/build/integrations/add-credentials).

## Best practices

- **One integration per external service.** A `GitHubIntegration` for GitHub, a `SalesforceIntegration` for Salesforce.
- **Document all methods.** Agents use tool descriptions to decide when to call them. Vague descriptions lead to wrong invocations.
- **Handle errors gracefully.** Raise descriptive exceptions; don't swallow HTTP errors silently. Raise `IntegrationConnectionError` (from `naas_abi_core.integration`) for transport-level failures, so callers can tell them from logic errors.
- **Guard optional keys.** Check for the API key before adding tools:

```python
# In your agent's New():
from my_module import ABIModule

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

- **Keep credentials in secrets.** Never hardcode API keys. Declare a field on the module's `Configuration` (`my_api_key: str`) and set it in `config.yaml` as `my_api_key: "{{ secret.MY_API_KEY }}"`; the value comes from the secret service (`.env` by default). A module can also read a secret at run time with `self.engine.services.secret.get("MY_API_KEY")`.

---

## Testing

Test the class without the engine or the network: build it with a fake key and patch the method that makes the HTTP call (`_make_request` above). `abi new integration` writes no test file; keep yours next to the class as `MyIntegration_test.py`. The steps are on [Test an integration](/user-guide/build/integrations/test-an-integration).
