In Business Central version 2024 Wave 1, there is a new page scripting tool to help automate testing. In this video I take a closer look and start thinking about how to use it, then I start writing code… Check it out:

In this video, Erik explores the new Page Scripting tool introduced in Business Central 2024 Wave 1 (BC24). He starts by demonstrating how the tool works as a screen recorder for creating test scripts, then dives into an ambitious hack: automatically generating page scripts from existing data using AL code. The idea is to turn test data into reusable YAML-based scripts rather than recording them manually.
The Page Scripting Tool: First Look
The Page Scripting tool is essentially a screen recorder for Business Central. You can find it in the toolbar, and it captures your interactions with pages — clicks, keyboard shortcuts, field inputs — and records them as replayable steps.
Erik demonstrates using the Payment Terms page as a test case:
- Open the Page Scripting tool from the toolbar
- Click Record to start capturing
- Create a new record using Alt+N
- Fill in fields: Code = “0”, Due Date Calculation = “5D”, Description = “testing”
- Click Stop to end the recording
The recorded script shows each step: “Invoke New”, “Focus Code”, “Input 0 into Code”, “Focus Due Date Calculation”, “Input 5D into Due Date Calculation”, and so on. You can then replay the script, and it will recreate the same actions — “Script completed without errors. Perfect, we love stuff that happens without errors.”
Editing Limitations
Erik quickly discovers some limitations of the tool’s editing capabilities:
- There’s no undo functionality
- Reordering steps by dragging isn’t supported
- Deleting individual steps is tricky — a delete option appears during recording but doesn’t seem fully functional
- If you record in the middle of a script, it inserts steps at the current position, which can break your script
Right-Click Context Menu
An interesting feature Erik discovers is the right-click context menu within the page scripting context. It offers options like:
- Copy/Paste Session Info — for injecting session user ID values
- Validate — to check values like row count
- Add Conditional Steps — for conditional logic based on current values
When examining properties of recorded steps, Erik notices Power FX references — suggesting that the scripting engine is built on Power Platform technology under the hood.
The YAML Script Format
When you save a recording, it exports as a YAML file. Erik examines the structure:
name: Recording
description: Test recording
start:
profile: BUSINESS MANAGER
steps:
- type: invoke
target:
- page: Payment Terms
runtimeRef: bgw
- action: Control_New
invokeType: New
description: Invoke New on <caption>New</caption>
- type: focus
target:
- page: Payment Terms
runtimeRef: bgw
- repeater: Control1
- field: Code
description: Focus <caption>Code</caption>
- type: input
target:
- page: Payment Terms
runtimeRef: bgw
- repeater: Control1
- field: Code
value: 0D
description: Input <value>0D</value> into <caption>Code</caption>
The key observations about the format:
- There’s a header section with name, description, and start profile
- Each step has a type (invoke, focus, input, setCurrentRow)
- Targets reference pages by name and a runtimeRef — a three-letter code that corresponds to internal control identifiers
- Every field input requires two steps: first a focus, then an input
- The runtimeRef is problematic because it potentially changes every time you recompile a page or add a new field
The Hack: Generating Scripts from Data
Erik’s big idea: instead of manually recording test scripts, what if you could take existing data and automatically generate a page script from it? If you have test data in a table, you should be able to programmatically produce a YAML script that would recreate those records.
Architecture
The solution consists of two objects:
- A codeunit (
ScriptingHelper) that does the heavy lifting of generating the YAML - A page extension on Payment Terms that adds a “Generate Script” action
The Complete Source Code
namespace Hougaard.ScriptingHelper;
using Microsoft.Foundation.PaymentTerms;
using System.Tooling;
using System.Utilities;
using System.Reflection;
codeunit 73100 ScriptingHelper
{
procedure GenerateScript(PageNo: Integer; Rec: Variant)
var
MetaData: Record "Page Metadata";
PageFields: Record "Page Control Field";
Ref: RecordRef;
FR: FieldRef;
Script: TextBuilder;
RepeaterName: Text;
InS: InStream;
FileName: Text;
Blob: Codeunit "Temp Blob";
OutS: OutStream;
begin
MetaData.Get(PageNo);
if MetaData.SourceTable = 0 then
error('No source table found, cannot create script');
Ref.GetTable(Rec);
// Get repeater from Page Meta Fields
RepeaterName := 'Control1';
Script.Append(Header());
if Ref.FindSet() then
repeat
Script.Append(NewRec(MetaData));
PageFields.Setrange(PageNo, MetaData.ID);
PageFields.SetFilter(FieldNo, '1');
if PageFields.FindSet() then // Loop primary key fields
repeat
FR := Ref.Field(PageFields.FieldNo);
Script.Append(FillOutField(MetaData, RepeaterName, PageFields, FR));
until PageFields.Next() = 0;
PageFields.SetFilter(FieldNo, '2|5');
if PageFields.FindSet() then // loop rest of fields
repeat
FR := Ref.Field(PageFields.FieldNo);
Script.Append(FillOutField(MetaData, RepeaterName, PageFields, FR));
until PageFields.Next() = 0;
until Ref.Next() = 0;
Script.Append(NewRec(MetaData));
Blob.CreateOutStream(OutS);
OutS.WriteText(Script.ToText());
Blob.CreateInStream(InS);
FileName := 'script.yml';
DownloadFromStream(InS, '', '', '', FileName);
end;
local procedure NewRec(MetaData: Record "Page Metadata"): Text
var
out: TextBuilder;
begin
out.AppendLine(' - type: invoke');
out.AppendLine(' target:');
out.AppendLine(' - page: ' + MetaData.Name);
out.AppendLine(' runtimeRef: xxx');
out.AppendLine(' - action: Control_New');
out.AppendLine(' invokeType: New');
out.AppendLine(' description: Invoke New on <caption>New</caption>');
exit(out.ToText());
end;
local procedure Header(): Text
var
out: TextBuilder;
begin
out.AppendLine('name: Recording');
out.AppendLine('description: Test recording');
out.AppendLine('start:');
out.AppendLine(' profile: BUSINESS MANAGER');
out.AppendLine('steps:');
exit(out.ToText());
end;
local procedure FillOutField(MetaData: Record "Page Metadata"; RepeaterName: Text;
PageFields: Record "Page Control Field"; var FR: FieldRef): Text
var
out: TextBuilder;
begin
out.AppendLine(' - type: focus');
out.AppendLine(' target:');
out.AppendLine(' - page: ' + MetaData.Name);
out.AppendLine(' runtimeRef: xxx');
out.AppendLine(' - repeater: ' + RepeaterName);
out.AppendLine(' - field: ' + PageFields.ControlName);
out.AppendLine(' description: Focus <caption>' + PageFields.ControlName + '</caption>');
out.AppendLine(' - type: input');
out.AppendLine(' target:');
out.AppendLine(' - page: ' + MetaData.Name);
out.AppendLine(' runtimeRef: xxx');
out.AppendLine(' - repeater: ' + RepeaterName);
out.AppendLine(' - field: ' + PageFields.ControlName);
out.AppendLine(' value: ' + format(FR.Value, 0, 9));
out.AppendLine(' description: Input <value>' + format(FR.Value, 0, 9)
+ '</value> into <caption>' + PageFields.ControlName + '</caption>');
exit(out.ToText());
end;
}
And the page extension to wire it up:
pageextension 73100 PaymentTerms extends "Payment Terms"
{
actions
{
addfirst(processing)
{
action(Generate)
{
Caption = 'Generate Script';
ApplicationArea = all;
trigger OnAction()
var
ScriptingHelper: Codeunit ScriptingHelper;
begin
ScriptingHelper.GenerateScript(Page::"Payment Terms", Rec);
end;
}
}
}
}
How It Works
The approach breaks down into several key steps:
- Get page metadata — Use the
Page Metadatatable to find the source table for the given page number. If there’s no source table (like on Role Center pages), it errors out. - Open the data — Use a
RecordRefto dynamically open the source table and iterate through records. By passing theRecvariant from the page, any active filters are preserved. - Build the YAML — Use a
TextBuilderto construct the script. For each record: emit a “New” invoke step, then for each field emit a “Focus” step followed by an “Input” step. - Download the file — Write the text to a
Temp Blob, create an InStream, and useDownloadFromStreamto save it as a .yml file.
Why TextBuilder?
Erik takes a moment to explain the choice of TextBuilder over simple string concatenation. In .NET (which AL runs on), strings are immutable. Every time you do text += moreText, it allocates a new string in memory. The TextBuilder (analogous to .NET’s StringBuilder) uses a concept sometimes called “ropes” — it keeps small snippets of text separate and only combines them into one final string when you call ToText(). For generating scripts with many lines, this is significantly more memory-efficient.
The Format Function Trick
One elegant detail is the use of Format(FR.Value, 0, 9) to convert field values to text. The Value property of a FieldRef returns a variant — it could be a date, a decimal, a text, or any other data type. The Format function accepts variants and handles them all. The format number 9 ensures XML-compatible formatting, which is a safer choice for generating structured output.
Challenges and Gotchas
Erik encounters several real-world problems during his live coding session:
Runtime References
The YAML format includes runtimeRef values — three-letter codes that identify controls internally. These potentially change every time you recompile a page or add a new field, making them fragile for script portability. Erik works around this by using “xxx” as a placeholder.
Case Sensitivity
The script failed on first run because of case sensitivity: the code generated control1 (lowercase) but the scripting tool expected Control1 (uppercase). A quick find-and-replace fixed it, but it highlights the importance of matching the exact control names.
Field Ordering
Perhaps the biggest challenge: the Page Control Field metadata table doesn’t return fields in the order they appear on the page. Erik’s generated script had description before due date calculation and other fields out of sequence. This matters because the page scripting tool replays steps in order, and field validation triggers may depend on entry sequence.
Erik’s workaround is to hardcode the field number filters — first processing field number 1 (the primary key), then specific field numbers like 2 and 5. Obviously this doesn’t scale, but it demonstrates the concept.
Editable and Visible Fields
The metadata stores editability and visibility as text values that might contain variable expressions rather than simple “true”/”false” strings. For example, a field’s Visible property might be set to a global variable name rather than a literal boolean. This makes it unreliable to filter on these properties programmatically.
Ideas for Improvement
Erik outlines several ways this proof of concept could be expanded:
- Page type detection — Examine the page metadata to determine if it’s a list page or card page, and adjust the script generation accordingly (repeater structure vs. card layout)
- Dynamic repeater discovery — Instead of hardcoding “Control1”, look at the page controls metadata to find the actual repeater control name
- Field selection dialog — Present users with a dialog asking which fields to include, since complex pages like the Customer Card have many fields that might not all be relevant
- Integration with the Simple Object Designer — Erik realizes that since his Simple Object Designer already has access to all the symbol information including proper field ordering, it might be an even better place to build this functionality
- Filter passthrough — The current code already supports passing filtered records from the page, so you could generate scripts for specific subsets of data
Summary
The BC24 Page Scripting tool is a promising addition to Business Central’s testing toolkit. While its built-in editing capabilities are still limited — no undo, no drag-to-reorder, limited delete — the YAML-based file format opens up interesting possibilities for programmatic script generation. Erik’s proof of concept demonstrates that with about 130 lines of AL code, you can automatically turn existing data into a replayable page script. The approach has real potential for generating test data scripts at scale, though challenges around field ordering, runtime references, and metadata completeness would need to be addressed for production use.