# Skill Agent Task Patterns

> Agent SDK task integration patterns for Business Central. Triggers on: Agent Task Builder, Agent Task Message Builder, Public API, AssignTask, ExternalId, agent session detection, BindSubscription, TryFunction agent task, multi-turn agent conversation, agent attachment, task lifecycle management, OnPrem-only methods, AddToTask, SetRequiresReview, SetSkipMessageSanitization, runtime availability.

- **Type:** Skill
- **Install:** `agentstack add skill-louage-frw-agentic-coding-skill-agent-task-patterns`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Louage](https://agentstack.voostack.com/s/louage)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Louage](https://github.com/Louage)
- **Source:** https://github.com/Louage/frw-agentic-coding/tree/main/assets/generated/aldc-community/skills/skill-agent-task-patterns

## Install

```sh
agentstack add skill-louage-frw-agentic-coding-skill-agent-task-patterns
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# BC Agent Task Integration Patterns

Knowledge base for every Agent SDK task integration pattern, with runtime-availability matrix and Extension-safe workarounds. For SDK architecture and the 3 interfaces, see `skill-agent-toolkit`.

**References**:
- [Tasks AL API](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/ai/ai-development-toolkit-tasks-api)
- [Agent SDK overview](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/ai/ai-development-toolkit-overview)

## API availability matrix (Runtime 17.0 / Platform 27.0)

**Critical**: not every documented method works in Extension scope today. Verify here before designing.

| Method                                       | Scope            | Status         | Workaround                                              |
| -------------------------------------------- | ---------------- | -------------- | ------------------------------------------------------- |
| `AddTaskMessage(From, Text)`                 | Extension        | ✅ Available    |, |
| `AddAttachment(Name, MediaType, InStream)`   | Extension        | ✅ Available    |, |
| `SetExternalId(Text)`                        | Extension        | ✅ Available    |, |
| `SetRequiresReview(true)`                    | **OnPrem only**  | ❌ Ext. blocked | Use `IAgentTaskExecution` Warning annotation            |
| `SetSkipMessageSanitization(true)`           | **OnPrem only**  | ❌ Ext. blocked | Pre-format message text upstream                        |
| `AddToTask(AgentTask)`                       | Future           | ❌ Not in 17.0  | Create a new follow-up task referencing original ExternalId |
| `Custom Agent.GetCustomAgents()`             | Future           | ❌ Not in 17.0  | Use `Agent Setup.OpenAgentLookup()`                     |

OnPrem-only methods are documented but throw at runtime when invoked from a cloud Extension. Treat the workarounds as the canonical Extension pattern.

## SDK codeunits, quick reference

| Codeunit                       | Purpose                                      | Key methods                                                |
| ------------------------------ | -------------------------------------------- | ---------------------------------------------------------- |
| `Agent Task Builder`           | Create new tasks (fluent API)                | `.Initialize(Guid, Text).SetExternalId(Text).AddTaskMessage(Text, Text).Create()` |
| `Agent Task Message Builder`   | Build messages with options                  | `.Initialize(From, Text)`, `.AddAttachment(Name, MediaType, InStream)`, `.AddToTask(Record)` |
| `Agent Task`                   | Query/manage existing tasks                  | `GetTaskByExternalId(Guid, Text)`, `CanSetStatusToReady(Record)`, `SetStatusToReady(Record)` |
| `Agent Message`                | Read/update message text                     | `GetText(Record)`, `UpdateText(Record, Text)`              |
| `Agent Setup`                  | Agent lookup + setup management              | `OpenAgentLookup(Enum, var Guid)`, `GetSetupRecord(...)`, `SaveChanges(...)` |
| `Agent`                        | Core agent operations                        | `SetInstructions(Guid, SecretText)`, `Deactivate(Guid)`, `IsActive(Guid)`, `GetDisplayName(Guid)`, `PopulateDefaultProfile(...)` |
| `Agent Session`                | Detect agent runtime context                 | `IsAgentSession(var Enum)`, `IsAgentSession(Enum)`, `GetCurrentSessionAgentTaskId()` |

## 8 integration patterns

### Pattern A, Public API (standard entry point)

**When**: every reusable task-creation surface. All other patterns call through here.

**Architecture**: Public codeunit (`Access = Public`) + Internal implementation codeunit.

**Contract**:

```al
// Overload 1: basic task
procedure AssignTask(AgentUserSecurityID: Guid; TaskTitle: Text[150]; From: Text[250]; Message: Text): Record "Agent Task"

// Overload 2: with ExternalId for tracking / multi-turn
procedure AssignTask(AgentUserSecurityID: Guid; TaskTitle: Text[150]; ExternalId: Text[2048]; From: Text[250]; Message: Text): Record "Agent Task"

// Overload 3: with attachments
procedure AssignTaskWithAttachment(AgentUserSecurityID: Guid; TaskTitle: Text[150]; From: Text[250]; Message: Text; var TempAttachments: Record "Agent Task File"): Record "Agent Task"

// Lifecycle
procedure Deactivate(AgentUserSecurityID: Guid)
procedure IsActive(AgentUserSecurityID: Guid): Boolean
```

**Implementation** (Impl codeunit, `Access = Internal`):

```al
local procedure AssignTaskInternal(
    AgentUserSecurityID: Guid;
    TaskTitle: Text[150];
    ExternalId: Text[2048];
    From: Text[250];
    Message: Text;
    var TempAttachments: Record "Agent Task File" temporary): Record "Agent Task"
var
    AgentTaskBuilder: Codeunit "Agent Task Builder";
    AgentTaskMsgBuilder: Codeunit "Agent Task Message Builder";
    ContentStream: InStream;
begin
    AgentTaskMsgBuilder.Initialize(From, Message);

    if TempAttachments.FindSet() then
        repeat
            TempAttachments.Content.CreateInStream(ContentStream);
            AgentTaskMsgBuilder.AddAttachment(
                TempAttachments."File Name",
                TempAttachments."Content Type",
                ContentStream);
        until TempAttachments.Next() = 0;

    AgentTaskBuilder := AgentTaskBuilder
        .Initialize(AgentUserSecurityID, TaskTitle)
        .AddTaskMessage(AgentTaskMsgBuilder);

    if ExternalId <> '' then
        AgentTaskBuilder.SetExternalId(ExternalId);

    exit(AgentTaskBuilder.Create());
end;
```

### Pattern B, Page action (user-initiated)

**When**: a button on a page sends work to an agent.

**Flow**: `AgentSetup.OpenAgentLookup()` → user picks agent → `PublicAPI.AssignTask()`.

```al
trigger OnAction()
var
    AgentSetup: Codeunit "Agent Setup";
    MyAgentAPI: Codeunit "My Agent Public API";
    AgentUserSecurityId: Guid;
begin
    if not AgentSetup.OpenAgentLookup(
        Enum::"Agent Metadata Provider"::"My Agent", AgentUserSecurityId) then
        exit;

    MyAgentAPI.AssignTask(
        AgentUserSecurityId,
        CopyStr(StrSubstNo('Process: %1', Rec.Name), 1, 150),
        'LEAD-' + Rec."No.",
        CopyStr(UserId(), 1, 250),
        StrSubstNo('Please process customer %1 (No. %2).', Rec.Name, Rec."No."));
end;
```

### Pattern C, Business event (automated)

**When**: a business event (posting, releasing, approval) auto-triggers an agent task.

**Non-negotiable rules**:

1. **Always wrap in `[TryFunction]`**, never block the business event on agent failure.
2. **Always filter by business condition BEFORE creating the task**, not inside the TryFunction.
3. **Always log failures via `Session.LogMessage`** with category and `GetLastErrorText()`.

```al
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Release Sales Document",
                 OnBeforeReleaseSalesDoc, '', false, false)]
