Building Your First Optimizely Opal Custom Tool

Optimizely Opal's Custom Tools feature lets you extend the Opal AI agent's capabilities by connecting it to your own services and APIs. In this guide, we'll walk through creating a simple custom tool from scratch, deploying it to the cloud, and integrating it with Opal.

By the end of this tutorial, you'll have a working custom tool that responds to natural language queries in Opal chat. We'll keep it straightforward, no complex integrations, just the essentials you need to get started.

What We're Building

We'll create a simple "greeting" tool that demonstrates the core concepts:

  • Accepting parameters from Opal

  • Processing requests

  • Returning formatted responses

  • Proper tool registration and discovery

Once you understand these basics, you can extend the pattern to build more sophisticated tools.

Prerequisites

  • Python 3.12 or higher (critical - the Opal SDK requires 3.12+)

  • Basic Python knowledge

  • A Railway account (free tier works fine)

  • An Optimizely account with Opal access

Part 1: Setting Up Your Local Environment

Create Your Project Directory

mkdir my-opal-tool
cd my-opal-tool

Set Up a Virtual Environment

Python version matters here. The Opal Tools SDK uses syntax that requires Python 3.12+:

# Create virtual environment with Python 3.12+
python3.12 -m venv venv

# Activate it
source venv/bin/activate  # On Windows: venv\Scripts\activate

Install the Opal Tools SDK

pip install optimizely-opal.opal-tools-sdk uvicorn fastapi

Note: While you install it as optimizely-opal.opal-tools-sdk, you'll import it in code as opal_tools_sdk. Here we also install uvicorn which is the ASGI server needed to run FastAPI applications.

Create Your Requirements File

pip freeze > requirements.txt

Part 2: Building the Python Script

Create a file called main.py:

from opal_tools_sdk import ToolsService, tool
from pydantic import BaseModel, Field
from fastapi import FastAPI

# Initialize FastAPI app
app = FastAPI()

# Initialize Opal Tools Service
tools_service = ToolsService(app)

# Define your tool's parameters
class GreetingParameters(BaseModel):
    name: str = Field(description="The name of the person to greet")
    style: str = Field(default="friendly", description="The style of greeting: friendly, formal, casual, or enthusiastic")

# Create your tool
@tool("greet_user", "Greets a user in a specified style")
async def greet_user(parameters: GreetingParameters):
    """
    A simple greeting tool that demonstrates the basics.
    """
    name = parameters.name
    style = parameters.style
    
    greetings = {
        "friendly": f"Hey {name}! Great to see you! πŸ‘‹",
        "formal": f"Good day, {name}. It is a pleasure to meet you.",
        "casual": f"Yo {name}, what's up?",
        "enthusiastic": f"OMG {name}!!! This is SO exciting!!! πŸŽ‰"
    }
    
    greeting = greetings.get(style, greetings["friendly"])
    
    return {
        "greeting": greeting,
        "style_used": style,
        "timestamp": "2024-11-05"  # In real apps, use datetime
    }

Key Concepts in the Code

  1. ToolsService: This automatically creates the /discovery endpoint that Opal uses to learn about your tool

  2. @tool decorator: Registers your function as an Opal tool with a name and description

  3. Pydantic BaseModel: Defines the parameters your tool accepts (with type validation)

  4. Return value: Can be a dictionary, string, or more complex objects

Part 3: Testing Locally

Before deploying, let's make sure everything works.

Run Your Service

uvicorn main:app --reload --port 8000

Test the Discovery Endpoint

Open your browser to http://localhost:8000/discovery. You should see JSON describing your tool:

{
  "tools": [
    {
      "name": "greet_user",
      "description": "Greets a user in a specified style",
      "parameters": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "style": {
            "type": "string",
            "title": "Style",
            "default": "friendly"
          }
        },
        "required": ["name"]
      }
    }
  ]
}

Test Your Tool Endpoint

Using curl or a tool like Postman:

curl -X POST http://localhost:8000/tools/greet_user \
  -H "Content-Type: application/json" \
  -d '{"name": "Jason", "style": "enthusiastic"}'

You should get back:

{
  "greeting": "OMG Jason!!! This is SO exciting!!! πŸŽ‰",
  "style_used": "enthusiastic",
  "timestamp": "2025-11-05"
}

