-
Notifications
You must be signed in to change notification settings - Fork 6
plugin sdk queries
Plugins respond to queries from clients (Security Desk, Config Tool, Web SDK). Understanding the query flow is essential for proper implementation.
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
- Directory distributes queries to roles that registered supported report types
- SDK filters queries using
SupportedQueriesandSupportedCustomReports - Plugin receives only queries it supports
- Plugin must send results and completion
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
SupportedQueriesare routed to your plugin - The SDK filters queries before calling
OnQueryReceived() - If
SupportedQueriesis empty or not overridden, the plugin receives no queries - Custom queries require filtering by
CustomReportId
The query handler is the entry point for all plugin-side query processing.
This abstract method must be implemented by all plugins:
protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
// Process query and send results
}- A query matching
SupportedQueriesis received - For
Customqueries,CustomReportIdmatchesSupportedCustomReports - Client is waiting for response
- Return quickly or offload long-running work
- Use async work for long-running queries
| 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.
When OnQueryReceived() is called, you must:
- Don't block - Return quickly or process asynchronously
-
Send results - Use
Engine.ReportManager.SendQueryResult()for each result batch -
Always complete - Must call
SendQueryCompleted()even if no results - Handle errors gracefully - Catch exceptions and report errors
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.
Send results in one or more batches, then explicitly signal completion so the client can stop waiting.
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, ...]
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);- Can be called multiple times for the same query (streaming results)
- Each call sends a batch of data
- Client receives results incrementally
- Use appropriate
ReportQueryResultstype for query
Signals that no more results will be sent:
Engine.ReportManager.SendQueryCompleted(
args.MessageId,
args.QuerySource,
PluginGuid,
true,
ReportError.None,
string.Empty
);-
successful- true if query succeeded, false if error -
error-ReportErrorenumeration value -
errorDetail- Optional error details for client
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.
- Add
ReportQueryType.CustomtoSupportedQueries - Override
SupportedCustomReportswith your report GUIDs - Check
CustomReportIdinOnQueryReceived() - Read
CustomQuery.FilterDataif the report defines a custom filter - Only process queries matching your GUIDs
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
}| 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. |
- Cancellations are informational - no response required
- May come from clients or from Directory timeouts
- Use to clean up resources for long-running queries
- Check
SystemsToCancelto see if cancellation applies to your plugin
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);
}
});
}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
DataTableusing the column-name constants the query type publishes. -
Trail: build a row with a row builder (
ActivityTrailRow,AuditTrailRow) that implementsIRow, 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.
For large datasets, structure the response so the client can start processing before the full result set is ready.
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);
}- Plugin SDK overview: Plugin architecture, lifecycle, and components.
- Plugin SDK threading: Engine thread, QueueUpdate, and async patterns for plugins.
- Plugin SDK lifecycle: Plugin initialization, startup, and disposal phases.
- ReportManager query reference: filters and result columns for every query type, including the native report categories a plugin can contribute to.
- About transactions: Batching multiple entity operations into one request.
- Overview
- Connecting to Security Center
- SDK certificates
- Referencing SDK assemblies
- SDK compatibility
- Entities
- Entity cache
- Entity loading, cache, and query options
- Transactions
- Events
- Actions
- Security Desk
- Custom events
- ReportManager
- ReportManager query reference
- Access control raw events
- DownloadAllRelatedData and StrictResults
- Privileges
- Partitions
- About custom fields
- About video
- About cameras
- Enrolling a video unit
- Archiver and auxiliary archiver roles
- Archive transfer
- About access control
- About cardholders and credentials
- About doors, areas, elevators, and access points
- About access rules and schedules
- About access control units and interface modules
- Enrolling an access control unit
- Door templates
- Visitors
- Mobile credentials
- About threat levels
- About alarms
- Maps
- Logging
- Overview
- Certificates
- Lifecycle
- Threading
- State management
- Configuration
- Restricted configuration
- Events
- Queries
- Request manager
- Database
- Entity ownership
- Engine extensions
- Entity mappings
- Server management
- Custom privileges
- Custom entity types
- Resolving non-SDK assemblies
- Deploying plugins
- .NET 8 support
- Overview
- Certificates
- Creating modules
- Tasks
- Pages
- Components
- Tile extensions
- Services
- Contextual actions
- Options extensions
- Configuration pages
- Monitors
- Shared components
- Commands
- Extending events
- Map extensions
- Timeline providers
- Image extractors
- Credential encoders
- Credential readers
- Cardholder fields extractors
- Badge printers
- Content builders
- Dashboard widgets
- Incidents
- Logon providers
- Pinnable content builders
- Custom report pages
- Overview
- Getting started
- MediaPlayer
- VideoSourceFilter
- MediaExporter
- MediaFile
- G64 converters
- FileCryptingManager
- PlaybackSequenceQuerier
- PlaybackStreamReader
- OverlayFactory
- PtzCoordinatesManager
- AudioTransmitter
- AudioRecorder
- AnalogMonitorController
- Camera blocking
- Overview
- Getting started
- Referencing entities
- Entity operations
- About access control in the Web SDK
- About video in the Web SDK
- Users and user groups
- Partitions
- Custom fields
- Custom card formats
- Actions
- Events and alarms
- Incidents
- Reports
- Tasks
- Macros
- Custom entity types
- System endpoints
- Performance guide
- Reference
- Under the hood
- Troubleshooting