local procedure OnBeforeRelease(var SalesHeader: Record "Sales Header")
begin
    if SalesHeader."Document Type" <> SalesHeader."Document Type"::Order then exit;
    if not MeetsCondition(SalesHeader) then exit;

    if not TryCreateTask(SalesHeader) then
        Session.LogMessage('BCA2A-FAIL',
            StrSubstNo('Task creation failed for %1: %2', SalesHeader."No.", GetLastErrorText()),
            Verbosity::Error, DataClassification::SystemMetadata,
            TelemetryScope::ExtensionPublisher, 'Category', 'BCA2A');
end;

[TryFunction]
local procedure TryCreateTask(var SalesHeader: Record "Sales Header")
var
    PublicAPI: Codeunit "My Agent Public API";
begin
    PublicAPI.AssignTask(
        GetAgentSecurityId(),
        'Validate SO ' + SalesHeader."No.",
        'SO-' + SalesHeader."No.",
        'System',
        BuildMessage(SalesHeader));
end;
```

### Pattern D, Attachment task

**When**: agent needs to process files (PDFs, images, CSVs, Excel).

**Signature**: `AddAttachment(Name: Text[250]; MediaType: Text; ContentStream: InStream)`

```al
procedure CreateTaskWithAttachment(AgentUserId: Guid; var DocumentBlob: Codeunit "Temp Blob")
var
    AgentTaskBuilder: Codeunit "Agent Task Builder";
    AgentTaskMsgBuilder: Codeunit "Agent Task Message Builder";
    ContentStream: InStream;
