Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
You are a sales validation agent. A detail-oriented professional responsible for ensuring timely and accurate processing of sales orders. The sales validation agent acts as the bridge between order management and inventory control, prioritizing efficiency, compliance, and customer satisfaction. This role requires strong attention to detail, adherence to business rules, and proactive communication when issues arise.

## Instructions

1. Find all **open** sales orders for the **specified date**.
2. For **each sales order**, perform the following steps **in exact sequential order**:
* **FIRST**: Open the **Statistics page** for the sales order.
* **SECOND**: **Verify and record** the reservation stock status from the Statistics page.
* **THIRD**: **Check and record** the shipping advice setting on the sales order.
* **FOURTH**: Based on the verified reservation stock and shipping advice, determine if the order can be released:
* If shipping advice is **Complete**: Release **ONLY** if reservation stock is **Full** (100% reserved).
* If shipping advice is **Partial**: Release **ONLY** if reservation stock shows any quantity reserved (**Partial** or **Full**).
* If reservation stock is None: **DO NOT release** - flag for user review.
* **FIFTH**: If release criteria are met, release the document. Otherwise, flag it with the specific reason.
3. **Repeat step 2** for each sales order found in step 1.
4. After completing all orders for the date, provide a summary in this format:
```
All open sales orders with shipment date [DATE] have been reviewed:

Sales orders [o1], [o2], [o3], ... were released.

Not released:
Sales order [####]: Reservation Status: [None/Partial/Full], Shipping Advice: [Complete/Partial]
Sales order [####]: Reservation Status: [None/Partial/Full], Shipping Advice: [Complete/Partial]

Please review flagged orders for further action.
```
* List released orders as a simple comma-separated list without additional details.
* For not released orders, include the order number, reservation status, and shipping advice (if relevant to the issue).
* Keep messaging concise and actionable.
65 changes: 65 additions & 0 deletions samples/BCAgents/SalesValidationAgent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Sales Validation Agent

A sample **third-party agent** extension for Microsoft Dynamics 365 Business Central that validates and processes sales orders by checking inventory reservation and releasing eligible orders to the warehouse. It's purpose is to demonstrate how the sample agent in Business Central can be productized into an app.

## Overview

The Sales Validation Agent is a Copilot-powered agent that automates the routine work of reviewing open sales orders for a given shipment date, verifying that inventory is reserved, and releasing qualifying orders. It is built on the Business Central **Agent** framework and is intended as a reference implementation for partners building their own agents.

---

## Project Structure

```
app/
├── Integration/ # Copilot capability registration & install logic
├── Interaction/ # User-facing pages and page extensions
└── Setup/ # Agent configuration, metadata, KPIs, and profile
├── KPI/ # Performance tracking (orders released)
├── Metadata/ # IAgentFactory / IAgentMetadata implementations
└── Profile/ # Role Center profile & page customizations
```

### Integration

Contains the install codeunit and enum extension that register the **Sales Validation Agent** as a Copilot capability. On first install the capability is registered with the platform; on subsequent installs or upgrades the agent's instructions are refreshed.

### Interaction

Provides the user-facing entry point for the agent. A page extension on the **Sales Order List** adds a *Validate with Agent* action that verifies the agent is active, prompts the user for a shipment date via a dialog page, and creates an `Agent Task` with the selected date.

### Setup

Holds the core configuration for the agent:

- **Setup table & page** – A lightweight table stores the agent's User Security ID (company-independent), and a `ConfigurationDialog` page lets admins provision or update the agent user with the correct profile and permissions. The Copilot capability must be enabled before the setup page can be opened.
- **Setup codeunit** – Central helper that resolves the agent user, supplies default profile and access controls (`D365 READ` + `D365 SALES`), loads instructions from a resource file, and ensures the setup record exists.

#### KPI

Tracks agent performance metrics. A KPI table records counters per agent user (currently *Orders Released*), exposed through a `CardPart` summary page. An event subscriber on `OnAfterReleaseSalesDoc` automatically increments the counter each time the agent releases a sales order, so metrics stay up to date without any manual bookkeeping.

#### Metadata

Implements the `IAgentFactory` and `IAgentMetadata` interfaces required by the Business Central Agent framework. The factory provides default initials, the setup page, the Copilot capability, the default profile, and access control templates. The `ShowCanCreateAgent` method controls UI visibility for agent creation. The agent here is single-instance. The metadata codeunit supplies page IDs for setup, summary, and task message cards. An enum extension on `Agent Metadata Provider` wires these implementations into the platform.

#### Profile

