Building AI-Powered Tools with Optimizely Opal - A Step-by-Step Guide

Learn how to build and integrate custom tools with Optimizely Opal using the Opal Tools SDK. This tutorial walks through creating tools, handling authorization, and leveraging AI reasoning.

Back

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:

  1. Go to your Opal instance
  2. Navigate to Tools section
  3. Add a new tool
  4. 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:

  1. Analyzed your question
  2. Figured out it needed the "get-adventurer-details" tool
  3. Called it with Name: "Bob"
  4. 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:

NameClassRaceHPLevelInventory
BobWarriorHuman161Sword, Shield, Armor
AliceMageElf101Book, Potion, Wand
CharlieRogueDwarf121Dagger, Sword, Shield
DavePriestGnome81Book, Potion, Wand
EveRogueHalfling141Dagger, Sword, Shield
FrankWarriorOrc181Sword, Shield, Armor
GeorgeWarriorTroll201Sword, Shield, Armor
HarryWarriorHuman161Sword, Shield, Armor
IvyMageElf101Book, 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:

  1. Alice (Mage, Elf)
  2. Ivy (Mage, Elf)
  3. Dave (Priest, Gnome)

Source: Guild Adventurer Roster

This is where it gets interesting. Opal:

  1. Called get-all-adventurers to see who's available
  2. Analyzed the data to identify characters with magical abilities
  3. Selected the best 3 for a magic-focused quest
  4. 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 scope
  • true - 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:

  1. Checks if the user is authenticated
  2. Verifies they have the required scope
  3. Includes the authentication token in the API call
  4. 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:

  1. Tools are simple C# methods marked with attributes
  2. Opal automatically discovers and understands your tools
  3. AI reasoning means Opal knows when and how to combine tools
  4. 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!

Written byMichał Mitas
Published onJanuary 26, 2026