begin
    DocumentBlob.CreateInStream(ContentStream);

    AgentTaskMsgBuilder.Initialize(CopyStr(UserId(), 1, 250), 'Review attached document.');
    AgentTaskMsgBuilder.AddAttachment('Document.pdf', 'application/pdf', ContentStream);

    AgentTaskBuilder := AgentTaskBuilder
        .Initialize(AgentUserId, 'Process Document')
        .SetExternalId('DOC-1001')
        .AddTaskMessage(AgentTaskMsgBuilder);

    AgentTaskBuilder.Create();
end;
```

**Common MIME types**: `application/pdf`, `image/png`, `image/jpeg`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (Excel xlsx), `text/csv`.

When you need a canonical end-to-end example combining attachment + business condition + telemetry + lead handoff, load `examples/lead-handoff-with-attachment.md`.

### Pattern E, Multi-turn conversation

**When**: continue an existing task with follow-up messages.

**Flow**: `GetTaskByExternalId()` → build follow-up → **workaround needed** (see below) → `SetStatusToReady()`.

**Status (Runtime 17.0)**: `AgentTaskMessageBuilder.AddToTask()` is **not available in Extension scope yet**. Until it ships, create a new task that references the original ExternalId:

```al
procedure SendFollowUpTask(AgentUserId: Guid; OriginalExternalId: Text; AdditionalInfo: Text)
var
    AgentTaskBuilder: Codeunit "Agent Task Builder";
    AgentTaskMsgBuilder: Codeunit "Agent Task Message Builder";
begin
    AgentTaskMsgBuilder.Initialize(CopyStr(UserId(), 1, 250), AdditionalInfo);

    AgentTaskBuilder := AgentTaskBuilder
        .Initialize(AgentUserId, CopyStr('Update for ' + OriginalExternalId, 1, 150))
        .SetExternalId(OriginalExternalId + '-UPD')
        .AddTaskMessage(AgentTaskMsgBuilder);

    AgentTaskBuilder.Create();
end;
```

**When `AddToTask` becomes available** in a future runtime, the canonical pattern will be:

```al
// FUTURE, not yet in Extension scope
AgentTaskRecord := AgentTaskCU.GetTaskByExternalId(AgentSecurityId, ExternalId);
AgentTaskMsgBuilder.Initialize('User', FollowUpText);
AgentTaskMsgBuilder.AddToTask(AgentTaskRecord);
if AgentTaskCU.CanSetStatusToReady(AgentTaskRecord) then
    AgentTaskCU.SetStatusToReady(AgentTaskRecord);
```

### Pattern F, Lifecycle management

**When**: monitor, restart, or stop tasks programmatically.

| Operation             | Method                                                   |
| --------------------- | -------------------------------------------------------- |
| Check active          | `Agent.IsActive(AgentUserSecurityId)`                    |
| Deactivate            | `Agent.Deactivate(AgentUserSecurityId)`                  |
| Find by ExternalId    | `AgentTask.GetTaskByExternalId(AgentUserId, ExternalId)` |
| Resume paused task    | `if AgentTask.CanSetStatusToReady(Rec) then AgentTask.SetStatusToReady(Rec)` |
| Display name lookup   | `Agent.GetDisplayName(AgentUserSecurityId)`              |

### Pattern G, Agent session detection

**When**: AL code must run only inside an agent session (not in normal user sessions).

```al
// Any agent
var
    AgentSession: Codeunit "Agent Session";
    Provider: Enum "Agent Metadata Provider";
begin
    if not AgentSession.IsAgentSession(Provider) then exit;
    // agent-specific logic
end;