Defines a dedicated **Sales Validation Agent (Copilot)** profile based on the *Order Processor Role Center*. Accompanying page customizations tailor the Role Center, Sales Order card, Sales Order List, Sales Order Statistics, Sales Order Subform, and SO Processor Activities pages to present only the information the agent needs.

---

## How It Works

1. **Create the agent** – The *Sales Validation Agent* Copilot capability is registered as **Preview** and is therefore enabled by default. Open the *Sales Val. Agent Setup* configuration dialog (accessible from the agent avatar in the Role Center) to provision the agent user with the correct profile and permissions.
2. **Assign a task** – From the **Sales Order List**, choose *Validate with Agent*, pick a shipment date, and a task is created for the agent.
3. **Agent processes orders** – The agent reads its instructions (loaded from `Instructions/InstructionsV1.txt`), validates open sales orders for the specified shipment date, checks inventory reservation, and releases eligible orders.
4. **KPIs are tracked** – Each time the agent releases an order, the `OnAfterReleaseSalesDoc` event subscriber increments the *Orders Released* counter, visible on the agent's summary page.

---

## Prerequisites

- Business Central **application version 27.4** or later
- The **Copilot & AI Capabilities** feature enabled in the environment
- Appropriate licensing for Copilot / Agent functionality
35 changes: 35 additions & 0 deletions samples/BCAgents/SalesValidationAgent/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"id": "5878e09e-13b4-4058-a9f6-4a9ee1605d6a",
"name": "Sales Validation Agent Sample",
"publisher": "Default Publisher",
"version": "1.0.0.0",
"brief": "",
"description": "",
"privacyStatement": "",
"EULA": "",
"help": "",
"url": "",
"logo": "",
"dependencies": [],
"screenshots": [],
"platform": "1.0.0.0",
"application": "27.4.0.0",
"idRanges": [
{
"from": 50100,
"to": 50149
}
],
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": true,
"includeSourceInSymbolFile": true,
"applyToDevExtension": false
},
"runtime": "17.0",
"features": [
"NoImplicitWith"
],
"resourceFolders": [".resources"],
"target": "Cloud"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace SalesValidationAgent.Integration;

using SalesValidationAgent.Setup;
using System.Agents;
using System.AI;
using System.Security.AccessControl;

codeunit 50101 "Sales Val. Agent Install"
{
Subtype = Install;
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;

trigger OnInstallAppPerDatabase()
var
SalesValAgentSetupRec: Record "Sales Val. Agent Setup";
begin
RegisterCapability();

if not SalesValAgentSetupRec.FindSet() then
exit;

repeat
InstallAgent(SalesValAgentSetupRec);
until SalesValAgentSetupRec.Next() = 0;
end;

local procedure InstallAgent(var SalesValAgentSetupRec: Record "Sales Val. Agent Setup")
begin
InstallAgentInstructions(SalesValAgentSetupRec);
end;

local procedure InstallAgentInstructions(var SalesValAgentSetupRec: Record "Sales Val. Agent Setup")
var
Agent: Codeunit Agent;
SalesValAgentSetup: Codeunit "Sales Val. Agent Setup";
begin
Agent.SetInstructions(SalesValAgentSetupRec."User Security ID", SalesValAgentSetup.GetInstructions());
end;

local procedure RegisterCapability()
var
CopilotCapability: Codeunit "Copilot Capability";
LearnMoreUrlTxt: Label 'https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/ai/ai-development-toolkit-sales-validation', Locked = true;
begin
if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"Sales Validation Agent") then
CopilotCapability.RegisterCapability(
Enum::"Copilot Capability"::"Sales Validation Agent",
Enum::"Copilot Availability"::Preview,
"Copilot Billing Type"::"Microsoft Billed",
LearnMoreUrlTxt);
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace SalesValidationAgent.Integration;

using System.AI;

