Part 2 – Extending Optimizely Opal with Custom Tools and Agents

In Part 1 we looked at Opal’s role as an AI workflow manager in the Optimizely stack. Now let’s go deeper and see how you can extend Opal with your own code. Opal isn’t just a chat interface — it can call out to custom HTTP endpoints (“tools”) and incorporate the results directly into multi-step workflows.

This post shows how to:

  • Build a custom Opal tool in TypeScript (and .NET for comparison)
  • Expose it to Opal via a discovery endpoint
  • Call Optimizely APIs (ODP or Feature Experimentation) inside the tool
  • Wire it into an agent that runs inside a workflow

How Tools Work in Opal

Opal discovers your tool via a discovery endpoint (/opal/discovery) that describes available functions. At runtime, Opal will POST JSON to your execution endpoint, and your tool returns structured JSON back.

This model makes it easy to bolt on custom logic. Your tool can call:

  • Optimizely Data Platform (ODP) for customer or product data
  • Feature Experimentation APIs to create or read experiments
  • Internal APIs like PIMs, DAMs, or analytics services

As long as you return JSON that matches your declared schema, Opal can use it inside an agent or workflow.


Example: Catalog Lookup Tool (TypeScript/Express)

Let’s build a minimal tool that fetches product details by SKU.

// server.ts
import express from "express";
import { z } from "zod";

const app = express();
app.use(express.json());

// Discovery endpoint
app.get("/opal/discovery", (_req, res) => {
  res.json({
    toolId: "com.example.catalog",
    name: "Catalog Lookup Tool",
    description: "Lookup product details by SKU",
    endpoints: [
      {
        id: "catalog-lookup",
        method: "POST",
        path: "/tools/catalog-lookup",
        inputSchema: {
          type: "object",
          properties: { sku: { type: "string" } },
          required: ["sku"]
        },
        outputSchema: {
          type: "object",
          properties: {
            sku: { type: "string" },
            title: { type: "string" },
            price: { type: "number" },
            availability: { type: "string" }
          },
          required: ["sku", "title"]
        }
      }
    ]
  });
});

// Execution endpoint
app.post("/tools/catalog-lookup", (req, res) => {
  const Input = z.object({ sku: z.string().min(1) });
  const parsed = Input.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: parsed.error });

  const { sku } = parsed.data;

  // TODO: replace with ODP or Commerce API call
  res.json({
    sku,
    title: "Contoso Road Helmet",
    price: 129.99,
    availability: "in_stock"
  });
});

app.listen(3000, () => console.log("Opal tool listening on :3000"));


Opal will now be able to discover and call this function.


.NET Minimal API Version

Here’s the same concept in .NET 8:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/opal/discovery", () => Results.Json(new {
    toolId = "com.example.catalog",
    name = "Catalog Lookup Tool",
    endpoints = new [] {
        new {
            id = "catalog-lookup",
            method = "POST",
            path = "/tools/catalog-lookup",
            inputSchema = new {
                type = "object",
                properties = new { sku = new { type = "string" } },
                required = new [] { "sku" }
            },
            outputSchema = new {
                type = "object",
                properties = new {
                    sku = new { type = "string" },
                    title = new { type = "string" },
                    price = new { type = "number" },
                    availability = new { type = "string" }
                },
                required = new [] { "sku", "title" }
            }
        }
    }
}));

app.MapPost("/tools/catalog-lookup", (CatalogInput input) =>
{
    if (string.IsNullOrWhiteSpace(input?.sku))
        return Results.BadRequest(new { error = "sku required" });

    return Results.Json(new {
        sku = input.sku,
        title = "Contoso Road Helmet",
        price = 129.99,
        availability = "in_stock"
    });
});

app.Run();

record CatalogInput(string sku);

Hooking the Tool into Opal

  1. Deploy your tool (e.g., Azure App Service, container, or internal API gateway).
  2. In Opal, create a Specialized Agent and attach your tool as an available action.
  3. Write clear agent instructions, e.g.:Agent name: Catalog QA Agent
    Purpose: Validate marketing copy against live catalog data.
    Tools allowed: com.example.catalog.catalog-lookup
    Guidance: Always call the tool with the SKU to verify product name and availability.
  4. Drop the agent into a workflow step in Opal’s builder (e.g., after content generation, before governance checks).

Calling Optimizely APIs Inside Your Tool

Your tool doesn’t need to be a toy—it can call Optimizely APIs to pull or push data:

ODP example (TS):

const odpResp = await fetch("https://api.zaius.com/v3/products?sku=" + sku, {
  headers: { Authorization: `Bearer ${process.env.ODP_API_TOKEN}` }
});
const product = await odpResp.json();

Feature Experimentation example (TS):

await fetch("https://api.optimizely.com/v2/flags", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.OPTLY_TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ key: "new_headline_test", /* ... */ })
});

With these APIs, your agent could:

  • Check product availability before campaigns launch
  • Auto-create feature flags or experiments
  • Personalize copy based on customer segments

Operational Notes

  • Validate inputs/outputs (Zod, FluentValidation) for predictable schemas
  • Use bearer tokens via Opal secrets, not hard-coded keys
  • Log requests with Opal’s requestId for observability
  • Keep latency low; workflows may branch or parallelize steps
  • Version endpoints; breaking changes should get new IDs

Conclusion

Opal isn’t limited to the built-in agents Optimizely provides. By exposing your own endpoints as custom tools, you can extend workflows with logic that ties directly into your data and APIs. In this article we walked through how to:

  • Build a simple tool in TypeScript or .NET with a discovery endpoint and a JSON execution endpoint
  • Return structured, predictable data so Opal agents can consume results reliably
  • Register the tool with Opal and wire it into a specialized agent for use in workflows
  • Call real Optimizely services like ODP and Feature Experimentation to validate data, create experiments, or personalize outputs
  • Apply best practices for security, validation, observability, and versioning

The end result is that Opal becomes more than a workflow orchestrator — it becomes a way to embed your own system intelligence into AI-driven processes. For developers and architects, this provides a practical and governed path to connect enterprise systems with agentic AI.