If you write AL extensions for Business Central and are wondering whether GitHub Copilot is worth the subscription, this post gives you a practical answer based on daily use.

After using GitHub Copilot regularly in AL development, one thing became clear quickly: it genuinely saves time. Not in the dramatic way the marketing suggests, but in a steady, practical way that adds up across a working day.

The key is using the right mode for the right task. This post focuses on how Chat and Agent mode deliver the most consistent value in AL projects. If you are new to the different Copilot modes in VS Code, start with Introduction to GitHub Copilot in VSCode for AL Development before reading this one.

The Right Mindset Before You Start

The biggest mistake developers make with Copilot is treating it as an autocomplete engine. The real productivity gains come from working with it deliberately: you define the intent, Copilot handles the structure. That means using Chat to think through what you are building before Agent starts writing code.

This approach is covered in depth in the post on Copilot Plan Mode in AL. The short version: clarify what you want before you ask Copilot to build it. The quality of what Agent produces is directly proportional to the clarity of the intent you give it.

Generating AL Boilerplate with Agent Mode

AL development involves a lot of structural code. Every new object follows a predictable pattern: properties, triggers, procedure stubs. This is where Agent mode earns its keep.

Rather than typing a codeunit skeleton by hand, open the Copilot Agent panel and describe what you need. A prompt like the following works well:

Create a codeunit called “Sales Posting Helper” with a procedure called ValidatePostingSetup that takes a Sales Header record and returns a Boolean. The procedure should check whether a General Posting Setup exists for the posting groups on the Sales Header.

Agent will generate the full object including the correct variable declarations, the Get call, and the exit statement. The result is close to production-ready and takes seconds rather than minutes.

codeunit 55103 "Sales Posting Helper"
{
    procedure ValidatePostingSetup(SalesHeader: Record "Sales Header"): Boolean
    var
        SalesLine: Record "Sales Line";
        GeneralPostingSetup: Record "General Posting Setup";
    begin
        if SalesHeader."Gen. Bus. Posting Group" = '' then
            exit(false);

        SalesLine.SetRange("Document Type", SalesHeader."Document Type");
        SalesLine.SetRange("Document No.", SalesHeader."No.");

        if not SalesLine.FindSet() then
            exit(false);

        repeat
            // A line without a general product posting group cannot be validated.
            if SalesLine."Gen. Prod. Posting Group" = '' then
                exit(false);

            if not GeneralPostingSetup.Get(SalesHeader."Gen. Bus. Posting Group",
                SalesLine."Gen. Prod. Posting Group") then
                exit(false);
        until SalesLine.Next() = 0;

        exit(true);
    end;
}

The generated code still needs review, but it removes the repetitive typing and eliminates the context-switching of looking up syntax you have written a dozen times before.

Exploring Unfamiliar APIs with Chat Mode

AL has a broad surface area. Even experienced developers encounter parts of the platform they do not use day-to-day. When you need to work with HttpClient, JSON handling, streams, or file operations, Chat mode is the fastest way to get a working starting point.

Open the Copilot Chat panel and ask something like:

Show me how to call an external REST API in AL using HttpClient and return the response as Text. Include basic error handling.

Copilot will return a working procedure you can copy directly into your extension and refine. This is not a replacement for reading the official HttpClient documentation, but it gives you a concrete starting point in seconds rather than having to construct the pattern from scratch.

codeunit 55110 "Rest Api Helper"
{
    procedure GetAsText(Url: Text): Text
    var
        Client: HttpClient;
        Response: HttpResponseMessage;
        RequestHeaders: HttpHeaders;
        Content: HttpContent;
        ResponseText: Text;
        ErrorBody: Text;
    begin
        if Url = '' then
            Error('URL must not be empty.');

        // Optional: request JSON responses
        Client.DefaultRequestHeaders(RequestHeaders);
        RequestHeaders.Add('Accept', 'application/json');

        // Network/TLS/DNS-level failure - no HTTP response at all
        if not Client.Get(Url, Response) then
            Error('Request failed before an HTTP response was received. Check URL and network connectivity.');

        // HTTP error status (4xx / 5xx)
        if not Response.IsSuccessStatusCode() then begin
            Content := Response.Content();
            if not Content.ReadAs(ErrorBody) then
                ErrorBody := '<no response body>';

            Error(
                'API call failed. Status: %1 %2. Body: %3',
                Response.HttpStatusCode(),
                Response.ReasonPhrase(),
                CopyStr(ErrorBody, 1, 500));
        end;

        // Success path
        Content := Response.Content();
        if not Content.ReadAs(ResponseText) then
            Error('Could not read the response body as text.');

        exit(ResponseText);
    end;
}

