Take the community feedback survey now.

Mark Stott
Dec 3, 2025
  90
(6 votes)

Creating Opal Tools Using The C# SDK

Over the last few months, my colleagues at Netcel and I have partaken in two different challenge events organised by Optimizely and centered around Opal. Opal is Optimizely’s agentic marketing AI, deeply integrated across the Optimizely One platform. What transforms Opal from a conversational assistant into a true content-orchestration engine is its use of Agents, Workflows and Opal Tools. Opal tools are APIs that allow Opal to perform functional tasks that would otherwise fall outside the AI’s native capabilities. Throughout August, our teams were looking at building Opal Tools as part of the Optimizely Opal Hackathon. Throughout November we took part in the Opal Innovation Challenge which saw us leveraging new capabilities using Opal Tools, Specialized Agents, Instructions and Workflows.

Optimizely currently provides SDKs in three languages for building Opal Tools: C#, JavaScript, and Python. For C#, Optimizely offers two separate SDKs: OptimizelyOpal.OpalToolsSDK and Optimizely.Opal.Tools. At the time of writing, the documentation and training materials primarily reference OptimizelyOpal.OpalToolsSDK, which only reached version 0.1.0 in May 2025. In contrast, Optimizely.Opal.Tools appears to be receiving more active development, with version 0.4.0 being released in September 2025.

Both C# SDKs can deliver Opal tools, we used OptimizelyOpal.OpalToolsSDK as part of the Hackathon and Optimizely.Opal.Tools as part of the innovation challenge. There are some differences in terms of capabilities for both SDKs which are as follows:

Feature OptimizelyOpal.OpalToolsSDK Optimizely.Opal.Tools
Automatic Discovery API
Tools API
Supports Tool Authentication
Supports Dependency Injection
Supports Island Functionality

 

While there isn’t a lot between them and both are pre-version 1.x, Optimizely.Opal.Tools is the stronger and more flexible offering.

Getting Started

This guide assumes you are familiar with C# and creating basic web applications. In your IDE of choice, create a new C# Web API project and install the Optimizely.Opal.Tools nuget package

Creating a Tool

In this example, we’ll create a simple “Hello World” tool. Every Opal Tool must have a parameters class, which can contain zero or more properties. Start by creating a new class with a single property named FirstName. If you decorate the property with the [Required] attribute, Opal will know that this parameter must always be provided. You should also add a [Description] attribute to help Opal understand the purpose of the property. Clear descriptions improve Opal’s ability to supply correct values.

public class HelloWorldParameters
{
    [Required]
    [Description("The first name of the person to greet.")]
    public string FirstName { get; set; } = string.Empty;
}

Next, create a class that serves as the entry point for your tool. Add a public method that returns either an object or Task<object> if your code can run asynchronously. Decorate the method with the [OpalTool] attribute to declare the tool’s name (using snake_case or kebab-case). Decorate the method with the [Description] attribute to explain when and how Opal should use it. Whatever you return must be serializable to JSON.

public class HelloWorldTools(IHelloWorldService service)
{
    [OpalTool("say-hello-world")]
    [Description("Says hello based on a provided first name")]
    public object HelloWorld(HelloWorldParameters parameters)
    {
        return new
        {
            Message = service.SayHello(parameters.FirstName)
        };
    }
}

Here we’re using dependency injection to provide an IHelloWorldService. This is supported only in the Optimizely.Opal.Tools SDK. You could also return a concrete type, but an anonymous object keeps the example simple.

Now set up your tools in Program.cs or Startup.cs. The AddOpalToolService() extension registers the tool infrastructure and must be called before registering any tools. Then call AddOpalTool<ToolClass>() for each tool class. Finally, use MapOpalTools() to make the endpoints available in the request pipeline:

var builder = WebApplication.CreateBuilder(args);

// Register tool dependencies:
builder.Services.AddScoped<IHelloWorldService, HelloWorldService>();

// Register Opal Tool Service and Tools
builder.Services.AddOpalToolService();
builder.Services.AddOpalTool<HelloWorldTools>();

