Skip to content
Open
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
151 changes: 151 additions & 0 deletions samples/BCAgents/AgentEmailIntegration/README.md
Original file line number Diff line number Diff line change
@@ -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.

40 changes: 40 additions & 0 deletions samples/BCAgents/AgentEmailIntegration/app.json
Original file line number Diff line number Diff line change
@@ -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"
}
76 changes: 76 additions & 0 deletions samples/BCAgents/AgentEmailIntegration/src/Sample.Codeunit.al
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Determines whether the scheduled agent task should run for the given setup.
/// </summary>
/// <param name="Setup">Setup record.</param>
/// <returns>True if the agent is active; otherwise false.</returns>
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;

/// <summary>
/// Schedules the next dispatcher execution.
/// </summary>
/// <param name="Setup">Setup record.</param>
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;

/// <summary>
/// Cancels any previously scheduled tasks and clears task IDs on the setup record.
/// </summary>
/// <param name="Setup">Setup record containing scheduled task IDs.</param>
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;

/// <summary>
/// Returns the delay (in milliseconds) used when scheduling the next run.
/// </summary>
/// <returns>Delay in milliseconds.</returns>
local procedure ScheduleDelay(): Integer
begin
exit(2 * 60 * 1000); // 2 minutes
end;
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Executes one agent cycle: retrieves emails, sends replies, and schedules the next run.
/// </summary>
/// <param name="Setup">Agent setup record.</param>
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;

/// <summary>
/// Persists the last successful synchronization datetime on the setup record.
/// </summary>
/// <param name="Setup">Setup record to update.</param>
/// <param name="LastSync">Datetime to store as the last sync time.</param>
local procedure UpdateLastSync(var Setup: Record "Sample Setup"; LastSync: DateTime)
begin
Setup.GetBySystemId(Setup.SystemId);
Setup."Last Sync At" := LastSync;
Setup.Modify();
Commit();
end;

}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Handles dispatcher failures by rescheduling the next run.
/// </summary>
/// <param name="Setup">Agent setup record.</param>
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;
}
Loading