Skip to content

plugin sdk queries

Andre Lafleur edited this page Jul 10, 2026 · 12 revisions

About plugin queries

Plugins respond to queries from clients (Security Desk, Config Tool, Web SDK). Understanding the query flow is essential for proper implementation.

Query flow architecture

sequenceDiagram
    participant Client as Client (Security Desk)
    participant Directory
    participant Host as Plugin Host
    participant Plugin

    Client->>Directory: Send ReportQuery
    Directory->>Host: Distribute to roles with supported report types
    Host->>Host: Filter based on SupportedQueries and SupportedCustomReports
    Host->>Plugin: OnQueryReceived()
    Plugin->>Plugin: Process query
    Plugin-->>Directory: Send results
    Directory-->>Client: Return results
Loading

Key points

  • Directory distributes queries to roles that registered supported report types
  • SDK filters queries using SupportedQueries and SupportedCustomReports
  • Plugin receives only queries it supports
  • Plugin must send results and completion

Declaring query support

Plugins must explicitly declare which query types they support by overriding SupportedQueries:

public override List<ReportQueryType> SupportedQueries => 
    new List<ReportQueryType>
    {
        ReportQueryType.DoorActivity,
        ReportQueryType.AuditTrails,
        ReportQueryType.Custom
    };

Important

  • Only queries in SupportedQueries are routed to your plugin
  • The SDK filters queries before calling OnQueryReceived()
  • If SupportedQueries is empty or not overridden, the plugin receives no queries
  • Custom queries require filtering by CustomReportId

Processing Queries

The query handler is the entry point for all plugin-side query processing.

OnQueryReceived()

This abstract method must be implemented by all plugins:

protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
    // Process query and send results
}

Method is called when

  • A query matching SupportedQueries is received
  • For Custom queries, CustomReportId matches SupportedCustomReports
  • Client is waiting for response

Query execution considerations

  • Return quickly or offload long-running work
  • Use async work for long-running queries

ReportQueryReceivedEventArgs Properties

Property Type Description
Query ReportQuery The query to process. Cast to specific type (for example, CustomQuery) as needed.
MessageId int Message identifier for this query request. Use with QuerySource to uniquely identify a query.
QuerySource Guid Unique identifier of the Application that sent the query. Combined with MessageId to uniquely identify the request.
DispatchedSystems List<Guid> Systems the query has been dispatched to.

Note

MessageId alone is not unique across multiple clients. Always use both MessageId and QuerySource together to identify a specific query request.

Query processing requirements

When OnQueryReceived() is called, you must:

  1. Don't block - Return quickly or process asynchronously
  2. Send results - Use Engine.ReportManager.SendQueryResult() for each result batch
  3. Always complete - Must call SendQueryCompleted() even if no results
  4. Handle errors gracefully - Catch exceptions and report errors

Query completion is mandatory

protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
    try
    {
        // Process query...
        ProcessQuery(args.Query);
    }
    finally
    {
        // ALWAYS send query completed
        Engine.ReportManager.SendQueryCompleted(
            args.MessageId, 
            args.QuerySource, 
            PluginGuid, 
            true, 
            ReportError.None, 
            string.Empty);
    }
}

If you don't call SendQueryCompleted(), the client waits until timeout.

Sending query results

Send results in one or more batches, then explicitly signal completion so the client can stop waiting.

Result Flow

flowchart TB
    A[OnQueryReceived called] --> B[Process query]
    B --> C{More results?}
    C -->|Yes| D[SendQueryResult<br/>messageId, results]
    D --> C
    C -->|No| E[SendQueryCompleted<br/>messageId, ...]
Loading

SendQueryResult()

Used to send result data to the client:

var results = new ReportQueryResults(ReportQueryType.DoorActivity)
{
    Results = dataSet, // DataSet containing result tables
    QuerySource = args.QuerySource,
    ResultSource = PluginGuid
};

Engine.ReportManager.SendQueryResult(args.MessageId, results);

Key points

  • Can be called multiple times for the same query (streaming results)
  • Each call sends a batch of data
  • Client receives results incrementally
  • Use appropriate ReportQueryResults type for query

SendQueryCompleted()

Signals that no more results will be sent:

Engine.ReportManager.SendQueryCompleted(
    args.MessageId,
    args.QuerySource,
    PluginGuid,
    true,
    ReportError.None,
    string.Empty
);

Parameters

  • successful - true if query succeeded, false if error
  • error - ReportError enumeration value
  • errorDetail - Optional error details for client

Custom Queries

For ReportQueryType.Custom, you must filter by CustomReportId:

public override List<Guid> SupportedCustomReports => 
    new List<Guid> { CustomReportPageGuid };

protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
    if (args.Query is CustomQuery customQuery)
    {
        if (customQuery.CustomReportId == CustomReportPageGuid)
        {
            // Handle this custom report
            HandleCustomReport(customQuery);
        }
    }
    
    Engine.ReportManager.SendQueryCompleted(...);
}

If the client report defines a custom ReportFilter, the query also includes the serialized custom filter payload in CustomQuery.FilterData. The payload format is application-defined. Use the same format on the client and server.

Custom query workflow

  1. Add ReportQueryType.Custom to SupportedQueries
  2. Override SupportedCustomReports with your report GUIDs
  3. Check CustomReportId in OnQueryReceived()
  4. Read CustomQuery.FilterData if the report defines a custom filter
  5. Only process queries matching your GUIDs

Query Cancellation

