Extension Points
WorkDesk is designed to be extended by platform engineers without modifying core source code. Four primary extension points exist: custom search providers, custom notification handlers, dashboard widgets, and App Studio app hosting. Each follows a plugin registration pattern using .NET dependency injection.
Extension Points Overview
Search Providers
Add a new searchable section to the unified global search — your data appears alongside tasks, history, and notifications.
Notification Handlers
Handle new notification types from custom EdgeStream topics — control how they appear, what they link to, and how they affect the badge count.
Dashboard Widgets
Register a new widget type that employees can add to their personal dashboard — provide the React component, config schema, and data fetcher.
App Studio Hosting
Add App Studio apps to the WorkDesk navigation — no code changes required, configured entirely through the admin API.
IWorkDeskSearchProvider — Custom Search Sections
Implement IWorkDeskSearchProvider to add your data as a searchable section in WorkDesk's unified search. Your results appear in the same search panel as native tasks, history, and notifications:
// IWorkDeskSearchProvider — interface contract
public interface IWorkDeskSearchProvider
{
// Unique key for this section — appears in ?section= query param
string SectionKey { get; }
// Display name shown in the search results panel
string DisplayName { get; }
Task<SearchSectionResult> SearchAsync(
string query,
Guid actorId,
Guid tenantId,
int limit,
int page,
CancellationToken ct);
}
public record SearchSectionResult(
IReadOnlyList<SearchResultItem> Items,
int TotalCount,
bool HasMore);
public record SearchResultItem(
string Id,
string Title,
string? Subtitle,
double Score, // 0.0 – 1.0 relevance score
string? ActionUrl, // URL to navigate to when result is clicked
Dictionary<string, string>? Highlights); // field → highlighted snippet
// Example implementation
public class ExpenseSearchProvider : IWorkDeskSearchProvider
{
public string SectionKey => "expenses";
public string DisplayName => "Expense Reports";
public async Task<SearchSectionResult> SearchAsync(
string query, Guid actorId, Guid tenantId, int limit, int page, CancellationToken ct)
{
var results = await _expenseRepo.SearchAsync(
query, actorId, tenantId, limit, page, ct);
return new SearchSectionResult(
Items: results.Select(r => new SearchResultItem(
Id: r.Id.ToString(),
Title: r.Description,
Subtitle: $"${r.Amount:F2} — {r.Status}",
Score: r.Relevance,
ActionUrl: $"/apps/expense-manager/reports/{r.Id}"
)).ToList(),
TotalCount: results.TotalCount,
HasMore: results.HasMore);
}
}
// Registration in Program.cs
services.AddWorkDeskSearchProvider<ExpenseSearchProvider>();
IWorkDeskNotificationHandler — Custom Notification Types
By default, WorkDesk handles the four built-in notification types. To handle notifications from custom EdgeStream topics (e.g., a custom app emitting its own events), implement IWorkDeskNotificationHandler:
// IWorkDeskNotificationHandler — handle custom notification types
public interface IWorkDeskNotificationHandler
{
// The notification type this handler is responsible for
string NotificationType { get; }
// Format the raw notification payload into a WorkDesk notification
WorkDeskNotification Handle(RawNotificationPayload raw);
// Optional: custom action URL builder
string? BuildActionUrl(RawNotificationPayload raw);
}
// Example — custom notification from an expense app
public class ExpenseApprovedNotificationHandler : IWorkDeskNotificationHandler
{
public string NotificationType => "expense_approved";
public WorkDeskNotification Handle(RawNotificationPayload raw)
{
return new WorkDeskNotification
{
Title = $"Expense Approved: {raw.Data["description"]}",
Body = $"Your expense of ${raw.Data["amount"]} was approved by {raw.Data["approverName"]}.",
Priority = NotificationPriority.Normal,
ActionUrl = BuildActionUrl(raw),
Icon = "fa-receipt",
IconColor = "#3cd282"
};
}
public string? BuildActionUrl(RawNotificationPayload raw)
=> $"/apps/expense-manager/reports/{raw.Data["reportId"]}";
}
// Registration
services.AddWorkDeskNotificationHandler<ExpenseApprovedNotificationHandler>();
Dashboard Widget Extension
New widget types can be registered so employees can add them to their personal dashboard. A widget registration requires three parts: a backend definition (data fetcher + config schema) and a frontend React component:
// Backend — register widget definition
public class ExpenseSummaryWidgetDefinition : IWorkDeskWidgetDefinition
{
public string WidgetType => "expense-summary";
public string DisplayName => "Expense Summary";
public string Description => "Shows your pending and approved expenses for the current month";
public string IconClass => "fa-receipt";
// JSON Schema for widget configuration (rendered in picker)
public string ConfigSchema => """
{
"type": "object",
"properties": {
"showChart": { "type": "boolean", "title": "Show Chart", "default": true },
"period": { "type": "string", "title": "Period", "enum": ["month", "quarter", "year"] }
}
}
""";
// Fetch data for the widget
public async Task<object> FetchDataAsync(
Guid actorId, Guid tenantId, JsonElement config, CancellationToken ct)
{
var period = config.GetProperty("period").GetString() ?? "month";
return await _expenseService.GetSummaryAsync(actorId, tenantId, period, ct);
}
}
services.AddWorkDeskWidget<ExpenseSummaryWidgetDefinition>();
// Frontend — register React component (in WorkDesk frontend plugin)
// workdesk-plugins/expense-summary-widget.tsx
import { registerWidget } from '@bizfirstai/workdesk-sdk';
registerWidget('expense-summary', ExpenseSummaryWidget);
function ExpenseSummaryWidget({ data, config }: WidgetProps) {
return (
<div className="widget-content">
<div className="metric">Pending: ${data.pendingAmount.toFixed(2)}</div>
<div className="metric">Approved: ${data.approvedAmount.toFixed(2)}</div>
{config.showChart && <ExpenseChart data={data.chartData} />}
</div>
);
}
Extension Registration Summary
| Extension Type | Interface | Registration Method | Takes Effect |
|---|---|---|---|
| Search Provider | IWorkDeskSearchProvider | services.AddWorkDeskSearchProvider<T>() | Next application restart |
| Notification Handler | IWorkDeskNotificationHandler | services.AddWorkDeskNotificationHandler<T>() | Next application restart |
| Dashboard Widget (backend) | IWorkDeskWidgetDefinition | services.AddWorkDeskWidget<T>() | Next application restart |
| Dashboard Widget (frontend) | React component | registerWidget(type, Component) | On next frontend bundle build |
| App Studio App | None (admin API) | PUT /api/workdesk/admin/navigation-config | Immediately (next page load) |
Extension Security Rules
- Search providers must always apply
actorIdandtenantIdfilters before performing any full-text search. WorkDesk does not apply these filters on behalf of the provider. - Notification handlers must not make API calls during the
Handlemethod — it is synchronous and runs in the notification delivery hot path. - Widget data fetchers must respect tenant isolation. The
tenantIdparameter is extracted from the user's JWT and cannot be overridden by client requests. - App Studio apps receive the user's JWT via postMessage. Apps must validate the JWT origin before trusting the token.
Extension Testing
WorkDesk provides test harnesses for validating extensions before deployment:
// Testing a search provider
var harness = new WorkDeskSearchProviderHarness<ExpenseSearchProvider>();
var result = await harness.SearchAsync(
query: "travel expense",
actorId: testActorId,
tenantId: testTenantId,
limit: 5);
Assert.True(result.Items.All(i => i.Score >= 0 && i.Score <= 1));
Assert.True(result.Items.All(i => i.ActionUrl != null));
// Verify tenant isolation
Assert.True(result.Items.All(i => i.BelongsToTenant(testTenantId)));
// Testing a notification handler
var handlerHarness = new WorkDeskNotificationHandlerHarness<ExpenseApprovedNotificationHandler>();
var notification = handlerHarness.Handle(rawPayload);
Assert.Equal("Expense Approved: Q1 Travel", notification.Title);
Assert.Contains("/expense-manager/reports/", notification.ActionUrl);
WorkDesk uses .NET DI to discover all registered extensions at startup. There is no separate plugin manifest or configuration file needed. As long as the extension is registered via the AddWorkDesk* methods in Program.cs, it will be picked up automatically by the relevant subsystems (search, notifications, dashboard).