Skip to content

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 := true when 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