Clients can cancel queries that are taking too long, or the Directory may cancel queries that timeout:

protected override void OnQueryCancelled(ReportQueryCancelledEventArgs args)
{
    // Cancel any ongoing work for this query
    // No need to call SendQueryCompleted() for cancellations
}

ReportQueryCancelledEventArgs Properties

Property Type Description
MessageId int Message identifier of the original query request. Use with QueryId to locate the query to cancel.
QueryId Guid Unique identifier matching ReportQuery.QueryId of the original query.
SystemsToCancel IEnumerable<Guid> External systems for which this query should be cancelled. Check if your PluginGuid is in this list.

Cancellation notes

  • Cancellations are informational - no response required
  • May come from clients or from Directory timeouts
  • Use to clean up resources for long-running queries
  • Check SystemsToCancel to see if cancellation applies to your plugin

Async query processing

Don't block the query thread - process asynchronously:

protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
    Task.Run(() => 
    {
        bool succeeded = true;
        ReportError error = ReportError.None;
        try
        {
            // Long-running query processing
            ReportQueryResults results = ProcessQuery(args.Query);
            Engine.ReportManager.SendQueryResult(args.MessageId, results);
        }
        catch (Exception ex)
        {
            Logger.TraceError(ex, "Query processing failed");
            succeeded = false;
            error = ReportError.Unknown;
        }
        finally
        {
            Engine.ReportManager.SendQueryCompleted(
                args.MessageId, args.QuerySource, PluginGuid, 
                succeeded, error, string.Empty);
        }
    });
}

Native report categories

Plugins can contribute rows to Security Center's own built-in reports, not just answer custom ones (Tutorial 04 walks through this end to end). Contribute to a native report when your integration answers a question Security Center already asks, so your rows appear in the task operators already use; build a custom report only when no built-in category fits your data's shape.

Every native category is one of three shapes, which decides how your handler builds a row:

  • Column: fill the product's DataTable using the column-name constants the query type publishes.
  • Trail: build a row with a row builder (ActivityTrailRow, AuditTrailRow) that implements IRow, instead of filling columns by hand.
  • Aggregate: like a column report, but one row per source entity with no time range (Health statistics).
Category ReportQueryType Built-in task Shape Contribute when
Door activity DoorActivity Door activities Column Your integration records door-centric access events.
Cardholder activity CardholderActivity Cardholder activities Column Your data is about the cardholder rather than the door.
Credential activity CredentialActivity Credential activities Column Your data is about the credential used.
Area activity AreaActivity Area activities Column Your events describe movement into or out of an area.
Elevator activity ElevatorActivity Elevator activities Column Your events are floor-level access on an elevator.
Access control unit activity UnitActivity Access control unit events Column Your integration reports the health or tamper state of an access control unit.
Zone activity ZoneActivity Zone activities Column Your integration drives or observes zone state (I/O zones, hardwired zones).
Intrusion area activity IntrusionAreaActivity Intrusion detection area activities Column Your integration is an intrusion panel or bridges one into Security Center.
Intrusion unit activity IntrusionUnitActivity Intrusion detection unit events Column Same as above, for the panel's own hardware events.
Camera / video events CameraEvent, VideoMotionEvent Camera events Column Your integration produces camera or video-analytics events.
Health events HealthEvent Health history Column Your integration reports discrete health events for its devices or services.
Health statistics HealthStatistics Health statistics Aggregate You report rolled-up availability metrics (uptime, MTBF, MTTR) rather than individual events.
Activity trails ActivityTrails Activity trails Trail Your integration performs trackable operator or system actions.
Audit trails AuditTrails Audit trails Trail Your integration changes configuration.
Custom Custom Your own task Column No category above fits your data's shape.

The access control activity family (Door, Cardholder, Credential, Area, Elevator, Access control unit activity) share one result schema, and Camera events has its own; for the exact filters and result columns of every one of these, see ReportManager query reference rather than re-deriving them here.

Health events, Health statistics, Activity trails, and Audit trails are not part of that reference, since they are answered only by a plugin, not run as ordinary Platform SDK queries. Health events (query type HealthEvent) returns: Health event number, Event source type, Source entity, Event description, Machine, Event timestamp, Severity (Information, Warning, Error), Error number, Occurrence count, and Observer entity (the role, server, or unit that reported it). Health statistics, Activity trails, and Audit trails have no fixed column list to publish, since the aggregate shape and the two row builders (ActivityTrailRow, AuditTrailRow) construct the row for you; see Tutorial 04, Step 12 (Health statistics), Step 10 (Activity trails), and Step 11 (Audit trails) for what each builder sets.

Query performance considerations

For large datasets, structure the response so the client can start processing before the full result set is ready.

Chunking Results

For large result sets, send data in chunks:

const int ChunkSize = 1000;
var allResults = GetQueryResults(args.Query);

for (int i = 0; i < allResults.Count; i += ChunkSize)
{
    var chunk = allResults.Skip(i).Take(ChunkSize).ToList();
    var dataSet = CreateDataSet(chunk);
    
    var results = new ReportQueryResults(args.Query.ReportQueryType)
    {
        Results = dataSet,
        QuerySource = args.QuerySource,
        ResultSource = PluginGuid
    };
    
    Engine.ReportManager.SendQueryResult(args.MessageId, results);
}

See also

Platform SDK

Plugin SDK

Workspace SDK

Media SDK

Macro SDK

Web SDK

Synergis RIO

Media Gateway

Genetec Web Player

Clone this wiki locally