diff --git a/samples/BCAgents/AgentEmailIntegration/README.md b/samples/BCAgents/AgentEmailIntegration/README.md
new file mode 100644
index 00000000..0aa4b2fa
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/README.md
@@ -0,0 +1,151 @@
+
+# Sample Agent + Email Integration (Business Central)
+
+This AL extension is a small **sample** showing how to connect **Agents** with **Email** in Microsoft Dynamics 365 Business Central.
+
+It uses a recurring **scheduled task** to:
+
+1. Retrieve inbound emails from a configured email account.
+2. Create (or append to) **Agent Tasks** based on the email conversation.
+3. Send email replies for agent outputs that have been **Reviewed**.
+
+## How it works
+
+```mermaid
+sequenceDiagram
+ participant TS as TaskScheduler
+ participant D as Sample Dispatcher
+ participant A as Agent
+ participant RE as Sample Retrieve Emails
+ participant E as Email
+ participant ATS as Agent Task
+ participant SR as Sample Send Replies
+ participant EM as Email Message
+
+ TS->>D: Run
+ D->>A: IsActive(Agent)
+ alt ShouldRun = false (Inactive)
+ D-->>TS: Exit
+ else ShouldRun = true
+ D->>RE: Retrieve emails
+ RE->>E: Retrieve emails
+ loop For each retrieved Email Inbox record
+ RE->>ATS: TaskExists(Conversation Id)?
+ alt Existing task
+ RE->>ATS: Append Agent Task Message
+ Note over RE,ATS: External ID = External Message Id
+ else New task
+ RE->>ATS: Create Agent Task + initial message
+ Note over RE,ATS: External ID = Conversation Id
+ end
+ RE->>E: Mark email as read
+ end
+
+ D->>SR: SendEmailReplies(Setup)
+ loop For each reviewed output message
+ SR->>ATS: Get external message id
+ SR->>EM: CreateReply
+ SR->>E: Reply
+ SR->>ATS: Mark output message as Sent
+ end
+
+ D->>TS: Create task
+ D->>ATS: Update Last Sync At if retrevial success
+ end
+```
+
+```mermaid
+sequenceDiagram
+ participant TS as TaskScheduler
+ participant EH as Sample Error Handler
+ participant A as Agent
+
+ TS->>EH: Run
+ EH->>A: IsActive(Agent)
+ alt ShouldRun = false
+ EH-->>TS: Exit
+ else ShouldRun = true
+ EH->>TS: Create task
+ end
+```
+
+An “agent cycle” is executed by the scheduled task running codeunit `50100 "Sample Dispatcher"`:
+
+1. **ShouldRun check**
+ - `Sample Dispatcher` calls `Agent.IsActive(Setup."Agent User Security ID")` to decide whether to run.
+2. **Retrieve emails → Agent tasks**
+ - `50102 "Sample Retrieve Emails"` calls `Email.RetrieveEmails(...)`.
+ - For each email in `Email Inbox`:
+ - If an agent task already exists for the email **conversation**, the message is appended to the existing task.
+ - Otherwise, a new agent task is created.
+ - The email is marked as read via `Email.MarkAsRead(...)`.
+3. **Send replies**
+ - `50103 "Sample Send Replies"` finds agent task output messages with:
+ - `Type = Output`
+ - `Status = Reviewed`
+ - `Agent User Security ID = Setup."Agent User Security ID"`
+ - It creates a reply-all email (`Email Message.CreateReplyAll`) and sends it with `Email.ReplyAll(...)`.
+ - On success, the agent output message is marked as sent.
+4. **Reschedule**
+ - `Sample Dispatcher` schedules itself again using `TaskScheduler.CreateTask(...)`.
+ - The sample currently reschedules with a fixed delay of **1 minute**.
+
+If the dispatcher fails, `50101 "Sample Error Handler"` is used as the task error handler and will reschedule the next run.
+
+## Included objects
+
+- `table 50100 "Sample Setup"`
+ - Stores which agent and email account to use, sync timestamps, and scheduled task IDs.
+- `codeunit 50100 "Sample Dispatcher"`
+ - Orchestrates one cycle (retrieve emails, send replies, reschedule).
+- `codeunit 50101 "Sample Error Handler"`
+ - Reschedules the dispatcher if the scheduled task run fails.
+- `codeunit 50102 "Sample Retrieve Emails"`
+ - Retrieves inbound emails and creates/appends agent task messages.
+- `codeunit 50103 "Sample Send Replies"`
+ - Sends reply-all emails for reviewed agent outputs.
+
+## Setup (what you need to configure)
+
+This repo focuses on the integration logic and **does not include a setup page**.
+To run the sample you must create a `Sample Setup` record (`table 50100`) in your own way (for example: add a small page in a companion extension, insert the record from a one-off helper codeunit, or create it during development/debugging).
+
+Populate:
+
+- `Agent User Security ID`
+ - The user security ID of the agent user.
+- `Email Account ID`
+ - The email account to retrieve from and reply with.
+- `Email Connector`
+ - The connector type used by the email account.
+
+These are used behind the scenes for tracking sychronizations:
+
+- `Earliest Sync At`
+ - Used as the lower bound when retrieving emails.
+- `Last Sync At`
+ - Updated after a successful dispatcher run.
+
+## Important note: scheduled task user + email access
+
+Business Central scheduled tasks run under a **user context** (the user who scheduled/created the task).
+
+That means:
+
+- The user who schedules the dispatcher must have permission to run this extension’s objects.
+- The user must have access to the selected email account/connector.
+ If the user does not have access, email retrieval/reply operations can fail.
+
+## Notes / limitations (by design for a sample)
+
+- The dispatcher’s “should run” logic is currently minimal (it only checks `Agent.IsActive(...)`).
+- Email retrieval uses a simple filter: unread only, attachments loaded, last message only, earliest email = `Earliest Sync At`, max 50.
+- Attachment MIME filtering is marked as TODO in `Sample Retrieve Emails`.
+
+## Quick start (developer flow)
+
+1. Publish the extension.
+2. Configure Email in Business Central (email account + connector).
+3. Schedule with the Schedule Sync action.
+4. Send an email to the email account selected.
+
diff --git a/samples/BCAgents/AgentEmailIntegration/app.json b/samples/BCAgents/AgentEmailIntegration/app.json
new file mode 100644
index 00000000..7c02bd8c
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/app.json
@@ -0,0 +1,40 @@
+{
+ "id": "db9ab3df-d57d-47fb-8870-f7617f9341aa",
+ "name": "Sample Agent Email Integration",
+ "publisher": "Microsoft",
+ "version": "1.0.0.0",
+ "brief": "",
+ "description": "",
+ "privacyStatement": "",
+ "EULA": "",
+ "help": "",
+ "url": "",
+ "logo": "",
+ "dependencies": [
+ {
+ "id": "00155c68-8cdd-4d60-a451-2034ad094223",
+ "name": "Business Central Agent Playground",
+ "publisher": "Microsoft",
+ "version": "27.2.0.0"
+ }
+ ],
+ "screenshots": [],
+ "platform": "1.0.0.0",
+ "application": "27.0.0.0",
+ "idRanges": [
+ {
+ "from": 50100,
+ "to": 50149
+ }
+ ],
+ "resourceExposurePolicy": {
+ "allowDebugging": true,
+ "allowDownloadingSource": true,
+ "includeSourceInSymbolFile": true
+ },
+ "runtime": "16.0",
+ "features": [
+ "NoImplicitWith"
+ ],
+ "target":"Cloud"
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al
new file mode 100644
index 00000000..7be8eafa
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al
@@ -0,0 +1,76 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+using System.Agents;
+
+codeunit 50104 "Sample"
+{
+
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ ///
+ /// Determines whether the scheduled agent task should run for the given setup.
+ ///
+ /// Setup record.
+ /// True if the agent is active; otherwise false.
+ procedure ShouldRun(Setup: Record "Sample Setup"): Boolean
+ var
+ Agent: Codeunit Agent;
+ begin
+ // TODO: Implement validation logic to determine if the agent should run
+ if not Agent.IsActive(Setup."Agent User Security ID") then
+ exit(false);
+
+ exit(true);
+ end;
+
+ ///
+ /// Schedules the next dispatcher execution.
+ ///
+ /// Setup record.
+ procedure ScheduleNextRun(Setup: Record "Sample Setup")
+ begin
+ // Ensure no other process can change the setup while we are scheduling the task
+ Setup.LockTable();
+ // Ensure we have the latest record before modifying
+ Setup.GetBySystemId(Setup.SystemId);
+
+ // Remove existing scheduled task if any before rescheduling
+ RemoveScheduledTask(Setup);
+
+ Setup."Scheduled Task ID" := TaskScheduler.CreateTask(Codeunit::"Sample Dispatcher", Codeunit::"Sample Error Handler", true, CompanyName(), CurrentDateTime() + ScheduleDelay(), Setup.RecordId);
+
+ Setup.Modify();
+ Commit();
+ end;
+
+ ///
+ /// Cancels any previously scheduled tasks and clears task IDs on the setup record.
+ ///
+ /// Setup record containing scheduled task IDs.
+ procedure RemoveScheduledTask(Setup: Record "Sample Setup")
+ var
+ NullGuid: Guid;
+ begin
+ if TaskScheduler.TaskExists(Setup."Scheduled Task ID") then
+ TaskScheduler.CancelTask(Setup."Scheduled Task ID");
+
+ Setup."Scheduled Task ID" := NullGuid;
+ Setup.Modify();
+ end;
+
+ ///
+ /// Returns the delay (in milliseconds) used when scheduling the next run.
+ ///
+ /// Delay in milliseconds.
+ local procedure ScheduleDelay(): Integer
+ begin
+ exit(2 * 60 * 1000); // 2 minutes
+ end;
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al
new file mode 100644
index 00000000..11ece333
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleDispatcher.Codeunit.al
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+using System.Agents;
+
+codeunit 50100 "Sample Dispatcher"
+{
+
+ Access = Internal;
+ TableNo = "Sample Setup";
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ trigger OnRun()
+ begin
+ RunSampleAgent(Rec);
+ end;
+
+ ///
+ /// Executes one agent cycle: retrieves emails, sends replies, and schedules the next run.
+ ///
+ /// Agent setup record.
+ procedure RunSampleAgent(Setup: Record "Sample Setup")
+ var
+ Sample: Codeunit "Sample";
+ LastSync: DateTime;
+ RetrievalSuccess: Boolean;
+ begin
+ // Validate task should still run
+ if not Sample.ShouldRun(Setup) then
+ exit;
+
+ LastSync := CurrentDateTime();
+
+ // Sync emails
+ RetrievalSuccess := Codeunit.Run(Codeunit::"Sample Retrieve Emails", Setup);
+ Codeunit.Run(Codeunit::"Sample Send Replies", Setup);
+
+ // Reschedule next run
+ Sample.ScheduleNextRun(Setup);
+
+ if RetrievalSuccess then
+ UpdateLastSync(Setup, LastSync);
+ end;
+
+ ///
+ /// Persists the last successful synchronization datetime on the setup record.
+ ///
+ /// Setup record to update.
+ /// Datetime to store as the last sync time.
+ local procedure UpdateLastSync(var Setup: Record "Sample Setup"; LastSync: DateTime)
+ begin
+ Setup.GetBySystemId(Setup.SystemId);
+ Setup."Last Sync At" := LastSync;
+ Setup.Modify();
+ Commit();
+ end;
+
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al
new file mode 100644
index 00000000..4481404b
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleErrorHandler.Codeunit.al
@@ -0,0 +1,36 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+codeunit 50101 "Sample Error Handler"
+{
+
+ Access = Internal;
+ TableNo = "Sample Setup";
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ trigger OnRun()
+ begin
+ RunSampleAgent(Rec);
+ end;
+
+ ///
+ /// Handles dispatcher failures by rescheduling the next run.
+ ///
+ /// Agent setup record.
+ procedure RunSampleAgent(Setup: Record "Sample Setup")
+ var
+ Sample: Codeunit "Sample";
+ begin
+ // Validate task should still run
+ if not Sample.ShouldRun(Setup) then
+ exit;
+
+ // Reschedule run
+ Sample.ScheduleNextRun(Setup);
+ end;
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al
new file mode 100644
index 00000000..82553715
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleRetrieveEmails.Codeunit.al
@@ -0,0 +1,215 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+using System.Agents;
+using System.Email;
+
+codeunit 50102 "Sample Retrieve Emails"
+{
+
+ Access = Internal;
+ TableNo = "Sample Setup";
+ InherentEntitlements = X;
+ InherentPermissions = X;
+ Permissions = tabledata "Email Inbox" = rd;
+
+ var
+ AgentTaskTitleLbl: Label 'Email from %1', Comment = '%1 = Sender Name';
+ MessageTemplateLbl: Label 'Subject: %1
Body: %2', Comment = '%1 = Subject, %2 = Body';
+
+ trigger OnRun()
+ begin
+ RunRetrieveEmails(Rec);
+ end;
+
+ ///
+ /// Retrieves emails and creates/updates agent tasks.
+ ///
+ /// Agent setup record containing email account configuration.
+ procedure RunRetrieveEmails(Setup: Record "Sample Setup")
+ var
+ EmailInbox: Record "Email Inbox";
+ TempFilters: Record "Email Retrieval Filters" temporary;
+ Email: Codeunit "Email";
+ LastSync: DateTime;
+ begin
+ LastSync := CurrentDateTime();
+
+ SetEmailFilters(Setup, TempFilters);
+ Email.RetrieveEmails(Setup."Email Account ID", Setup."Email Connector", EmailInbox, TempFilters);
+
+ if EmailInbox.FindSet() then;
+
+ repeat
+ // Process each email as needed
+ AddEmailToAgentTask(Setup, EmailInbox);
+ until EmailInbox.Next() = 0;
+
+ UpdateEarliestSyncTime(Setup, EmailInbox.Count(), LastSync);
+ Commit();
+ end;
+
+ ///
+ /// Updates the earliest sync datetime used for the next retrieval window.
+ ///
+ /// Setup record.
+ /// Number of emails retrieved/processed in this run.
+ /// Datetime of the last synchronization.
+ local procedure UpdateEarliestSyncTime(var Setup: Record "Sample Setup"; EmailCount: Integer; LastSync: DateTime)
+ begin
+ Setup.GetBySystemId(Setup.SystemId);
+
+ // Only update the earliest sync time if we processed the fewer emails than the limit
+ // This ensures we don't miss any emails in case there are more emails to process
+ if EmailCount < MaxNoOfEmailsToProcess() then
+ Setup."Earliest Sync At" := LastSync;
+ Setup.Modify();
+ end;
+
+ ///
+ /// Returns the maximum number of emails to retrieve/process per run.
+ ///
+ /// Maximum number of emails to process.
+ local procedure MaxNoOfEmailsToProcess(): Integer
+ begin
+ exit(50);
+ end;
+
+ ///
+ /// Builds the email retrieval filters.
+ ///
+ /// Setup record.
+ /// Temporary retrieval filters record to populate.
+ local procedure SetEmailFilters(Setup: Record "Sample Setup"; var TempFilters: Record "Email Retrieval Filters" temporary)
+ begin
+ TempFilters."Unread Emails" := true;
+ TempFilters."Load Attachments" := true;
+ TempFilters."Last Message Only" := true;
+ TempFilters."Earliest Email" := Setup."Earliest Sync At";
+ TempFilters."Max No. of Emails" := MaxNoOfEmailsToProcess();
+ TempFilters.Insert();
+ end;
+
+ ///
+ /// Adds a retrieved email to an agent task (existing or new task).
+ ///
+ /// Setup record.
+ /// Current email inbox record to process.
+ local procedure AddEmailToAgentTask(Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox")
+ var
+ AgentTaskBuilder: Codeunit "Agent Task Builder";
+ begin
+ if AgentTaskBuilder.TaskExists(Setup."Agent User Security ID", EmailInbox."Conversation Id") then
+ AddEmailToExistingTask(EmailInbox)
+ else
+ AddEmailToNewAgentTask(Setup, EmailInbox);
+
+ MarkEmailAsProcessed(Setup, EmailInbox);
+ end;
+
+ ///
+ /// Appends an email as a new message on an existing agent task.
+ ///
+ /// Email to be appended.
+ local procedure AddEmailToExistingTask(var EmailInbox: Record "Email Inbox")
+ var
+ AgentTaskRecord: Record "Agent Task";
+ AgentTaskMessage: Record "Agent Task Message";
+ AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder";
+ EmailMessage: Codeunit "Email Message";
+ MessageText: Text;
+ begin
+ AgentTaskRecord.ReadIsolation(IsolationLevel::ReadCommitted);
+ AgentTaskRecord.SetRange("External ID", EmailInbox."Conversation Id");
+ if not AgentTaskRecord.FindFirst() then
+ exit;
+
+ AgentTaskMessage.ReadIsolation(IsolationLevel::ReadCommitted);
+ AgentTaskMessage.SetRange("Task ID", AgentTaskRecord.ID);
+ AgentTaskMessage.SetRange("External ID", EmailInbox."External Message Id");
+ if AgentTaskMessage.Count() >= 1 then
+ exit;
+
+ EmailMessage.Get(EmailInbox."Message Id");
+ MessageText := StrSubstNo(MessageTemplateLbl, EmailMessage.GetSubject(), EmailMessage.GetBody());
+
+ AgentTaskMessageBuilder.Initialize(EmailInbox."Sender Address", MessageText)
+ .SetMessageExternalID(EmailInbox."External Message Id")
+ .SetIgnoreAttachment(false)
+ .SetAgentTask(AgentTaskRecord);
+
+ AddEmailAttachmentsToTaskMessage(EmailMessage, AgentTaskMessageBuilder);
+ AgentTaskMessageBuilder.Create();
+ end;
+
+ ///
+ /// Creates a new agent task and adds the email as the initial task message.
+ ///
+ /// Setup record.
+ /// Email used to create the task.
+ local procedure AddEmailToNewAgentTask(Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox")
+ var
+ AgentTaskRecord: Record "Agent Task";
+ AgentTaskBuilder: Codeunit "Agent Task Builder";
+ AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder";
+ EmailMessage: Codeunit "Email Message";
+ MessageText: Text;
+ AgentTaskTitle: Text[150];
+ begin
+ EmailMessage.Get(EmailInbox."Message ID");
+ MessageText := StrSubstNo(MessageTemplateLbl, EmailMessage.GetSubject(), EmailMessage.GetBody());
+ AgentTaskTitle := CopyStr(StrSubstNo(AgentTaskTitleLbl, EmailInbox."Sender Name"), 1, MaxStrLen(AgentTaskRecord.Title));
+
+ AgentTaskMessageBuilder.Initialize(EmailInbox."Sender Address", MessageText)
+ .SetMessageExternalID(EmailInbox."External Message ID")
+ .SetIgnoreAttachment(false);
+
+ AgentTaskBuilder.Initialize(Setup."Agent User Security ID", AgentTaskTitle)
+ .SetExternalId(EmailInbox."Conversation Id")
+ .AddTaskMessage(AgentTaskMessageBuilder);
+
+ AddEmailAttachmentsToTaskMessage(EmailMessage, AgentTaskMessageBuilder);
+ AgentTaskBuilder.Create();
+ end;
+
+ ///
+ /// Marks the processed email as read in the external mailbox.
+ ///
+ /// Setup record.
+ /// Email containing the external message identifier.
+ local procedure MarkEmailAsProcessed(var Setup: Record "Sample Setup"; var EmailInbox: Record "Email Inbox")
+ var
+ Email: Codeunit "Email";
+ begin
+ Email.MarkAsRead(Setup."Email Account ID", Setup."Email Connector", EmailInbox."External Message Id");
+ end;
+
+ ///
+ /// Adds all email attachments to the agent task message being built.
+ ///
+ /// Email message containing attachments to add.
+ /// Builder instance to add the attachments to.
+ local procedure AddEmailAttachmentsToTaskMessage(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessageBuilder: Codeunit "Agent Task Message Builder")
+ var
+ InStream: InStream;
+ FileMIMEType: Text[100];
+ IsFileMimeTypeSupported: Boolean;
+ Ignore: Boolean;
+ begin
+ if not EmailMessage.Attachments_First() then
+ exit;
+
+ repeat
+ EmailMessage.Attachments_GetContent(InStream);
+ FileMIMEType := CopyStr(EmailMessage.Attachments_GetContentType(), 1, 100);
+ // TODO: Add logic to check for supported MIME types
+ IsFileMimeTypeSupported := true; // Placeholder for actual MIME type check
+ Ignore := IsFileMimeTypeSupported; // Placeholder for actual logic to determine if attachment should be ignored even if file type is supported
+ AgentTaskMessageBuilder.AddAttachment(EmailMessage.Attachments_GetName(), FileMIMEType, InStream, Ignore);
+ until EmailMessage.Attachments_Next() = 0;
+ end;
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al
new file mode 100644
index 00000000..6faed3aa
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSendReplies.Codeunit.al
@@ -0,0 +1,120 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+using System.Agents;
+using System.Email;
+
+
+codeunit 50103 "Sample Send Replies"
+{
+
+ Access = Internal;
+ TableNo = "Sample Setup";
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ trigger OnRun()
+ begin
+ SendEmailReplies(Rec);
+ end;
+
+ var
+ AllSentSuccessfully: Boolean;
+ EmailSubjectTxt: Label 'Agent reply to task %1', Comment = '%1 = Agent Task id';
+
+ ///
+ /// Sends email replies for reviewed output messages and marks them as sent.
+ ///
+ /// Setup record.
+ procedure SendEmailReplies(Setup: Record "Sample Setup")
+ var
+ OutputAgentTaskMessage: Record "Agent Task Message";
+ InputAgentTaskMessage: Record "Agent Task Message";
+ AgentMessage: Codeunit "Agent Message";
+ begin
+ AllSentSuccessfully := true;
+
+ OutputAgentTaskMessage.ReadIsolation(IsolationLevel::ReadCommitted);
+ OutputAgentTaskMessage.SetRange(Status, OutputAgentTaskMessage.Status::Reviewed);
+ OutputAgentTaskMessage.SetRange(Type, OutputAgentTaskMessage.Type::Output);
+ OutputAgentTaskMessage.SetRange("Agent User Security ID", Setup."Agent User Security ID");
+
+ if not OutputAgentTaskMessage.FindSet() then
+ exit;
+
+ repeat
+ if not InputAgentTaskMessage.Get(OutputAgentTaskMessage."Task ID", OutputAgentTaskMessage."Input Message ID") then
+ continue;
+
+ if InputAgentTaskMessage."External ID" = '' then
+ continue;
+
+ if TryReply(Setup, InputAgentTaskMessage, OutputAgentTaskMessage) then
+ AgentMessage.SetStatusToSent(OutputAgentTaskMessage)
+ else
+ AllSentSuccessfully := false;
+ until OutputAgentTaskMessage.Next() = 0;
+ Commit();
+ end;
+
+ ///
+ /// Returns whether all replies were sent successfully in the last run.
+ ///
+ /// True if all replies were sent successfully; otherwise false.
+ procedure GetAllSentSuccessfully(): Boolean
+ begin
+ exit(AllSentSuccessfully);
+ end;
+
+ ///
+ /// Attempts to create and send a reply email for an agent task output message.
+ ///
+ /// Setup record.
+ /// The original inbound message being replied to.
+ /// The outbound agent message to send.
+ /// True if the email was sent successfully; otherwise false.
+ local procedure TryReply(Setup: Record "Sample Setup"; var InputAgentTaskMessage: Record "Agent Task Message"; var OutputAgentTaskMessage: Record "Agent Task Message"): Boolean
+ var
+ AgentMessage: Codeunit "Agent Message";
+ Email: Codeunit Email;
+ EmailMessage: Codeunit "Email Message";
+ Body: Text;
+ Subject: Text;
+ begin
+ Subject := StrSubstNo(EmailSubjectTxt, InputAgentTaskMessage."Task ID");
+ Body := AgentMessage.GetText(OutputAgentTaskMessage);
+ EmailMessage.CreateReplyAll(Subject, Body, true, InputAgentTaskMessage."External ID");
+ AddMessageAttachments(EmailMessage, OutputAgentTaskMessage);
+
+ exit(Email.ReplyAll(EmailMessage, Setup."Email Account ID", Setup."Email Connector"));
+ end;
+
+ ///
+ /// Adds agent task message attachments to the email message being sent.
+ ///
+ /// Email message to add attachments to.
+ /// Task message with the attachments to be included.
+ local procedure AddMessageAttachments(var EmailMessage: Codeunit "Email Message"; var AgentTaskMessage: Record "Agent Task Message")
+ var
+ AgentTaskFile: Record "Agent Task File";
+ AgentTaskMessageAttachment: Record "Agent Task Message Attachment";
+ AgentTaskFileInStream: InStream;
+ begin
+ AgentTaskMessageAttachment.SetRange("Task ID", AgentTaskMessage."Task ID");
+ AgentTaskMessageAttachment.SetRange("Message ID", AgentTaskMessage.ID);
+ if not AgentTaskMessageAttachment.FindSet() then
+ exit;
+
+ repeat
+ if not AgentTaskFile.Get(AgentTaskMessageAttachment."Task ID", AgentTaskMessageAttachment."File ID") then
+ exit;
+
+ AgentTaskFile.CalcFields(Content);
+ AgentTaskFile.Content.CreateInStream(AgentTaskFileInStream, TextEncoding::UTF8);
+ EmailMessage.AddAttachment(AgentTaskFile."File Name", AgentTaskFile."File MIME Type", AgentTaskFileInStream);
+ until AgentTaskMessageAttachment.Next() = 0;
+ end;
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al
new file mode 100644
index 00000000..46c0180f
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Page.al
@@ -0,0 +1,160 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+using System.Agents;
+using System.Email;
+using System.Agents.Playground.CustomAgent;
+using System.Threading;
+
+page 50100 "Sample Setup"
+{
+ PageType = Card;
+ ApplicationArea = All;
+ UsageCategory = Administration;
+ SourceTable = "Sample Setup";
+
+ layout
+ {
+ area(Content)
+ {
+ group(GroupName)
+ {
+ field(Name; AgentName)
+ {
+ Caption = 'Agent';
+ ApplicationArea = All;
+ Editable = false;
+
+ trigger OnAssistEdit()
+ var
+ CustomAgentInfo: Record "Custom Agent Info";
+ CustomAgent: Codeunit "Custom Agent";
+ begin
+ Rec."Agent User Security ID" := SelectAgents(); // TODO: BCApps change required
+
+ CustomAgent.GetCustomAgentById(Rec."Agent User Security ID", CustomAgentInfo);
+ AgentName := CustomAgentInfo."User Name";
+ end;
+ }
+
+ field(EmailAccount; EmailAccountName)
+ {
+ Caption = 'Email Account';
+ ApplicationArea = All;
+ Editable = false;
+
+ trigger OnAssistEdit()
+ var
+ TempEmailAccount: Record "Email Account" temporary;
+ EmailAccounts: Page "Email Accounts";
+ begin
+ if not CheckMailboxExists() then
+ Page.RunModal(Page::"Email Account Wizard");
+
+ if not CheckMailboxExists() then
+ exit;
+
+ EmailAccounts.EnableLookupMode();
+ EmailAccounts.FilterConnectorV4Accounts(true);
+ if EmailAccounts.RunModal() = Action::LookupOK then begin
+ EmailAccounts.GetAccount(TempEmailAccount);
+ Rec."Email Account ID" := TempEmailAccount."Account Id";
+ Rec."Email Connector" := TempEmailAccount.Connector;
+ Rec."Email Address" := TempEmailAccount."Email Address";
+ EmailAccountName := TempEmailAccount."Email Address";
+ end;
+ end;
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action(ScheduleSync)
+ {
+ Caption = 'Schedule Sync';
+ ApplicationArea = All;
+
+ trigger OnAction()
+ var
+ Sample: Codeunit "Sample";
+ begin
+ Sample.ScheduleNextRun(Rec);
+ end;
+ }
+ action(RemoveScheduledSync)
+ {
+ Caption = 'Remove Scheduled Sync';
+ ApplicationArea = All;
+
+ trigger OnAction()
+ var
+ Sample: Codeunit "Sample";
+ begin
+ Sample.RemoveScheduledTask(Rec);
+ end;
+ }
+ action(ScheduledTasks)
+ {
+ Caption = 'Scheduled Tasks';
+ ApplicationArea = All;
+
+ trigger OnAction()
+ var
+ ScheduledTask: Page "Scheduled Tasks";
+ begin
+ ScheduledTask.Run();
+ end;
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ var
+ CustomAgentInfo: Record "Custom Agent Info";
+ CustomAgent: Codeunit "Custom Agent";
+ begin
+ if not Rec.FindFirst() then begin
+ Rec.Init();
+ Rec.Insert();
+ end else begin
+ CustomAgent.GetCustomAgentById(Rec."Agent User Security ID", CustomAgentInfo);
+ AgentName := CustomAgentInfo."User Name";
+ EmailAccountName := Rec."Email Address";
+ end;
+ end;
+
+ trigger OnQueryClosePage(CloseAction: Action): Boolean
+ begin
+ Rec.Modify();
+ end;
+
+ var
+ AgentName: Text;
+ EmailAccountName: Text;
+
+
+ local procedure CheckMailboxExists(): Boolean
+ var
+ EmailAccounts: Record "Email Account";
+ EmailAccount: Codeunit "Email Account";
+ IConnector: Interface "Email Connector";
+ begin
+ EmailAccount.GetAllAccounts(false, EmailAccounts);
+ if EmailAccounts.IsEmpty() then
+ exit(false);
+
+ repeat
+ IConnector := EmailAccounts.Connector;
+ if IConnector is "Email Connector v4" then
+ exit(true);
+ until EmailAccounts.Next() = 0;
+ end;
+}
\ No newline at end of file
diff --git a/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al
new file mode 100644
index 00000000..5227cd7b
--- /dev/null
+++ b/samples/BCAgents/AgentEmailIntegration/src/SampleSetup.Table.al
@@ -0,0 +1,55 @@
+// ------------------------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+// ------------------------------------------------------------------------------------------------
+
+namespace Microsoft.Agent.Sample;
+
+using System.Email;
+
+table 50100 "Sample Setup"
+{
+ Access = Internal;
+ Extensible = false;
+ InherentEntitlements = RIMDX;
+ InherentPermissions = RIMDX;
+ DataClassification = SystemMetadata;
+
+ fields
+ {
+ field(1; Id; Integer)
+ {
+ AutoIncrement = true;
+ }
+ field(2; "Agent User Security ID"; Guid)
+ {
+ }
+ field(3; "Email Account ID"; Guid)
+ {
+ }
+ field(4; "Email Connector"; Enum "Email Connector")
+ {
+ }
+ field(5; "Email Address"; Text[2048])
+ {
+ }
+
+ field(6; "Last Sync At"; DateTime)
+ {
+ }
+ field(7; "Earliest Sync At"; DateTime)
+ {
+ }
+ field(8; "Scheduled Task ID"; Guid)
+ {
+ }
+ }
+
+ keys
+ {
+ key(Key1; Id)
+ {
+ Clustered = true;
+ }
+ }
+}
\ No newline at end of file