If both tests pass, you're ready to deploy!

Part 4: Deploying to Railway

Railway makes deployment straightforward. Here's the process:

Create a Railway Account

  1. Go to railway.com

  2. Sign up (GitHub auth is easiest)

  3. The free tier provides everything you need for testing

Install Railway CLI

# macOS/Linux
curl -fsSL https://railway.app/install.sh | sh

# Windows (PowerShell)
iwr https://railway.app/install.ps1 | iex

Initialize Your Project

# Login to Railway
railway login

# Initialize your project
railway init

You'll be prompted to:

  • Create a new project or select existing

  • Choose a name for your project

Configure for Deployment

Railway needs two configuration files to deploy your Python app properly.

1. Create runtime.txt

Create a file called runtime.txt in your project root to specify your Python version:

python-3.12.0

2. Create Procfile

Create a file called Procfile (no file extension) in your project root. Inside the Procfile, add this single line:

web: uvicorn main:app --host 0.0.0.0 --port $PORT

Your project structure should now look like this:

my-opal-tool/
β”œβ”€β”€ main.py
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ runtime.txt       ← Tells Railway which Python version
β”œβ”€β”€ Procfile          ← Tells Railway how to start the app
└── venv/

This tells Railway:

  • Which Python version to use (runtime.txt)

  • How to start your application (Procfile)

  • What dependencies to install (requirements.txt - Railway detects this automatically)

The $PORT environment variable in the Procfile is automatically provided by Railway.

Note: Both files must be named exactly as shown - Procfile with a capital P and runtime.txt in lowercase, both with no extra file extensions.

Deploy

railway up

That's it! Railway will:

  • Detect it's a Python project

  • Install dependencies from requirements.txt

  • Start your service using the Procfile

  • Provide you with a public URL

Get Your Service URL

railway domain

This creates a public domain for your service. Copy this URL - you'll need it for Optimizely.

Your tool is now live at: https://your-project.railway.app

Part 5: Registering Your Tool in Optimizely

Navigate to Opal Settings

  1. Log into Optimizely

  2. Go to Opal β†’ Tools

  3. Click "Add Tool Registry"

Configure Your Tool

  • Tool Name: Give it a friendly name (e.g., "Greeting Tool")

  • Discovery URL: Your Railway URL + /discovery

    • Example: https://my-opal-tool.railway.app/discovery

  • Bearer Token (Optional): None (for this basic example)

Save and Sync

After saving, you'll see a "Sync" that can be found under Actions in the Registries list view. Click it. This is critical - Optimizely queries your /discovery endpoint to learn about your tool's capabilities.

Important: It doesn’t seem like the sync happens automatically so every time you redeploy your tool with changes, you should manually sync in Optimizely for it to pick up the updates.

Part 6: Critical Concept - Tool Sync

This is a common gotcha: Optimizely doesn't seem to automatically detect changes to your tool.

The Sync Workflow

Deploy to Railway β†’ Manual Sync in Optimizely β†’ Changes Live in Opal

When to sync:

  • βœ… After initial deployment

  • βœ… After changing tool parameters

  • βœ… After adding new tools

  • βœ… After modifying descriptions or names

  • βœ… After fixing bugs that affect the tool's interface

You don't need to sync:

  • ❌ For internal logic changes that don't affect the tool's signature

  • ❌ For changes that only affect response content (not structure)

How to Sync

  1. Go to Opal β†’ Tools β†’ Registries

  2. Find your tool in the list

  3. Click the … and then the "Sync" button

  4. Wait for confirmation (usually a few seconds)

Think of sync as "hey Optimizely, go check what my tool can do now."

Part 7: Setting Up Your Custom Agent

Now let's make Opal aware of your tool.

Create a Custom Agent

  1. Navigate to Opal β†’ Agents

  2. Click "Add Agent"

  3. Choose β€œSpecialized Agent”

  4. Configure your agent:

    • Name: Something descriptive (e.g., "Greeting Assistant")

    • Id: This is a unique id for your agent

    • Description: What your agent does

    • Prompt Template: In the INPUT section, add instructions for how the agent should use your tool

    • Tools: Enable your "Greeting Tool"

Example System Prompt