var app = builder.Build();

// Add the Opal tools into the HTTP request pipeline
app.MapOpalTools();

app.Run();

Testing Your Tools

Because Opal Tools are invoked by Opal itself, the only way to test them locally is through API testing tools such as Postman or HTTP files. HTTP files allow you to version-control your test requests and run them directly from your IDE. Requirements for using HTTP files are:

Here is an example HTTP file for testing both the discovery endpoint and the tool endpoint. Your IDE will render a “Send Request” link above each request:

### Variables for all tools
@Tools_HostAddress = https://localhost:8000
@token = dev-token-value
###

### 1. Tool Discovery Endpoint
GET /discovery
Accept: application/json
###

### 2. Say Hello

POST /tools/say-hello-world
Accept: application/json
Content-Type: application/json
Authorization: Bearer 

{
  "parameters": {
    "FirstName": "Mark"
  }
}

###

Executing the discovery endpoint from the HTTP file should provide the following response:

{
  "functions": [
    {
      "name": "say-hello-world",
      "description": "Says hello based on a provided first name",
      "parameters": [
        {
          "name": "FirstName",
          "type": "string",
          "description": "The first name of the person to greet.",
          "required": false
        }
      ],
      "endpoint": "/tools/say-hello-world",
      "auth_requirements": [],
      "http_method": "POST"
    }
  ]
}

Executing the tool endpoint should produce the following response:

{
  "message": "Hello Mark"
}

You’ll notice from the HTTP file example that the tool endpoint uses an Authorization header while the discovery endpoint does not. This is because the discovery endpoint must allow anonymous access. Tool endpoints can optionally require a bearer token. Note that this token must be static and shared by all tools within the same application. This is because you can only provide a single static value when registering your tools application in Opal.

Because the SDK controls the mapping of tool endpoints, you cannot apply individual [Authorize] attributes to them. If you want to require bearer-token authentication then you’ll need to implement custom middleware. That middleware should intercept requests whose path begins with /tools/ and validate that a bearer token is present (and valid) before allowing the request to proceed:

public sealed class ToolAuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public ToolAuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Check if the request is for a tool endpoint
        if (context.Request.Path.StartsWithSegments("/tools"))
        {
            // Validate that the Authorization header is present and valid...
            if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader) || !IsValidToken(authHeader))
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                await context.Response.WriteAsync("Unauthorized");
                return;
            }
        }

        await _next(context);
    }

    private bool IsValidToken(string authHeader)
    {
        // Validate the bearer token value here...
    }
}

Adding Authentication to a Tool

The bearer token discussed earlier is used solely to secure communication between Opal and your tool’s endpoint. It does not represent the identity of the Opal user. There may be scenarios where your tool needs to operate in the context of the authenticated user who initiated the request. To support this, Optimizely Opal can pass user authentication data to Opal Tools.

At the time of writing, the only supported authentication provider is “OptiId”. If your discovery endpoint includes a tool that references an unsupported provider, Opal will fail to process the discovery response. During initial tool registration, Opal provides feedback indicating that the provider is unsupported. If the tool has already been registered and you are syncing updates, Opal will return a success response but silently skip the update; something that can be quite misleading.

Let’s create a new tool that requires user authentication. To do this, you must apply the [OpalAuthorization] attribute to the method, specifying the provider (“OptiId”), the scope and whether authentication is mandatory. The method signature should include both a custom parameters object (like the previous example) and an instance of OpalToolContext, which contains the user’s authentication data and additional request context.

[OpalTool("test-auth")]
[Description("A tool to test auth")]
[OpalAuthorization("OptiId", "cms", true)]
public object TestAuthorization(AuthenticationTestParameters parameters, OpalToolContext context)
{
    return new
    {
        Provider = context?.AuthorizationData?.Provider,
        Details = context?.AuthorizationData?.Credentials,
        EmailAddress = parameters?.EmailAddress
    };
}