enumextension 50100 "Sales Val. Copilot Capability" extends "Copilot Capability"
{
value(50100; "Sales Validation Agent")
{
Caption = 'Sales Validation Agent';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace SalesValidationAgent.Interaction;

/// <summary>
/// A simple dialog page that prompts the user to select a shipment date
/// for the Sales Validation Agent to process open sales orders.
/// </summary>
page 50102 "Sales Val. Agent Date Picker"
{
PageType = StandardDialog;
Caption = 'Select Shipment Date';

layout
{
area(Content)
{
field(ShipmentDate; ShipmentDate)
{
Caption = 'Shipment Date';
ToolTip = 'Specify the shipment date to validate sales orders for.';
ApplicationArea = All;
}
}
}

var
ShipmentDate: Date;

procedure GetShipmentDate(): Date
begin
exit(ShipmentDate);
end;

procedure SetShipmentDate(NewDate: Date)
begin
ShipmentDate := NewDate;
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace SalesValidationAgent.Interaction;

using Microsoft.Sales.Document;
using SalesValidationAgent.Setup;
using System.Agents;

/// <summary>
/// Extends the Sales Order List page to enable Sales Validation Agent task assignment.
/// Adds an action that allows users to trigger the agent to validate and process open sales orders.
/// </summary>
pageextension 50101 "Sales Val. Agent Sales Orders" extends "Sales Order List"
{
actions
{
addlast(processing)
{
action(ValidateWithAgent)
{
Caption = 'Validate with Agent';
ToolTip = 'Assign a validation task to the Sales Validation Agent to process open orders for a specific date.';
Image = Task;
ApplicationArea = All;

trigger OnAction()
var
AgentTask: Record "Agent Task";
Agent: Codeunit Agent;
AgentTaskBuilder: Codeunit "Agent Task Builder";
SalesValAgentSetup: Codeunit "Sales Val. Agent Setup";
DatePicker: Page "Sales Val. Agent Date Picker";
AgentUserSecurityId: Guid;
TaskTitle: Text[150];
From: Text[250];
Message: Text;
ShipmentDate: Date;
begin
if not SalesValAgentSetup.TryGetAgent(AgentUserSecurityId) then
Error(SVAgentDoesNotExistErr);

if not Agent.IsActive(AgentUserSecurityId) then
Error(SVAgentNotActiveErr);

DatePicker.SetShipmentDate(WorkDate());
if DatePicker.RunModal() <> Action::OK then
exit;

ShipmentDate := DatePicker.GetShipmentDate();
if ShipmentDate = 0D then
Error(ShipmentDateRequiredErr);

Message := StrSubstNo(TaskMessageLbl, ShipmentDate);
TaskTitle := CopyStr(StrSubstNo(TaskTitleLbl, ShipmentDate), 1, MaxStrLen(TaskTitle));
From := CopyStr(UserId(), 1, MaxStrLen(From));

AgentTask := AgentTaskBuilder.Initialize(AgentUserSecurityId, TaskTitle)
.AddTaskMessage(From, Message)
.Create();

Message(TaskAssignedMsg, AgentTask.ID, ShipmentDate);
end;
}
}
}

var
SVAgentDoesNotExistErr: Label 'The Sales Validation Agent has not been created.';
SVAgentNotActiveErr: Label 'The Sales Validation Agent is not active.';
ShipmentDateRequiredErr: Label 'A shipment date must be specified.';
TaskMessageLbl: Label 'Run and process shipment date %1.', Locked = true, Comment = '%1 = Shipment Date';
TaskTitleLbl: Label 'Validate Sales Orders for %1', Comment = '%1 = Shipment Date';
TaskAssignedMsg: Label 'Task %1 assigned successfully to validate sales orders for date: %2.', Comment = '%1 = Task ID, %2 = Shipment Date';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace SalesValidationAgent.Setup.KPI;

page 50104 "Sales Val. Agent KPI"
{
PageType = CardPart;
ApplicationArea = All;
UsageCategory = Administration;
Caption = 'Sales Validation Agent Summary';
SourceTable = "Sales Val. Agent KPI";
Editable = false;
Extensible = false;

layout
{
area(Content)
{
cuegroup(KeyMetrics)
{
Caption = 'Key Performance Indicators';

field(OrdersReleased; Rec."Orders Released")
{
Caption = 'Orders Released';
ToolTip = 'Specifies the number of sales orders released by the agent.';
}
}
}
}

trigger OnOpenPage()
begin
GetRelevantAgent();
end;

/// <summary>
/// Retrieves the relevant agent's KPI record for display.
/// This page is launched via IAgentMetadata.GetSummaryPageId(). The platform sets a filter on the
/// "User Security ID" field before opening the page, so the source record may not be fully populated
/// on open - the filter is evaluated here to resolve and load the correct record.
/// </summary>
local procedure GetRelevantAgent()
var
UserSecurityIDFilter: Text;
begin
if IsNullGuid(Rec."User Security ID") then begin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should have a comment explaining how that page behaves and is launched.
There's a convention that the source record must have a User Security ID Guid field and that the record is not fully populated. A comment would explain your code.

UserSecurityIDFilter := Rec.GetFilter("User Security ID");
if not Evaluate(Rec."User Security ID", UserSecurityIDFilter) then
Error(AgentDoesNotExistErr);
end;

if not Rec.Get(Rec."User Security ID") then
Rec.Insert();
end;

var
AgentDoesNotExistErr: Label 'The agent does not exist. Please check the configuration.';
}
Loading