A complete Opal agent implementation for a cultural institution client, demonstrating three levels of platform capability: specialized agents, external tool integrations with a live public API, and multi-step workflow orchestration.
See it in action: output/demo-run.md — full E2E output from all three agents.
cd tools
npm install
cp .env.example .env # add your ANTHROPIC_API_KEY
# Start the tool service
npm run dev &
# Run the full content pipeline (writer → reviewer)
npm run harness -- run ../agents/whitney-exhibition-content-workflow.json \
--agents-dir ../agents \
-p exhibition_title="Biennial 2025" \
-p artist_names="Various" \
-p exhibition_description="The Whitney Biennial returns" \
-p opening_date="March 2025" -vThe Whitney Museum of American Art is a major NYC institution focused on contemporary American art. Their digital team uses Optimizely for:
- Exhibition landing pages — high-traffic pages that spike at exhibition launches and closings
- Membership & ticketing funnels — conversion-optimized flows with A/B testing
- Multi-channel content — coordinated messaging across web, email, social, and in-gallery signage
These agents address real operational needs: analyzing experiment results, generating on-brand content at scale, and enforcing quality standards before publication.
A key differentiator of this implementation: it connects to real data, not mocked inputs.
The Whitney exposes a fully public, unauthenticated REST API returning JSON. No API key required.
| Endpoint | Returns | Used By |
|---|---|---|
/api/exhibitions |
Exhibition titles, dates, curatorial descriptions, URLs | get_exhibitions tool |
/api/artworks |
26,995 works — metadata, images, alt text, visual descriptions | search_collection tool |
/api/artists |
4,000+ artists with biographical data | search_collection tool |
/api/events |
Events from 2008 onward | Available for future agents |
/api/guides |
Audio guides with stops | Available for future agents |
Query syntax: Ransack-style — q[field_matcher]=value. Supports cont (contains), eq (equals), gt/lt (greater/less than), true/false booleans. Sorting via q[s]=field+asc|desc.
API docs: https://whitney.org/about/website/api
Nightly-updated CSV datasets under CC0 public domain license:
artists.csvandartworks.csv- Repository: https://github.com/whitneymuseum/open-access
The API includes structured accessibility fields per artwork:
alt_text— human-written alternative textai_alt_text— AI-generated alternative textvisual_description— detailed verbal description for blind/low-vision visitorsaudio_description— exhibition-level audio descriptions
This is not common — Whitney has invested in accessibility at the data model level, which makes it possible for our tools to surface and validate accessibility content programmatically.
No published style guide exists. Voice rules in our check_brand_voice tool are derived from:
- Actual exhibition wall text patterns (see Whitney's "Words on Walls" article)
- Website copy tone: inviting, action-oriented ("Explore", "Dive Into"), avoids academic jargon
- Social media tone: conversational on Threads/Instagram, more formal on web
- DEI language practices observed in collection descriptions and accessibility pages
- Visual identity by Experimental Jetset — Neue Haas Grotesk typeface, "responsive W" mark
Type: Specialized Agent | Creativity: 0.2 | Inference: complex | Tools: none
A standalone agent that analyzes A/B test results from Optimizely experiments. Paste in test data, and it returns a structured report with statistical assessment, experimentation pitfall checks (peeking, novelty effects, Simpson's paradox), museum-specific context (traffic spikes, seasonal patterns), and a clear verdict: Ship, Extend, Kill, or Investigate.
Key design decisions:
creativity: 0.2— statistical analysis needs precision, not creativityenabled_tools: []— pure analysis on provided data, no external calls needed- Museum-specific pitfall checks (exhibition launch spikes, member preview events, seasonal tourism) that a generic analytics agent would miss
Matching instruction template: instructions/CLEAR - Exhibition AB Test Analysis.txt
Type: Workflow Agent (2 steps)
An end-to-end content workflow that takes a curatorial brief and produces publication-ready exhibition content:
[Curatorial Brief] → Content Writer → Content Reviewer → [Approved Package or Revision List]
Step 1 — Content Writer (whitney-content-writer.json)
- Creativity: 0.7 — content generation benefits from higher temperature
- Takes exhibition details (title, artist, description, dates, location)
- Generates 4 content types: gallery wall text, website landing page, email announcement, 3 social media variants
- Enforces Whitney voice guidelines directly in the prompt
Step 2 — Content Reviewer (whitney-content-reviewer.json)
- Creativity: 0.2 — review is analytical, not generative
- Calls external tools (
check_brand_voice,check_accessibility) on each content section - Produces a consolidated review: pass/fail per section with specific revision list
- Content is APPROVED only if ALL sections pass both checks
Type: External Tool Service (Hono + Cloudflare Workers)
A TypeScript service deployed as a Cloudflare Worker, exposing four tools via /discovery and /registry endpoints:
Fetches real exhibition data from whitney.org/api/exhibitions. Filters by status (current, upcoming, past) and keyword search. Returns titles, dates, URLs, and curatorial descriptions. This enables agents to pull real data instead of requiring manual input.
Searches the Whitney's 26,995-work collection. Filters by artist name, classification (Paintings, Sculpture, Video, etc.), keyword, and on-view status. Returns artwork metadata with accessibility fields (alt text, visual descriptions). Enables content agents to reference real collection data.
Evaluates text against 5 Whitney-derived voice rules:
| Rule | What it checks |
|---|---|
| WBV-001 | Institutional tone (no casual slang or superlatives) |
| WBV-002 | Accessible art language (flags academic jargon) |
| WBV-003 | Inclusive language (DEI-aligned terminology) |
| WBV-004 | Active voice preference |
| WBV-005 | Consistent Whitney terminology ("exhibition" not "exhibit") |
Sensitivity adjusts by content type — social posts allow slightly more casual tone than wall text.
Evaluates content readability and accessibility:
| Check | What it flags |
|---|---|
| ACC-001 | Flesch-Kincaid reading level vs target (default: grade 8) |
| ACC-002 | Sentences exceeding 30 words |
| ACC-003 | Paragraphs exceeding 150 words without breaks |
| ACC-004 | Abbreviations used without definition |
| ACC-005 | Image references without descriptive language |
Setup:
cd tools
npm install
cp .env.example .env # fill in ANTHROPIC_API_KEY (+ optionally Cloudflare creds)
# Start tool service locally
npm run dev
# http://localhost:8787/discovery
# Run an agent via the local test harness (requires ANTHROPIC_API_KEY in .env)
npm run harness -- run ../agents/whitney-content-writer.json \
-p exhibition_title="Biennial 2025" \
-p artist_names="Various" \
-p exhibition_description="The Whitney Biennial returns" \
-p opening_date="March 2025" -v
# Deploy to Cloudflare (after `npx wrangler login`)
npm run deploy ┌──────────────────────────────────────────────────────────────────────┐
│ EXTERNAL DATA SOURCES │
│ │
│ whitney.org/api/exhibitions ──┐ │
│ whitney.org/api/artworks ─────┤── Public REST API (no auth) │
│ whitney.org/api/artists ──────┘ │
│ │
│ github.com/whitneymuseum/open-access ── CC0 CSV (nightly update) │
└──────────────────┬───────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ TOOL SERVICE (Hono + Cloudflare Workers) │
│ tools/src/index.ts │
│ Deployed to: Cloudflare Workers │
│ Discovery: /discovery | /registry │
│ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ get_exhibitions │ │ search_collection │ ← Live API wrappers │
│ │ (current/upcoming │ │ (artist, type, │ │
│ │ past, search) │ │ keyword, on-view) │ │
│ └──────────────────┘ └────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ check_brand_voice│ │ check_accessibility│ ← Content analysis │
│ │ (5 voice rules, │ │ (Flesch-Kincaid, │ │
│ │ scored output) │ │ 5 checks) │ │
│ └──────────────────┘ └────────────────────┘ │
└──────────────────┬───────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ OPAL PLATFORM │
│ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│ WORKFLOW: Exhibition Content Pipeline │
│ │ (whitney-exhibition-content-workflow.json) │ │
│ │
│ │ Trigger ──→ ┌─────────────────────┐ │ │
│ (message) │ Content Writer │ creativity: 0.7 │
│ │ │ Wall text, web copy,│ inference: complex │ │
│ │ email, social x3 │ │
│ │ └─────────┬───────────┘ │ │
│ │ content package (shared memory) │
│ │ ┌─────────▼───────────┐ │ │
│ │ Content Reviewer │ creativity: 0.2 │
│ │ │ Calls tools: │──→ check_brand_voice │ │
│ │ │──→ check_accessibility │
│ │ └─────────┬───────────┘ │ │
│ │ │
│ │ APPROVED or REVISION LIST │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ A/B Test Analyzer (standalone) │ creativity: 0.2 │
│ │ No tools — pure analysis on user input │ inference: complex │
│ │ Verdict: Ship / Extend / Kill / Invest │ │
│ └────────────────────────────────────────┘ │
│ │
│ Built-in tools also available: │
│ browse_web · search_web · get_today │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ INSTRUCTION TEMPLATES │
│ instructions/ │
│ └── CLEAR - Exhibition AB Test Analysis.txt │
│ (C) Context → (L) Layout → (E) Example → (A) Action → (R) │
└──────────────────────────────────────────────────────────────────────┘
Opal discovers tools at runtime via HTTP endpoints. Two patterns exist in the ecosystem:
The official @optimizely-opal/opal-tools-sdk package provides @tool decorators on Express. Auto-generates /discovery. Best for teams already on Express who want the least boilerplate.
Any HTTP service that exposes /discovery or /registry returning tool schemas. We use Hono on Cloudflare Workers — zero cold start, edge-deployed, no Express dependency. The tool metadata is defined as a plain array and the routes are registered in a loop:
const app = new Hono();
app.get('/discovery', (c) => c.json({ tools: TOOLS.map(t => t.meta) }));
for (const tool of TOOLS) {
app.post(`/tools/${tool.name}`, async (c) => { /* ... */ });
}
export default app;browse_web, search_web, get_today — no deployment needed, enabled per agent via enabled_tools.
- Tool service exposes
/discoveryor/registryreturning JSON - Endpoint returns function names, descriptions, parameter schemas
- Opal reads this at runtime to know what tools the agent can call
- Agent's
enabled_toolsarray controls which tools are available to it - The LLM decides when to call a tool based on the description
-
Real data, not demos — The tool service calls Whitney's actual API. An agent can pull today's current exhibitions, search the real collection, and generate content grounded in facts — not hallucinated metadata.
-
Content at exhibition scale — Major museums launch 10-20 exhibitions per year. Each needs wall text, web copy, emails, and social posts in a consistent voice. This workflow turns a 2-week content cycle into hours.
-
Brand consistency without bottlenecks — Brand voice rules are codified in the tool, not trapped in one editor's head. New team members and freelancers produce on-brand content from day one.
-
Accessibility as a first-class concern — Museums serve diverse audiences including ESL visitors, children, and visitors with disabilities. Baking readability checks into the pipeline ensures every piece of content is evaluated. The Whitney's own API provides
alt_text,ai_alt_text, andvisual_descriptionfields — our tools surface and validate these. -
Experimentation culture — The A/B test analyzer makes experiment results accessible to non-technical stakeholders (curators, directors), encouraging data-informed digital experience decisions.
whitney-museum-opal-agent/
├── README.md ← you are here
├── .github/workflows/
│ └── deploy.yml ← CI/CD: deploys tools/ to Cloudflare Workers
├── output/
│ └── demo-run.md ← full E2E output from all three agents
├── agents/
│ ├── whitney-exhibition-ab-test-analyzer.json ← specialized agent (standalone)
│ ├── whitney-content-writer.json ← specialized agent (workflow step 1)
│ ├── whitney-content-reviewer.json ← specialized agent (workflow step 2)
│ └── whitney-exhibition-content-workflow.json ← workflow orchestration
├── tools/
│ ├── package.json
│ ├── tsconfig.json
│ ├── wrangler.toml ← Cloudflare Workers config
│ ├── .env.example ← API keys + deployment credentials
│ └── src/
│ ├── index.ts ← tool service (Hono, 4 tools)
│ ├── cli.ts ← test harness CLI entry point
│ ├── runner.ts ← single-agent agentic loop
│ ├── workflow.ts ← multi-step workflow orchestration
│ ├── tool-client.ts ← discovery fetch + tool call routing
│ ├── config.ts ← agent JSON loading + validation
│ ├── prompt.ts ← [[param]] template rendering
│ └── types.ts ← shared TypeScript interfaces
└── instructions/
└── CLEAR - Exhibition AB Test Analysis.txt ← CLEAR instruction template