Example call from a page action:

trigger OnAction()
var
    Helper: Codeunit "Rest Api Helper";
    Result: Text;
begin
    Result := Helper.GetAsText('https://api.example.com/data');
    Message(CopyStr(Result, 1, 250));
end;

Note: In Business Central go to Extension Management, find your extension, choose Configure and enable Allow HttpClient Requests. Without this, all outbound calls are blocked at runtime regardless of the code.


Always verify error handling, authentication, and timeout behavior manually. Copilot’s suggestions rarely cover these fully out of the box.

Generating Test Code with Agent Mode

Writing tests is important work, but the scaffolding is highly repetitive. A typical AL test codeunit always needs the same structure: library references, an Initialize procedure, data creation helpers, and assertions. Agent mode is genuinely good at generating this scaffolding when you give it a clear description of what to test.

A prompt like the following works well:

Create a test procedure for a codeunit called “Sales Posting Helper”. The test should verify that ValidatePostingSetup returns true when a valid General Posting Setup exists for the posting groups on a Sales Header. Use the standard AL test library codeunits.

What happened next was a good example of what Agent mode can do beyond simple code generation. The test libraries were not yet listed as a dependency in the project’s app.json. Rather than failing or generating broken code, Agent detected this, searched the workspace for existing app.json files, identified the correct Microsoft ID for the Tests – Test Libraries dependency, updated app.json automatically, and then explained exactly what to do next: press Ctrl+Shift+P and run AL: Download Symbols to pull the dependency from the connected sandbox.

That kind of autonomous problem-solving across multiple files is what separates Agent mode from a simple code generator.

The generated test codeunit itself is thorough and correct:

codeunit 55104 "Sales Posting Helper Test"
{
    Subtype = Test;
    TestPermissions = Disabled;

    var
        Assert: Codeunit Assert;
        LibrarySales: Codeunit "Library - Sales";
        LibraryERM: Codeunit "Library - ERM";

    [Test]
    procedure ValidatePostingSetup_ReturnsTrue_WhenValidGeneralPostingSetupExists()
    var
        SalesHeader: Record "Sales Header";
        SalesLine: Record "Sales Line";
        GenBusPostingGroup: Record "Gen. Business Posting Group";
        GenProdPostingGroup: Record "Gen. Product Posting Group";
        GeneralPostingSetup: Record "General Posting Setup";
        SalesPostingHelper: Codeunit "Sales Posting Helper";
    begin
        // [GIVEN] Separate Gen. Bus. and Gen. Prod. Posting Groups are created
        LibraryERM.CreateGenBusPostingGroup(GenBusPostingGroup);
        LibraryERM.CreateGenProdPostingGroup(GenProdPostingGroup);

        // [GIVEN] A General Posting Setup exists linking those two groups
        LibraryERM.CreateGeneralPostingSetup(GeneralPostingSetup, GenBusPostingGroup.Code, GenProdPostingGroup.Code);

        // [GIVEN] A Sales Order whose Gen. Bus. Posting Group matches the setup
        LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, LibrarySales.CreateCustomerNo());
        SalesHeader."Gen. Bus. Posting Group" := GenBusPostingGroup.Code;
        SalesHeader.Modify(false);

        // [GIVEN] At least one Sales Line with a Gen. Prod. Posting Group that matches the setup
        LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::" ", '', 0);
        SalesLine."Gen. Prod. Posting Group" := GenProdPostingGroup.Code;
        SalesLine.Modify(false);

        // [WHEN] ValidatePostingSetup is called
        // [THEN] It returns true because a matching General Posting Setup record exists
        Assert.IsTrue(
            SalesPostingHelper.ValidatePostingSetup(SalesHeader),
            'ValidatePostingSetup should return true when a valid General Posting Setup exists for the posting groups on the Sales Header.');
    end;
}

The test procedure name follows the Arrange/Act/Assert naming convention, uses isolated posting groups created specifically for the test to avoid clashing with existing data, and uses Modify(false) to override posting groups without triggering validate triggers. The assertion message is descriptive enough to be useful when the test fails.

