---
audience: developer
summary: Add a new tool the AI can call. End-to-end, including the manifest, the tool
  definition, and the executor.
title: Write a plugin
path: tutorials/developer/write-a-plugin
status: published
---

# Write a plugin

A plugin is a Python class that implements the `BasePlugin` protocol
and lives under `app/plugins/builtin/`. Once registered, it
contributes one or more tools that AIs in your tenant can call.

We'll build `scaiwave.greeter` — a toy plugin with one tool,
`greet`, that says hello in N languages.

## 1. Pick a plugin id

The id is `<owner>.<name>`. Built-in plugins use `scaiwave.*`; if
you're building a third-party plugin use your namespace (`acme.crm`,
`bigco.sso`, etc.). It must be unique per tenant.

## 2. Create the file

`app/plugins/builtin/greeter.py`:

```python
"""Greeter plugin — says hello in N languages."""

import json

from app.plugins.base import BasePlugin
from app.plugins.protocol import (
    PluginContext,
    PluginManifest,
    ToolDefinition,
)


class GreeterPlugin(BasePlugin):
    @property
    def manifest(self) -> PluginManifest:
        return PluginManifest(
            id="scaiwave.greeter",
            name="Greeter",
            version="1.0.0",
            description="Say hello in N languages.",
            tools=[
                ToolDefinition(
                    name="greet",
                    description=(
                        "Say hello in the given language. "
                        "Use when the user explicitly asks for a greeting "
                        "in a specific language, not as small talk."
                    ),
                    usage="[/greet <language>]",
                    parameters={
                        "type": "object",
                        "properties": {
                            "language": {
                                "type": "string",
                                "description": "Language code (en, de, nl, fr, ja, …).",
                            },
                            "name": {
                                "type": "string",
                                "description": "Whom to greet (optional).",
                            },
                        },
                        "required": ["language"],
                    },
                ),
            ],
        )

    async def get_system_context(self, ctx: PluginContext) -> str | None:
        return None  # No system-prompt injection needed for this plugin.

    async def execute_tool(
        self, tool_name: str, args: str, ctx: PluginContext,
    ) -> str:
        if tool_name != "greet":
            return ""
        try:
            parsed = json.loads(args) if isinstance(args, str) else args
        except (ValueError, TypeError):
            parsed = {"language": str(args).strip()}

        language = (parsed.get("language") or "en").lower()
        name = (parsed.get("name") or "world").strip()

        greetings = {
            "en": f"Hello, {name}!",
            "de": f"Hallo, {name}!",
            "nl": f"Hallo, {name}!",
            "fr": f"Bonjour, {name}!",
            "ja": f"こんにちは、{name}さん！",
        }
        return greetings.get(language, greetings["en"])
```

## 3. Register

Add to `app/plugins/builtin/__init__.py`:

```python
from app.plugins.builtin.greeter import GreeterPlugin

BUILTIN_PLUGINS = [
    # … existing entries …
    GreeterPlugin(),
]
```

Restart the server. The plugin is now discoverable.

## 4. Enable it

```bash
curl -X POST "$BASE/v1/admin/plugins/scaiwave.greeter/enable" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

Or via the **Admin → Plugins** UI.

## 5. Test from chat

Send a message to an AI: *"Say hello in German to Alice."* The AI's
function-calling schema now includes `greet`; it should call it
with `{"language": "de", "name": "Alice"}` and respond with the
result.

## What `PluginContext` carries

```python
@dataclass
class PluginContext:
    db: AsyncSession           # tenant-scoped SQLAlchemy session
    tenant: TenantContext      # current tenant
    settings: Settings         # config
    redis: aioredis.Redis      # for caching / coordination
    room_id: str               # current room id
    sender_id: str             # the AI participant making the call
    user_token: str | None     # the user's JWT (for token-exchange)
    scaigrid: ScaiGridClient | None
    arq: ArqRedis | None       # for enqueueing background work
    approval_service: ApprovalService | None
    event_service_factory: Callable | None
```

You can read DB rows (always tenant-scoped), publish events, enqueue
ARQ jobs, etc.

## Permissions

Declare in the manifest:

```python
permissions=[PluginPermission.EXTERNAL_HTTP, PluginPermission.READ_NOTES],
```

The framework checks before each call. Available:

- `READ_MESSAGES`, `WRITE_MESSAGES`
- `READ_NOTES`, `WRITE_NOTES`
- `EXTERNAL_HTTP`
- `SEARCH`
- `READ_USERS`
- `MANAGE_TASKS`

## Side effects

If your tool mutates anything, set `side_effects=True` on the
`ToolDefinition`. In multi-user rooms, the framework can route
side-effect calls through an approval flow — a card with **Approve**
/ **Reject** appears in the chat before execution.

## Fallback hints

When your tool errors, return a result that includes a "Fallback
suggestions" line. The AI's tool-etiquette rule tells it to try a
different angle rather than give up:

```python
return (
    "Greeter failed: language not supported.\n"
    "Fallback suggestions: `web_search` to find a greeting yourself."
)
```

## Testing

Plugins are easy to test in isolation:

```python
# tests/unit/test_greeter_plugin.py
async def test_greet_returns_english_by_default(db, tenant_context):
    plugin = GreeterPlugin()
    ctx = PluginContext(
        db=db, tenant=tenant_context, settings=Settings(),
        redis=None, room_id="r", sender_id="ai",
    )
    out = await plugin.execute_tool("greet", '{"language":"en"}', ctx)
    assert "Hello" in out
```

## Where to go next

- Reference: [Plugin protocol](/docs/scaiwave/reference/plugin-protocol) — the full interface.
- [Concepts: Plugins and tools](/docs/scaiwave/concepts/plugins-and-tools).
