Introduction
Optimizely Opal is an AI-powered assistant that helps you work more efficiently across Optimizely One. Whether you're managing content in the CMS, analyzing experiment results in Experimentation, or working with data across different products, Opal lets you ask questions and perform tasks in natural language instead of navigating through multiple interfaces.
What makes Opal really powerful is its ability to use custom tools. Think of tools as functions that Opal can call to perform specific actions - fetching data, updating content, running calculations, or integrating with external systems. When you ask Opal a question, it figures out which tools to use and how to combine them to get you the answer.
In this tutorial, we'll build a fun RPG-themed example: a Guild Master system that manages adventurers. You'll learn how to:
- Set up the Opal Tools SDK in a .NET project
- Create your first tool and register it with Opal
- Build multiple tools that work together
- Add authorization using OptiId
All the code examples are available in the Opal Tools Tutorial repository on GitHub. Each step has its own branch so you can follow along or jump to any section.
What Are Opal Tools?
Opal Tools are essentially API endpoints that Opal can discover and call. When you create a tool, you're defining:
- What it does - a description that helps Opal understand when to use it
- What inputs it needs - parameters with descriptions so Opal knows what values to provide
- What it returns - the data that Opal will use in its response
The magic happens when Opal analyzes your question, picks the right tools, and chains them together to give you exactly what you need.
Prerequisites
Before we start, you'll need:
- .NET 10.0 SDK installed
- An Optimizely Opal account
- Basic knowledge of C# and ASP.NET Core
- Your favorite code editor (Visual Studio, VS Code, or Rider)
Step 1: Your First Tool - Getting Started
Let's start with the basics: creating a single tool that returns information about an adventurer.
Setting Up the Project
First, create a new ASP.NET Core Web API project and add the Opal Tools NuGet package:
<PackageReference Include="Optimizely.Opal.Tools" Version="0.4.0" />Configuring Opal Tools
In your Program.cs, register the Opal Tools service:
using OpalToolsTutorial.Web.RolePlayingGame;
using Optimizely.Opal.Tools;
var builder = WebApplication.CreateBuilder(args);
// Add the Opal Tools service
builder.Services.AddOpalToolService();
// Register sample tools
builder.Services.AddOpalTool<AdventurerTools>();
var app = builder.Build();
// Map the Opal Tools endpoints (creates /discovery and tool-specific endpoints)
app.MapOpalTools();
// Start the app
app.Run();That's it for setup. The AddOpalToolService() registers the necessary services, and MapOpalTools() creates all the endpoints Opal needs to discover and call your tools.
Creating Your First Tool
Now let's create a tool that returns adventurer details:
using System.ComponentModel;
using Optimizely.Opal.Tools;
namespace OpalToolsTutorial.Web.RolePlayingGame;
public class AdventurerTools
{
[OpalTool(Name = "get-adventurer-details")]
[Description("Get's adventurer details based on his name")]
public object GetCharacter(AdventurerParameters parameters)
{
if (parameters.Name == "Bob")
{
return new {
name = "Bob",
race = "Human",
className = "Warrior",
level = 1,
hp = 16,
inventory = new[] { "Sword", "Shield", "Armor" }
};
}
else
{
return new {
error = "Character not found"
};
}
}
}The parameter class defines what inputs your tool needs:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace OpalToolsTutorial.Web.RolePlayingGame;
public class AdventurerParameters
{
[Required]
[Description("Name of the adventurer")]
public string Name { get; set; } = string.Empty;
}A few things to note:
- The
[OpalTool]attribute marks this as a tool and gives it a name - The
[Description]attribute helps Opal understand what the tool does - Parameters use
[Required]and[Description]so Opal knows what to ask for - The method returns an object - Opal will serialize this as JSON
Registering Your Tool in Opal
Once your API is running, you need to tell Opal about it:
- Go to your Opal instance
- Navigate to Tools section
- Add a new tool
- Enter your API's discovery URL:
https://your-api-url/discovery
Opal will automatically discover all your tools from that endpoint. For now, we're skipping the bearer token field - we'll cover authorization in Step 3.
Creating an Opal Agent
Now let's create an agent that can use this tool. You can import the agent configuration from the repository or create it manually:
Agent Configuration:
- Name: The Guild Master
- Description: An agent built to support a process of hiring adventurers for a quest in an RPG game
- Prompt Template:
# Context You're a Guild Master and manage a guild full of adventurers. # Requests User may provide you with a different types of [[request]] for which you will have to select appropriate tool and call it to fulfill a [[request]]. - Parameters:
request(string, required): A request for the guild master
- Enabled Tools: get-adventurer-details
To quickly set this up, you can download the guild-master-agent.json configuration from the repository and import it directly into your Opal instance.
Testing It Out
Now let's see it in action. Ask your agent:
User: Tell me something about Bob
Opal Response: Bob is a Human Warrior with 16 HP. He is level 1 and his inventory includes a Sword, Shield, and Armor.
Behind the scenes, Opal:
- Analyzed your question
- Figured out it needed the "get-adventurer-details" tool
- Called it with
Name: "Bob" - Took the JSON response and formatted it into a natural answer
You can inspect the execution details in your Opal instance by viewing the agent execution logs, which show exactly how Opal processed the request, which tools it called, and what data was returned.
Step 2: Multiple Tools and AI Reasoning
Now let's add some complexity. We'll create a second tool and see how Opal can reason about which tools to use and when.
Adding a New Tool
Let's add a tool that returns all available adventurers. First, we'll restructure our code to use a proper data model:
namespace OpalToolsTutorial.Web.RolePlayingGame;
public record Adventurer
{
public required string Name { get; set; }
public required string Race { get; set; }
public required string Class { get; set; }
public required int Level { get; set; }
public required int HP { get; set; }
public required string[] Inventory { get; set; }
}Now update the AdventurerTools class to use this model and add the new tool:
public class AdventurerTools
{
private static readonly Dictionary<string, Adventurer> Adventurers = new Dictionary<string, Adventurer>
{
{ "Bob", new Adventurer { Name = "Bob", Race = "Human", Class = "Warrior", Level = 1, HP = 16, Inventory = new[] { "Sword", "Shield", "Armor" } } },
{ "Alice", new Adventurer { Name = "Alice", Race = "Elf", Class = "Mage", Level = 1, HP = 10, Inventory = new[] { "Book", "Potion", "Wand" } } },
{ "Charlie", new Adventurer { Name = "Charlie", Race = "Dwarf", Class = "Rogue", Level = 1, HP = 12, Inventory = new[] { "Dagger", "Sword", "Shield" } } },
{ "Dave", new Adventurer { Name = "Dave", Race = "Gnome", Class = "Priest", Level = 1, HP = 8, Inventory = new[] { "Book", "Potion", "Wand" } } },
{ "Eve", new Adventurer { Name = "Eve", Race = "Halfling", Class = "Rogue", Level = 1, HP = 14, Inventory = new[] { "Dagger", "Sword", "Shield" } } },
{ "Frank", new Adventurer { Name = "Frank", Race = "Orc", Class = "Warrior", Level = 1, HP = 18, Inventory = new[] { "Sword", "Shield", "Armor" } } },
{ "George", new Adventurer { Name = "George", Race = "Troll", Class = "Warrior", Level = 1, HP = 20, Inventory = new[] { "Sword", "Shield", "Armor" } } },
{ "Harry", new Adventurer { Name = "Harry", Race = "Human", Class = "Warrior", Level = 1, HP = 16, Inventory = new[] { "Sword", "Shield", "Armor" } } },
{ "Ivy", new Adventurer { Name = "Ivy", Race = "Elf", Class = "Mage", Level = 1, HP = 10, Inventory = new[] { "Book", "Potion", "Wand" } } },
};
[OpalTool(Name = "get-adventurer-details")]
[Description("Get's adventurer details based on his name")]
public object GetAdventurer(AdventurerParameters parameters)
{
if (Adventurers.ContainsKey(parameters.Name))
{
return Adventurers[parameters.Name];
}
else
{
return new {
error = "Adventurer not found"
};
}
}
[OpalTool(Name = "get-all-adventurers")]
[Description("Get's all Adventurers details")]
public object GetAllAdventurers(GetAllAdventurersParameters parameters)
{
return Adventurers.Values.ToList();
}
}The new tool uses an empty parameter class since it doesn't need any inputs:
namespace OpalToolsTutorial.Web.RolePlayingGame;
public class GetAllAdventurersParameters
{
}Updating Your Opal Agent
Update your agent configuration to include the new tool in the "Enabled Tools" list:
- get-adventurer-details
- get-all-adventurers
Testing Advanced Reasoning
Now let's see Opal's reasoning capabilities in action. Try these queries:
User: I'm gathering a party for a quest. Tell me about adventurers I could hire.
Opal Response: Here are the adventurers currently available for hire:
| Name | Class | Race | HP | Level | Inventory |
|---|---|---|---|---|---|
| Bob | Warrior | Human | 16 | 1 | Sword, Shield, Armor |
| Alice | Mage | Elf | 10 | 1 | Book, Potion, Wand |
| Charlie | Rogue | Dwarf | 12 | 1 | Dagger, Sword, Shield |
| Dave | Priest | Gnome | 8 | 1 | Book, Potion, Wand |
| Eve | Rogue | Halfling | 14 | 1 | Dagger, Sword, Shield |
| Frank | Warrior | Orc | 18 | 1 | Sword, Shield, Armor |
| George | Warrior | Troll | 20 | 1 | Sword, Shield, Armor |
| Harry | Warrior | Human | 16 | 1 | Sword, Shield, Armor |
| Ivy | Mage | Elf | 10 | 1 | Book, Potion, Wand |
Opal automatically chose to use the get-all-adventurers tool and formatted the results in a nice table.
Now try something more complex:
User: I'm going to quest that requires some use of magical skills. Recommend me a party of 3 members that will do best on such.
Opal Response: For a quest requiring magical skills, I recommend the following party of 3 members:
- Alice (Mage, Elf)
- Ivy (Mage, Elf)
- Dave (Priest, Gnome)
Source: Guild Adventurer Roster
This is where it gets interesting. Opal:
- Called
get-all-adventurersto see who's available - Analyzed the data to identify characters with magical abilities
- Selected the best 3 for a magic-focused quest
- Formatted a clear recommendation
Check your agent execution logs in Opal to see the full trace of how it processed this complex request. This demonstrates Opal's ability to not just call tools, but to reason about the data they return and provide intelligent responses.
Step 3: Adding Authorization with OptiId
So far, anyone could call our tools. But what if you want to restrict certain actions to authenticated users? This is where Opal's authorization features come in.
Creating a Protected Tool
Let's add a tool for hiring adventurers that requires authentication:
[OpalTool(Name = "hire-adventurers")]
[Description("Hires adventurers and assigns them on a quest")]
[OpalAuthorization("OptiId", "cms", true)]
public object HireAdventurers(HireAdventurersParameters parameters)
{
return "Those adventurers have been hired by " + parameters.GuildClientName +
": " + string.Join(", ", parameters.AdventurerNames);
}The key here is the [OpalAuthorization] attribute:
"OptiId"- specifies we're using OptiId for authentication"cms"- the required scopetrue- indicates the user identity is required
The parameter class includes the user's name, which Opal will automatically provide from the authenticated user's context:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace OpalToolsTutorial.Web.RolePlayingGame;
public class HireAdventurersParameters
{
[Required]
[Description("A list of adventurer names to be hired")]
public List<string> AdventurerNames { get; set; } = new List<string>();
[Required]
[Description("A name of the current Opal user making this request")]
public string GuildClientName { get; set; } = string.Empty;
}How OptiId Authorization Works
When Opal calls an authorized tool, it:
- Checks if the user is authenticated
- Verifies they have the required scope
- Includes the authentication token in the API call
- Passes the user's identity as a parameter
This means your tool receives information about who's making the request, allowing you to implement user-specific logic like tracking who hired which adventurers, enforcing quotas, or applying role-based permissions.
Updating Your Opal Agent
Update your agent configuration to include the new tool:
- get-adventurer-details
- get-all-adventurers
- hire-adventurers
Testing the Authorized Tool
Now when you ask to hire adventurers:
User: I would like to hire Bob, Charlie and George for a quest.
Opal Response: Those adventurers have been hired by Michal Mitas: Bob, Charlie, George.
Notice that Opal automatically filled in GuildClientName with the authenticated user's name (in this case, "Michal Mitas"). You didn't have to specify it in your request - Opal extracted it from the user context and passed it to your tool. You can review the execution logs in your Opal instance to see how the authentication context was included in the tool call.
Wrapping Up
Opal Tools give you a straightforward way to extend Optimizely's AI capabilities with your own custom functionality. The SDK handles all the complexity of discovery, parameter parsing, and integration - you just focus on the business logic.
The key takeaways:
- Tools are simple C# methods marked with attributes
- Opal automatically discovers and understands your tools
- AI reasoning means Opal knows when and how to combine tools
- Authorization is built-in and easy to configure
The full source code for this tutorial is available at https://github.com/michal-mitas/optimizely-opal-tools-tutorial, with each step in its own branch so you can see exactly what changes at each stage.
If you build something cool with Opal Tools, I'd love to hear about it. Feel free to reach out or share your experiences in the Optimizely community.
Happy coding!