Portal Community

Extension Points Overview

Search Providers

IWorkDeskSearchProvider

Add a new searchable section to the unified global search — your data appears alongside tasks, history, and notifications.

Notification Handlers

IWorkDeskNotificationHandler

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

IWorkDeskWidgetDefinition

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

Admin API

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 TypeInterfaceRegistration MethodTakes Effect
Search ProviderIWorkDeskSearchProviderservices.AddWorkDeskSearchProvider<T>()Next application restart
Notification HandlerIWorkDeskNotificationHandlerservices.AddWorkDeskNotificationHandler<T>()Next application restart
Dashboard Widget (backend)IWorkDeskWidgetDefinitionservices.AddWorkDeskWidget<T>()Next application restart
Dashboard Widget (frontend)React componentregisterWidget(type, Component)On next frontend bundle build
App Studio AppNone (admin API)PUT /api/workdesk/admin/navigation-configImmediately (next page load)

Extension Security Rules

Extension Security Requirements
  • Search providers must always apply actorId and tenantId filters 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 Handle method — it is synchronous and runs in the notification delivery hot path.
  • Widget data fetchers must respect tenant isolation. The tenantId parameter 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);
Extension Discovery

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).