You are a friendly assistant that greets users. When someone asks for a greeting 
or introduces themselves, use the greet_user tool. Ask them what style of greeting 
they'd prefer if they don't specify: friendly, formal, casual, or enthusiastic.

Enable for Chat

  1. In the Tools list, toggle "Enabled in Chat"

Part 8: Testing in Opal Chat

Go to Opal Chat and try it out:

User: "Hi, my name is Jason. Can you greet me enthusiastically?"

Opal will:

  1. Understand the request

  2. Call your greet_user tool with name="Jason" and style="enthusiastic"

  3. Return the greeting in conversation

You should see something like:

OMG Jason!!! This is SO exciting!!! πŸŽ‰


Common Issues and Solutions

Issue: "Module not found" errors

Solution: Make sure you're using Python 3.12+. The SDK uses newer Python syntax.

Issue: Railway deployment fails

Solution: Check that your Procfile is in the root directory and requirements.txt includes all dependencies.

Issue: Optimizely shows "Tool unavailable"

Solution:

  1. Verify your Railway app is running (railway status)

  2. Check that your discovery endpoint is accessible

  3. Click "Sync" in Optimizely

Issue: Changes not appearing in Opal

Solution: Did you sync? Always sync after redeployment.

Issue: Tool not being called by Opal

Solution:

  1. Check your agent's system prompt - does it clearly explain when to use the tool?

  2. Verify the tool is enabled in your agent's configuration

  3. Try being more explicit in your prompt to Opal


Best Practices

1. Descriptive Tool Names and Descriptions

Opal uses these to decide when to call your tool. Be clear and specific:

@tool(
    "get_weather",  # βœ… Clear, action-oriented
    "Retrieves current weather conditions for a specified location"  # βœ… Specific
)

2. Use Pydantic Validation

Let Pydantic handle parameter validation:

class WeatherParams(BaseModel):
    location: str  # Required
    units: str = "fahrenheit"  # Optional with default
    include_forecast: bool = False  # Optional boolean

3. Meaningful Error Responses

Return helpful error messages:

try:
    result = do_something(parameters.value)
    return {"result": result}
except Exception as e:
    return {"error": str(e), "suggestion": "Try using a different value"}

4. Test Locally First

Always test with curl/Postman before deploying. It's much faster to debug locally.

5. Version Your Tools

Consider adding versioning to your tool names if you're planning updates:

@tool("greet_user_v1", "...")

This lets you run multiple versions simultaneously during migrations.


What's Next?

Now that you have the basics down, you can extend your tool:

  • Add authentication: Use @requires_auth for tools that need user credentials

  • Connect to APIs: Integrate with external services (databases, third-party APIs, etc.)

  • Return Islands: Use IslandResponse for interactive UI components

  • Handle complex data: Process files, work with structured data, generate reports

The pattern stays the same:

  1. Define parameters (Pydantic)

  2. Implement logic (Python function)

  3. Return results (dictionary/object)

  4. Deploy β†’ Sync β†’ Test

Conclusion

You've now built and deployed your first Optimizely Opal custom tool! The key steps were:

  1. βœ… Set up Python 3.12+ environment

  2. βœ… Install Opal Tools SDK

  3. βœ… Create tool with @tool decorator

  4. βœ… Test locally with discovery and tool endpoints

  5. βœ… Deploy to Railway

  6. βœ… Register tool in Optimizely

  7. βœ… Sync after deployment (don't forget!)

  8. βœ… Configure custom agent

  9. βœ… Enable and test in chat

The most important lessons:

  • Python 3.12+ is required for the SDK

  • Always sync in Optimizely after redeployment

  • Test locally before deploying to save time

  • Clear descriptions help Opal use your tool correctly

The possibilities from here are endless. You can connect Opal to your data, automate workflows, integrate with company systems, and build sophisticated AI agents that understand your business context.

Happy building!


jason thompson

Jason Thompson is the CEO and co-founder of 33 Sticks, a boutique analytics company focused on helping businesses make human-centered decisions through data. He regularly speaks on topics related to data literacy and ethical analytics practices and is the co-author of the analytics children’s book β€˜A is for Analytics’

https://www.hippieceolife.com/
Next
Next

What Every Optimization Team Needs to Understand About Working With AI Whether They Use It for Code or Not