// Specific agent
if not AgentSession.IsAgentSession(Enum::"Agent Metadata Provider"::"Lead Qualifier") then exit;
```

### Pattern H, Agent session event binding (performance)

**When**: event subscribers should be active only during agent tasks, never in normal user sessions.

**Architecture**: SingleInstance codeunit + BindSubscription on `System Initialization.OnAfterInitialization`.

```al
codeunit 52120 "My Agent Session Events"
{
    Access = Internal;
    SingleInstance = true;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Initialization",
                     OnAfterInitialization, '', false, false)]
    local procedure OnInit()
    var
        AgentSession: Codeunit "Agent Session";
    begin
        if not AgentSession.IsAgentSession(
            Enum::"Agent Metadata Provider"::"My Agent") then exit;

        GlobalEvents.SetAgentTaskID(AgentSession.GetCurrentSessionAgentTaskId());
        if BindSubscription(GlobalEvents) then;
    end;

    var
        GlobalEvents: Codeunit "My Agent Events";
}
```

## Human review without `SetRequiresReview`

`SetRequiresReview` is OnPrem-only. To force human review from an Extension, add a Warning annotation in `IAgentTaskExecution.AnalyzeAgentTaskMessage`:

```al
procedure AnalyzeAgentTaskMessage(
    AgentTaskMessage: Record "Agent Task Message";
    var Annotations: Record "Agent Annotation")
var
    AgentMessage: Codeunit "Agent Message";
    MessageText: Text;
begin
    MessageText := AgentMessage.GetText(AgentTaskMessage);

    if MessageText.Contains('DISQUALIFICATION') then begin
        Annotations.Severity := Annotations.Severity::Warning;
        Annotations.Message :=
            'Disqualification requires human approval. Review before proceeding.';
        Annotations.Insert();
    end;
end;
```

Warning annotations trigger the user-intervention flow via `GetAgentTaskUserInterventionSuggestions`. Error annotations stop the task.

## Agent discovery, current state

Until `Custom Agent.GetCustomAgents()` becomes available in Extension scope, use `Agent Setup.OpenAgentLookup()` for user selection and `Agent.GetDisplayName(Guid)` for display:

```al
trigger OnLookup(var Text: Text): Boolean
var
    AgentSetup: Codeunit "Agent Setup";
    SelectedAgentUserId: Guid;
begin
    if AgentSetup.OpenAgentLookup(
        Enum::"Agent Metadata Provider"::"Quote Builder", SelectedAgentUserId) then begin
        Rec."Target Agent Security ID" := SelectedAgentUserId;
        Text := Format(SelectedAgentUserId);
        exit(true);
    end;
    exit(false);
end;

local procedure GetAgentDisplayText(AgentSecurityId: Guid): Text
var
    AgentCU: Codeunit Agent;
begin
    if IsNullGuid(AgentSecurityId) then exit('');
    exit(AgentCU.GetDisplayName(AgentSecurityId) + ' (' + Format(AgentSecurityId) + ')');
end;
```

## Pattern selection guide

| Situation                                  | Pattern(s)        |
| ------------------------------------------ | ----------------- |
| Standard reusable task creation            | A (Public API)    |
| User clicks button to assign work          | B → A             |
| Business event triggers agent              | C → A             |
| Agent processes uploaded files             | D + (A, B, or C)  |
| Follow-up on existing task                 | E                 |
| Monitor / restart / stop tasks             | F                 |
| Code runs only in agent context            | G                 |
| Performance: subscribers only during tasks | H                 |
| Need to force human review (Extension)     | Warning annotation in `AnalyzeAgentTaskMessage` |

## Non-negotiable rules

1. **Public API is the entry point**, all other patterns call through it.
2. **TryFunction for event-driven creation**, never block posting, releasing, approval.
3. **Filter before creating**, business condition checked outside the TryFunction.
4. **Log failures to telemetry**, `Session.LogMessage` with category and error text.
5. **Mensaje contiene todo el contexto**, el agente solo sabe lo que le pasas por el mensaje + attachments.
6. **ExternalId follows `{PREFIX}-{No.}`**, `SO-1001`, `LEAD-001`, `EMAIL-{threadId}`. Never GUIDs or auto-increments.
7. **Use `Agent Task Message Builder`** for attachments, never manipulate the underlying records directly.
8. **Verify availability** against the matrix above before designing, don't ship code that relies on OnPrem-only methods.

## Examples

Load an example only when you need its concrete template:

- When implementing an end-to-end a

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Louage](https://github.com/Louage)
- **Source:** [Louage/frw-agentic-coding](https://github.com/Louage/frw-agentic-coding)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-louage-frw-agentic-coding-skill-agent-task-patterns
- Seller: https://agentstack.voostack.com/s/louage
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