The generated test will still need review to ensure it reflects your specific business logic, but it provides a production-quality starting point that would take considerably longer to write from scratch.

Explaining Existing Code with Ask Mode

This is perhaps the most underrated use of Copilot in AL work. Large extensions accumulate complexity quickly. When you open a codeunit written by someone else, or one you wrote six months ago, it is not always immediately clear what it does or why it was written the way it was.

Select the procedure you want to understand, open the Copilot Chat panel using Ask mode, and type something like:

Explain what this procedure does, which tables it interacts with, and what the overall responsibility of this codeunit is.

The response goes well beyond a simple summary. For the Sales Posting Helper codeunit, Copilot identified the single responsibility precisely: answering the question of whether a Sales Header is safe to post from a General Posting Setup perspective. It then broke down the procedure logic step by step, listed every table the codeunit interacts with and whether access is read or write, and added a design characteristics section that most developers would not think to document themselves.

Some of the design observations Copilot surfaced:

  • The procedure is stateless with no global variables and no side effects.
  • It uses short-circuit AND logic, exiting immediately as soon as any condition fails rather than checking all conditions first.
  • It treats a missing posting group on any line as a failure, mirroring how Business Central’s own posting routines behave.
  • It returns false instead of calling Error(), leaving the decision about how to handle the failure to the caller.

That last point in particular is the kind of design insight that is easy to miss when reading unfamiliar code quickly. Copilot surfaced it in seconds.

All access is read-only. No Insert, Modify, or Delete anywhere. Copilot noted this explicitly, which is a useful signal when reviewing code for unexpected side effects.

This is particularly valuable when onboarding new developers, reviewing code in a pull request, or investigating a bug you did not introduce. The post on Chat, Ask, or Edit covers when to use each mode in more detail.

Refactoring with Edit Mode

When you have existing code that needs reorganization, Edit mode is precise and reviewable. Select the block of code you want to refactor, activate Edit mode, and describe the change you want:

Extract the validation logic in this procedure into a separate procedure called ValidateHeader.

Copilot understood the intent precisely. It extracted the header guard into a local procedure called ValidateHeader, marked it as local since it is an implementation detail not needed by callers, and updated ValidatePostingSetup to delegate to it at the top of the procedure. The diff view makes the change immediately clear: red shows what was removed, green shows what was added.

This is also a good example of why generated code always needs review before accepting. The suggested ValidateHeader procedure contained a syntax error: exit false and exit true without parentheses or semicolons. The correct AL syntax is exit(false); and exit(true);. Copilot produced a structurally correct refactoring with a small but real error in the details.

That is the pattern you will encounter regularly. Copilot gets the intent right and the structure right. The fine details of AL syntax occasionally slip through. The developer remains responsible for verifying the result before accepting. In AL especially, even small changes can affect posting behavior, so that review step is not optional.

For a deeper look at how to keep that control as tasks get more complex, the post on using Copilot Agent without losing control is worth reading alongside this one.

Where Copilot Has Limits

Understanding where Copilot helps also means being clear about where it does not. Copilot struggles with complex business rules that are unique to a customer’s process, architectural decisions about how to structure extensions, deep Business Central platform knowledge such as posting routines, dimension handling and approval workflows, and debugging logic errors in existing code.

These are the genuinely hard parts of AL development and they still depend entirely on the developer. Always review generated code carefully before accepting it. Copilot can produce plausible-looking AL that contains subtle errors, and in Business Central a posting error can be expensive to find and fix.

A Practical Summary

GitHub Copilot is a strong development assistant for AL work when used with clear intent. Agent mode handles boilerplate and test scaffolding well. Chat mode accelerates API exploration. Ask mode makes unfamiliar code readable. Edit mode keeps refactoring precise and reviewable.

The developers who get the most from it are the ones who bring a clear description of what they want to build before they ask Copilot to build it. When you combine that discipline with the right mode for the task, Copilot makes a meaningful difference to how much you can get done in a day.

For a complete overview of all five Copilot modes and how they work together in AL projects, the practical mental model for using GitHub Copilot in AL development brings the full picture together.

What has your experience been with Copilot in AL development? Have you found a mode or workflow that works particularly well for you? Leave a comment below.


Discover more from think about IT

Subscribe to get the latest posts sent to your email.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Post Navigation