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
ToolsService: This automatically creates the
/discoveryendpoint that Opal uses to learn about your tool@tool decorator: Registers your function as an Opal tool with a name and description
Pydantic BaseModel: Defines the parameters your tool accepts (with type validation)
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
Go to railway.com
Sign up (GitHub auth is easiest)
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
Log into Optimizely
Go to Opal β Tools
Click "Add Tool Registry"
Configure Your Tool
Tool Name: Give it a friendly name (e.g., "Greeting Tool")
Discovery URL: Your Railway URL +
/discoveryExample:
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
Go to Opal β Tools β Registries
Find your tool in the list
Click the
β¦and then the "Sync" buttonWait 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
Navigate to Opal β Agents
Click "Add Agent"
Choose βSpecialized Agentβ
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
INPUTsection, add instructions for how the agent should use your toolTools: 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
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:
Understand the request
Call your
greet_usertool withname="Jason"andstyle="enthusiastic"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:
Verify your Railway app is running (
railway status)Check that your discovery endpoint is accessible
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:
Check your agent's system prompt - does it clearly explain when to use the tool?
Verify the tool is enabled in your agent's configuration
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_authfor tools that need user credentialsConnect to APIs: Integrate with external services (databases, third-party APIs, etc.)
Return Islands: Use
IslandResponsefor interactive UI componentsHandle complex data: Process files, work with structured data, generate reports
The pattern stays the same:
Define parameters (Pydantic)
Implement logic (Python function)
Return results (dictionary/object)
Deploy β Sync β Test
Conclusion
You've now built and deployed your first Optimizely Opal custom tool! The key steps were:
β Set up Python 3.12+ environment
β Install Opal Tools SDK
β Create tool with
@tooldecoratorβ Test locally with discovery and tool endpoints
β Deploy to Railway
β Register tool in Optimizely
β Sync after deployment (don't forget!)
β Configure custom agent
β 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!