Extending Agents
How to extend Smart Agents in AL — custom data sources, event subscribers, and agent behaviors
This guide explains how to extend Smart Agents from your own AL extension. You can add custom data sources, subscribe to processing events, modify agent behavior, and integrate with your own Business Central customizations.
Extension Model
Smart Agents uses a publisher/subscriber event model for extensibility. The core extension publishes events at key points in the agent processing pipeline. Your extension subscribes to these events to inject custom logic.
All public events are published by the SA Agent Router and SA Data Scope Manager codeunits.
Adding Custom Data Sources
By default, agents can access standard Business Central tables. If your organization uses custom tables or table extensions, you need to register them as data sources so agents can query them.
Registering a Custom Table
Subscribe to the OnRegisterDataSources event to make your custom tables available to agents:
codeunit 50100 "My Data Source Registration"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Data Scope Manager",
'OnRegisterDataSources', '', false, false)]
local procedure RegisterCustomTables(var DataSourceBuffer: Record "SA Data Source Buffer" temporary)
begin
DataSourceBuffer.Init();
DataSourceBuffer."Table ID" := Database::"My Custom Table";
DataSourceBuffer."Table Name" := 'My Custom Table';
DataSourceBuffer.Category := 'Custom';
DataSourceBuffer.Description := 'Custom data for project tracking';
DataSourceBuffer."Read Only" := true;
DataSourceBuffer.Insert();
end;
}Once registered, administrators can include your custom table in agent data scopes through the Agent Card configuration page.
Providing Custom Data Context
When an agent processes a query, it collects context data from the tables in its data scope. You can customize what data is sent for your custom tables by subscribing to the OnCollectDataContext event:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Agent Router",
'OnCollectDataContext', '', false, false)]
local procedure ProvideCustomContext(
AgentId: Code[20];
TableId: Integer;
UserMessage: Text;
var ContextJson: JsonObject;
var Handled: Boolean)
var
MyTable: Record "My Custom Table";
DataArray: JsonArray;
RowObj: JsonObject;
begin
if TableId <> Database::"My Custom Table" then
exit;
MyTable.SetRange(Status, MyTable.Status::Active);
if MyTable.FindSet() then
repeat
Clear(RowObj);
RowObj.Add('projectNo', MyTable."Project No.");
RowObj.Add('description', MyTable.Description);
RowObj.Add('budget', MyTable."Budget Amount");
RowObj.Add('actual', MyTable."Actual Amount");
DataArray.Add(RowObj);
until MyTable.Next() = 0;
ContextJson.Add('projects', DataArray);
Handled := true;
end;Subscribing to Processing Events
The Agent Router publishes events before and after each stage of query processing. Subscribe to these events to add custom logic.
Before Processing
The OnBeforeProcessMessage event fires before the query is sent to the Smart Agents API. Use it to validate, modify, or reject queries:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Agent Router",
'OnBeforeProcessMessage', '', false, false)]
local procedure ValidateBeforeProcessing(
AgentId: Code[20];
var UserMessage: Text;
var ContextJson: JsonObject;
var Cancel: Boolean;
var CancelReason: Text)
begin
// Block queries that mention restricted topics
if UserMessage.Contains('salary') or UserMessage.Contains('payroll') then begin
Cancel := true;
CancelReason := 'Salary and payroll queries are not permitted through Smart Agents. Please use the HR module directly.';
end;
end;After Processing
The OnAfterProcessMessage event fires after the API returns a response. Use it to log, audit, or post-process results:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Agent Router",
'OnAfterProcessMessage', '', false, false)]
local procedure LogAgentResponse(
AgentId: Code[20];
UserMessage: Text;
ResponseText: Text;
CreditsConsumed: Integer;
ModelUsed: Text)
var
AuditLog: Record "My Custom Audit Log";
begin
AuditLog.Init();
AuditLog."Entry No." := 0; // Auto-increment
AuditLog."Agent ID" := AgentId;
AuditLog."Query Summary" := CopyStr(UserMessage, 1, 250);
AuditLog."Credits Used" := CreditsConsumed;
AuditLog."Model" := CopyStr(ModelUsed, 1, 50);
AuditLog."Timestamp" := CurrentDateTime;
AuditLog."User ID" := UserId;
AuditLog.Insert(true);
end;On Credit Consumption
The OnCreditConsumed event fires each time credits are deducted. Use it for custom billing integrations:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Credit Tracker",
'OnCreditConsumed', '', false, false)]
local procedure TrackCreditUsage(
UserId: Code[50];
AgentId: Code[20];
CreditsConsumed: Integer;
ModelUsed: Text)
var
CostCenter: Code[20];
begin
// Map credit usage to a cost center dimension
CostCenter := GetUserCostCenter(UserId);
PostCreditChargeToDimension(CostCenter, CreditsConsumed);
end;Modifying Agent Behavior
Custom Agent Routing
Override the default agent selection logic by subscribing to OnResolveAgent:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Agent Router",
'OnResolveAgent', '', false, false)]
local procedure CustomAgentRouting(
var AgentId: Code[20];
UserMessage: Text;
CurrentPageId: Integer;
var Handled: Boolean)
begin
// Auto-select the inventory agent when the user is on the Item Card
if CurrentPageId = Page::"Item Card" then begin
AgentId := 'INVENTORY-AGENT';
Handled := true;
end;
end;Custom Data Scope Filtering
Add additional filtering to the data scope beyond what is configured in the agent:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"SA Data Scope Manager",
'OnFilterDataScope', '', false, false)]
local procedure RestrictDataByCompany(
AgentId: Code[20];
TableId: Integer;
UserId: Code[50];
var TableFilters: JsonObject)
begin
// Restrict all data queries to the user's assigned company
if IsMultiCompanyUser(UserId) then
TableFilters.Add('companyFilter', GetUserDefaultCompany(UserId));
end;Page Extensions
You can extend Smart Agents pages to add custom fields, actions, or factboxes.
Extending the Agent Card
Add custom fields to the agent configuration page:
pageextension 50100 "My Agent Card Ext" extends "SA Agent Card"
{
layout
{
addafter(Description)
{
field("Cost Center"; Rec."Cost Center")
{
ApplicationArea = All;
ToolTip = 'The cost center charged for this agent''s credit usage.';
}
}
}
}Extending the Chat Page
Add custom actions to the chat interface:
pageextension 50101 "My Chat Ext" extends "SA Chat"
{
actions
{
addlast(Processing)
{
action(ExportToExcel)
{
ApplicationArea = All;
Caption = 'Export to Excel';
ToolTip = 'Export the current conversation data to Excel.';
Image = ExportToExcel;
trigger OnAction()
begin
ExportConversationToExcel(Rec."Conversation ID");
end;
}
}
}
}Table Extensions
Add custom fields to Smart Agents tables:
tableextension 50100 "My Agent Ext" extends "SA Agent"
{
fields
{
field(50100; "Cost Center"; Code[20])
{
Caption = 'Cost Center';
DataClassification = OrganizationIdentifiableInformation;
TableRelation = "Dimension Value".Code where("Global Dimension No." = const(1));
}
field(50101; "Department Code"; Code[20])
{
Caption = 'Department Code';
DataClassification = OrganizationIdentifiableInformation;
TableRelation = "Dimension Value".Code where("Global Dimension No." = const(2));
}
}
}Best Practices
- Keep event subscribers fast — Event subscribers run synchronously during query processing. Avoid long-running operations that would delay agent responses
- Handle errors gracefully — Wrap your subscriber logic in error handling to prevent your extension from blocking Smart Agents functionality
- Use the Handled pattern — Set
Handled := truewhen your subscriber fully handles the event to prevent default processing - Respect data scope — Do not bypass the Data Scope Manager's access checks. Always work within the resolved data scope
- Test with the sandbox — Test your extensions in a Business Central sandbox environment before deploying to production