Architecting a Plugin SDK for Extending the Genesys Cloud Agent Desktop with Custom Panels
What This Guide Covers
You are building a Plugin SDK framework - a standardized architecture for loading, rendering, and managing third-party or internal custom panels inside the Genesys Cloud Agent Desktop - so that your development teams can build self-contained “plugins” (CRM sidebars, knowledge base viewers, order management panels, quality coaching cards) that register with a central Plugin Manager, receive interaction lifecycle events, and render inside designated panel slots without modifying the core Agent Desktop codebase. When complete, adding a new agent tool is as simple as registering a plugin manifest, and removing one is a config change - no redeployment of the host application required.
Prerequisites, Roles & Licensing
- Genesys Cloud: Any CX tier with Client App SDK integration.
- Skills required: TypeScript, React (or any SPA framework), iFrame postMessage API.
- Architecture decision: Plugins run in isolated iFrames for security and fault isolation - a crashing plugin cannot take down the host application.
The Implementation Deep-Dive
1. Plugin Architecture Overview
[Agent Desktop Host Application]
|
├── [Plugin Manager] ── Registers, loads, and routes events to plugins
| |
| ├── [Plugin: CRM Sidebar] ────── iFrame (crm-plugin.yourcompany.com)
| ├── [Plugin: Knowledge Base] ──── iFrame (kb-plugin.yourcompany.com)
| ├── [Plugin: Order Lookup] ────── iFrame (orders-plugin.yourcompany.com)
| └── [Plugin: Coaching Card] ───── iFrame (coaching-plugin.yourcompany.com)
|
└── [Genesys Cloud Client App SDK] ── Interaction events, user context
2. Plugin Manifest Schema
Each plugin declares its capabilities, panel placement, and required permissions in a JSON manifest:
// types/plugin-manifest.ts
export interface PluginManifest {
id: string; // Unique plugin identifier
name: string; // Human-readable name
version: string; // Semantic version
author: string;
// Where the plugin renders
placement: 'sidebar' | 'top-bar' | 'interaction-panel' | 'full-screen-modal';
// Plugin entry point URL (loaded in iFrame)
entryUrl: string;
// Dimensions
defaultWidth?: number; // px (for sidebar)
defaultHeight?: number; // px (for panel)
minWidth?: number;
// Which interaction events the plugin subscribes to
subscriptions: PluginSubscription[];
// Permissions required from the host
permissions: PluginPermission[];
// Conditions under which the plugin is shown
activationRules: ActivationRule[];
}
export type PluginSubscription =
| 'interaction.started'
| 'interaction.connected'
| 'interaction.held'
| 'interaction.disconnected'
| 'interaction.wrapup'
| 'user.presenceChanged'
| 'view.activated';
export type PluginPermission =
| 'read:interaction' // Can read interaction data
| 'read:customer' // Can read customer profile
| 'write:participantData' // Can set participant attributes
| 'action:transfer' // Can initiate transfers
| 'action:wrapup'; // Can set wrap-up codes
export interface ActivationRule {
type: 'mediaType' | 'queue' | 'always';
value?: string; // e.g., "voice", "chat", or queue name
}
Example manifest:
{
"id": "crm-sidebar-salesforce",
"name": "Salesforce CRM Sidebar",
"version": "2.1.0",
"author": "Internal Tools Team",
"placement": "sidebar",
"entryUrl": "https://crm-plugin.internal.yourcompany.com/index.html",
"defaultWidth": 380,
"subscriptions": ["interaction.started", "interaction.connected", "interaction.disconnected"],
"permissions": ["read:interaction", "read:customer", "write:participantData"],
"activationRules": [
{ "type": "always" }
]
}
3. Plugin Manager (Host Side)
// plugin-manager.ts
import type { PluginManifest, PluginSubscription } from './types/plugin-manifest';
interface LoadedPlugin {
manifest: PluginManifest;
iframe: HTMLIFrameElement;
ready: boolean;
}
export class PluginManager {
private plugins: Map<string, LoadedPlugin> = new Map();
private pluginContainer: HTMLElement;
constructor(containerId: string) {
this.pluginContainer = document.getElementById(containerId)!;
}
async registerPlugin(manifest: PluginManifest): Promise<void> {
if (this.plugins.has(manifest.id)) {
console.warn(`Plugin ${manifest.id} already registered`);
return;
}
// Create sandboxed iFrame
const iframe = document.createElement('iframe');
iframe.id = `plugin-${manifest.id}`;
iframe.src = manifest.entryUrl;
iframe.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms');
iframe.style.width = `${manifest.defaultWidth || 380}px`;
iframe.style.height = '100%';
iframe.style.border = 'none';
// Create panel wrapper
const panel = document.createElement('div');
panel.className = 'plugin-panel';
panel.dataset.pluginId = manifest.id;
panel.dataset.placement = manifest.placement;
panel.appendChild(iframe);
this.pluginContainer.appendChild(panel);
const plugin: LoadedPlugin = { manifest, iframe, ready: false };
this.plugins.set(manifest.id, plugin);
// Listen for plugin ready signal
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow && event.data?.type === 'plugin:ready') {
plugin.ready = true;
console.log(`✓ Plugin ${manifest.id} ready`);
// Send initial configuration
this.sendToPlugin(manifest.id, {
type: 'host:config',
payload: {
permissions: manifest.permissions,
theme: this.getCurrentTheme()
}
});
}
});
}
broadcastEvent(event: PluginSubscription, payload: any): void {
for (const [id, plugin] of this.plugins) {
if (!plugin.ready) continue;
if (!plugin.manifest.subscriptions.includes(event)) continue;
// Check activation rules
if (!this.isPluginActive(plugin.manifest, payload)) continue;
// Filter payload based on plugin permissions
const filteredPayload = this.filterByPermissions(payload, plugin.manifest.permissions);
this.sendToPlugin(id, {
type: `interaction:${event}`,
payload: filteredPayload
});
}
}
private sendToPlugin(pluginId: string, message: any): void {
const plugin = this.plugins.get(pluginId);
if (!plugin?.iframe.contentWindow) return;
plugin.iframe.contentWindow.postMessage(message, new URL(plugin.manifest.entryUrl).origin);
}
private filterByPermissions(payload: any, permissions: string[]): any {
const filtered = { ...payload };
if (!permissions.includes('read:customer')) {
delete filtered.customerName;
delete filtered.customerPhone;
delete filtered.crmData;
}
if (!permissions.includes('read:interaction')) {
delete filtered.conversationId;
delete filtered.participants;
}
return filtered;
}
private isPluginActive(manifest: PluginManifest, context: any): boolean {
return manifest.activationRules.some(rule => {
if (rule.type === 'always') return true;
if (rule.type === 'mediaType') return context.mediaType === rule.value;
if (rule.type === 'queue') return context.queueName === rule.value;
return false;
});
}
private getCurrentTheme(): object {
return {
mode: document.body.classList.contains('dark') ? 'dark' : 'light',
primaryColor: getComputedStyle(document.body).getPropertyValue('--primary-color').trim()
};
}
unregisterPlugin(pluginId: string): void {
const plugin = this.plugins.get(pluginId);
if (plugin) {
plugin.iframe.remove();
this.plugins.delete(pluginId);
console.log(`✓ Plugin ${pluginId} unregistered`);
}
}
}
4. Plugin Client SDK (Plugin Side)
Provide a lightweight SDK that plugin authors import:
// @genesys-plugins/client-sdk
export class PluginClient {
private handlers: Map<string, Function[]> = new Map();
private hostOrigin: string;
constructor(hostOrigin: string = '*') {
this.hostOrigin = hostOrigin;
window.addEventListener('message', (event) => {
const { type, payload } = event.data || {};
const listeners = this.handlers.get(type) || [];
listeners.forEach(fn => fn(payload));
});
// Signal readiness to host
window.parent.postMessage({ type: 'plugin:ready', pluginId: this.getPluginId() }, this.hostOrigin);
}
on(event: string, handler: Function): void {
if (!this.handlers.has(event)) this.handlers.set(event, []);
this.handlers.get(event)!.push(handler);
}
setParticipantData(key: string, value: string): void {
window.parent.postMessage({
type: 'plugin:action',
action: 'setParticipantData',
payload: { key, value }
}, this.hostOrigin);
}
private getPluginId(): string {
return new URLSearchParams(window.location.search).get('pluginId') || 'unknown';
}
}
Usage in a plugin:
// CRM Sidebar Plugin - main.ts
import { PluginClient } from '@genesys-plugins/client-sdk';
const client = new PluginClient('https://agent-desktop.yourcompany.com');
client.on('interaction:interaction.connected', (data) => {
const { customerPhone, conversationId } = data;
// Look up customer in Salesforce
fetchSalesforceContact(customerPhone).then(contact => {
renderContactCard(contact);
// Write CRM ID back to Genesys participant data
client.setParticipantData('sfdc_contact_id', contact.id);
});
});
Validation, Edge Cases & Troubleshooting
Edge Case 1: Plugin Crashes and Shows a White iFrame
A JavaScript error in the CRM plugin causes an unhandled exception. The iFrame goes blank. The agent sees a white box where CRM data should be.
Solution: Implement a heartbeat protocol. The host sends a ping message every 10 seconds; the plugin responds with pong. If 3 consecutive pings go unanswered, the host replaces the iFrame with an error banner (“CRM Plugin unavailable - click to retry”) and logs the failure. The retry button reloads the iFrame source.
Edge Case 2: Plugin Requests Permissions It Wasn’t Granted
A plugin attempts to call setParticipantData but its manifest only declares read:interaction. The host should enforce this.
Solution: In the Plugin Manager’s message event handler, validate every plugin:action message against the plugin’s declared permissions before executing it. If a permission is missing, log a security warning and ignore the action. Never trust the plugin to self-enforce permissions.
Edge Case 3: Multiple Plugins Compete for Sidebar Space
Three sidebar plugins are registered, but the sidebar is only 400px wide. They overlap.
Solution: Implement a tabbed plugin container. Each sidebar plugin becomes a tab. The most recently activated plugin (based on the current interaction context) is shown by default. Agents can click tabs to switch between plugins. Persist the agent’s preferred tab order in localStorage.