public class AuthenticationTestParameters
{
    [Description("The email address of the current opal user making this request.")]
    [Required]
    public string EmailAddress { get; set; } = string.Empty;
}

When inspecting the discovery endpoint for this tool, you’ll notice that the auth_requirements property is an array. This is intentional as Optimizely intends for Opal to support multiple authentication schemes for a single tool. If you want a tool to accept more than one provider, simply apply multiple [OpalAuthorization] attributes to the method.

{
  "functions": [
    {
      "name": "test-auth",
      "description": "A tool to test auth",
      "parameters": [
        {
          "name": "EmailAddress",
          "type": "string",
          "description": "The email address of the current opal user making this request.",
          "required": true
        }
      ],
      "endpoint": "/tools/test-auth",
      "auth_requirements": [
        {
          "provider": "OptiId",
          "scope_bundle": "cms",
          "required": true
        }
      ],
      "http_method": "POST"
    }
  ]
}

When Opal executes this tool, it sends the request body (shown below) to your endpoint. Notice that we decorated AuthenticationTestParameters.EmailAddress with a [Description] attribute of “The email address of the current Opal user making this request.”. Opal understands this description and will populate parameters.EmailAddress automatically based on the current user without any intervention being needed. This really highlights the ability for Opal to infer, supply, and validate contextual user data without intervention from the user or the tool. It can also give you a means to feed back to the user asynchronously using email.

{
  "parameters": {
    "EmailAddress": "jo.bloggs@example.com"
  },
  "auth": {
    "provider": "OptiID",
    "credentials": {
      "token_type": "Bearer",
      "access_token": "<access-token-1446-chars>",
      "org_sso_id": "<id>",
      "cfg_client_id": "",
      "user_id": "<guid-no-hyphens>",
      "instance_id": "<guid-no-hyphens>",
      "customer_id": "<guid-no-hyphens>",
      "product_sku": "OPAL"
    }
  },
  "environment": {
    "execution_mode": "interactive"
  },
  "chat_metadata": {
    "thread_id": "<guid>"
  }
}

Summary

In this article, we explored how to build Opal Tools with the Optimizely.Opal.Tools SDK, define parameters, handle authentication, and secure tool endpoints. You should now have a clear understanding of how Opal discovers, registers, and executes tools, as well as how user and bearer authentication data flow into your tool methods. With these fundamentals, you’re ready to create secure, functional tools that extend Opal’s capabilities.

Key Points:

  • Choose Optimizely.Opal.Tools over OptimizelyOpal.OpalToolsSDK - The newer SDK is more mature and feature rich.
  • All Opal Tool methods require a parameters class - If your tool takes no parameters, you must define an empty parameters class.
  • Good descriptions are critical for Opal’s understanding - Use [Description] attributes effectively to get the best results.
  • Discovery endpoints must allow anonymous access, tool endpoints should be secured with a bearer token.
  • Use [OpalAuthorization] and OpalToolContext for Opal Tools which require an authorized user context.
  • Use HTTP files for local testing of your discovery and tool endpoints.

I am an OMVP and the author and maintainer of Stott Security and Stott Robots Handler for Optimizely CMS 12. You can find all of my content collated on https://www.stott.pro/ 

Dec 03, 2025

Comments

Please login to comment.
Latest blogs
Building a Lightweight Optimizely SaaS CMS Solution with 11ty

Modern web development often requires striking a difficult balance between site performance and the flexibility needed by content editors. To addre...

Minesh Shah (Netcel) | Dec 3, 2025

Introducing the OMVP Strategy Roundtable: Our First Episode Is Live

One of our biggest priorities this year was strengthening the strategic voice within the OMVP community. While the group has always been rich with...

Satata Satez | Dec 1, 2025

Optimizely CMS - Learning by Doing: EP08 - Integrating UI : Demo

  Episode 8  is Live!! The latest installment of my  Learning by Doing: Build Series  on  Optimizely CMS 12  is now available on YouTube! This vide...

Ratish | Dec 1, 2025 |