Skip to main content

Enable Workflows on More Pages

The Generic Workflow extension ships with built-in support for several standard Business Central pages. If you need to add workflow approval functionality to a page that is not included out of the box, you can create your own page extension that follows the same pattern.

This guide walks through the process step by step, using the Vendor Card as an example.

Supported Pages (Out of the Box)

The following pages already include Generic Workflow support:

PageType
Vendor CardCard
Vendor ListList
Vendor Bank Account CardCard
Vendor Bank Account ListList
Customer CardCard
Customer ListList
Customer Bank Account CardCard
Customer Bank Account ListList
Employee CardCard
Employee ListList
G/L Account CardCard
Chart of AccountsList
Item CardCard
Item ListList

If the page you need is not listed above, follow the steps below to create your own page extension.

Prerequisites

Before extending a page, make sure the following are in place:

  1. Generic Workflow is installed and enabled in your environment
  2. A workflow table setting has been configured for the table behind the page you want to extend (see Setup)
  3. A dependency on the Generic Workflow extension is declared in your extension's app.json

Add the Dependency

Add the following to the dependencies array in your app.json:

{
"id": "1d85692f-c02d-40d2-b217-d0ff803d62cf",
"name": "Generic Workflow",
"publisher": "Theta Systems Limited",
"version": "26.0.0.0"
}

Anatomy of a Workflow Page Extension

Every workflow-enabled page extension consists of four parts:

  1. FactBoxes — Workflow Status and Changes to Approve panels
  2. Approval actions — Approve, Reject, Delegate, and Comments
  3. Request Approval actions — Send, Cancel, and Revert Changes
  4. Control logic — Triggers and helper procedures that bind everything together

The sections below explain each part with code from the Vendor Card page extension.


Step 1 — Add the Workflow FactBoxes

Add two FactBox parts at the top of the FactBox area. These display the current workflow status and any pending field-level changes awaiting approval.

layout
{
addfirst(factboxes)
{
part(WorkflowStatus_WF_TSL; "Workflow Status FactBox")
{
ApplicationArea = All;
Editable = false;
Enabled = false;
ShowFilter = false;
Visible = ShowWorkflowStatus;
}
part(Change_WF_TSL; RecChangeListFactBox_WF_TSL)
{
ApplicationArea = All;
Editable = false;
Enabled = false;
ShowFilter = false;
UpdatePropagation = SubPart;
Visible = ShowChangeFactBox;
}
}
}
PartPurpose
Workflow Status FactBoxStandard Business Central FactBox that shows the current workflow step and status
RecChangeListFactBox_WF_TSLCustom FactBox that lists the old and new values for fields that have been changed and are pending approval
tip

Use UpdatePropagation = SubPart on card pages and UpdatePropagation = Both on list pages to ensure the changes FactBox refreshes correctly.


Step 2 — Add the Approval Actions

These actions allow an approver to act on a pending approval request directly from the page.

actions
{
addlast(reporting)
{
group(Approval_WF_TSL)
{
Caption = 'Approval';

action(Approve_WF_TSL)
{
ApplicationArea = All;
Caption = 'Approve';
Image = Approve;
ToolTip = 'Approve the requested changes for this record. The record will be unlocked for editing once all approvers have approved.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.ApproveRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}

action(Reject_WF_TSL)
{
ApplicationArea = All;
Caption = 'Reject';
Image = Reject;
ToolTip = 'Reject the approval request and return the record to the requester for correction or cancellation.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.RejectRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}

action(Delegate_WF_TSL)
{
ApplicationArea = All;
Caption = 'Delegate';
Image = Delegate;
ToolTip = 'Delegate the approval request to a substitute approver who will act on your behalf.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.DelegateRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}

action(Comment_WF_TSL)
{
ApplicationArea = All;
Caption = 'Comments';
Image = ViewComments;
ToolTip = 'View or add comments related to the approval request for this record.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit "Approvals Mgmt.";
begin
ApprovalsMgmt.GetApprovalComment(Rec);
EnableControls();
end;
}
}
}
}
note

All four actions are only visible when the current user has open approval entries (OpenApprovalEntriesExistCurrUser). Users who are not assigned as approvers will not see these actions.


Step 3 — Add the Request Approval Actions

These actions allow a user to submit, cancel, or revert a workflow request.

group(RequestApproval_WF_TSL)
{
Caption = 'Request Approval';
Image = SendApprovalRequest;

action(SendApprovalRequest_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Send A&pproval Request';
Enabled = (not OpenApprovalEntriesExist and EnabledApprovalWorkflowsExist);
Image = SendApprovalRequest;
ToolTip = 'Send an approval request for the pending changes on this record. The record will be locked until the request is approved, rejected, or cancelled.';

trigger OnAction()
begin
ApprovalsMgt.SendApprovalRequest(Rec.RecordId);
EnableControls();
end;
}

action(CancelApprovalRequest_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Cancel Approval Re&quest';
Enabled = CanCancelApprovalForRecord;
Image = CancelApprovalRequest;
ToolTip = 'Cancel the open approval request and unlock the record, allowing further changes without approval.';

trigger OnAction()
begin
ApprovalsMgt.CancelApprovalRequest(Rec.RecordId);
EnableControls();
end;
}

action(RevertChanges_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Revert Changes';
Enabled = (not OpenApprovalEntriesExist and EnabledApprovalWorkflowsExist);
Image = Undo;
ToolTip = 'Revert all pending changes on this record back to the last approved values and remove any usage restrictions.';

trigger OnAction()
var
RecordRestrictionMgt: Codeunit RecordRestrictionMgt_WF_TSL;
RecordRestrictionMgt2: Codeunit "Record Restriction Mgt.";
begin
if ApprovalsMgt.HasOpenApprovalEntries(Rec.RecordId) then
RecordRestrictionMgt2.CheckRecordHasUsageRestrictions(Rec);

RecordRestrictionMgt2.AllowRecordUsage(Rec);
RecordRestrictionMgt.UpdateRecordValues(Rec, true);
EnableControls();
end;
}
}
ActionEnabled When
Send Approval RequestNo open approvals exist and an active workflow is configured for the table
Cancel Approval RequestThe current user can cancel the existing approval
Revert ChangesNo open approvals exist and an active workflow is configured

Step 4 — Promote Actions to the Action Bar

Promote the approval and request actions so they appear in the action bar categories for quick access.

For card pages that already have standard approval categories (e.g. Vendor Card, Customer Card), add to the existing categories:

addfirst(Category_Category4) // Approve category
{
actionref(Approve_WF_TSL_Promoted; Approve_WF_TSL) { }
actionref(Reject_WF_TSL_Promoted; Reject_WF_TSL) { }
}
addlast(Category_Category4)
{
actionref(Delegate_WF_TSL_Promoted; Delegate_WF_TSL) { }
actionref(Comment_WF_TSL_Promoted; Comment_WF_TSL) { }
}
addlast(Category_Category5) // Request Approval category
{
actionref(SendApprovalRequest_WF_TSL_Promoted; SendApprovalRequest_WF_TSL) { }
actionref(CancelApprovalRequest_WF_TSL_Promoted; CancelApprovalRequest_WF_TSL) { }
actionref(RevertChanges_WF_TSL_Promoted; RevertChanges_WF_TSL) { }
}

For list pages or pages without existing approval categories, create new promoted groups:

addlast(Promoted)
{
group(Category_Approve_WF_TSL)
{
Caption = 'Approve';
actionref(Approve_WF_TSL_Promoted; Approve_WF_TSL) { }
actionref(Reject_WF_TSL_Promoted; Reject_WF_TSL) { }
actionref(Delegate_WF_TSL_Promoted; Delegate_WF_TSL) { }
actionref(Comment_WF_TSL_Promoted; Comment_WF_TSL) { }
}
group(Category_RequestApproval_WF_TSL)
{
Caption = 'Request Approval';
actionref(SendApprovalRequest_WF_TSL_Promoted; SendApprovalRequest_WF_TSL) { }
actionref(CancelApprovalRequest_WF_TSL_Promoted; CancelApprovalRequest_WF_TSL) { }
actionref(RevertChanges_WF_TSL_Promoted; RevertChanges_WF_TSL) { }
}
}

Step 5 — Add the Page Triggers and Helper Logic

The final piece wires everything together with page triggers and a shared EnableControls procedure.

trigger OnModifyRecord(): Boolean
begin
CurrPage.Activate(false);
end;

trigger OnAfterGetCurrRecord()
begin
EnableControls();
NotificationMgt.SendRecordChangeNotificationIfNoActiveApprovals(Rec);
end;

trigger OnOpenPage()
begin
EnableControls();
end;

local procedure EnableControls()
var
TempWorkflowRecordChange: Record "Workflow - Record Change" temporary;
begin
if (Rec."No." = '') then
exit;
ApprovalsMgt.GetApprovalControlState(
Rec.RecordId, TempWorkflowRecordChange,
ShowChangeFactBox, OpenApprovalEntriesExistCurrUser, OpenApprovalEntriesExist,
CanCancelApprovalForRecord, EnabledApprovalWorkflowsExist, ShowStandardWFActions);
CurrPage.Change_WF_TSL.Page.SetTempRecord(TempWorkflowRecordChange);
ShowWorkflowStatus :=
CurrPage.WorkflowStatus_WF_TSL.Page.SetFilterOnWorkflowRecord(Rec.RecordId);
end;

var
ApprovalsMgt: Codeunit ApprovalsMgt_WF_TSL;
NotificationMgt: Codeunit NotificationMgt_WF_TSL;
CanCancelApprovalForRecord: Boolean;
EnabledApprovalWorkflowsExist: Boolean;
OpenApprovalEntriesExist: Boolean;
OpenApprovalEntriesExistCurrUser: Boolean;
ShowChangeFactBox: Boolean;
ShowStandardWFActions: Boolean;
ShowWorkflowStatus: Boolean;

Understanding the Variables

VariableTypePurpose
ApprovalsMgtCodeunitGeneric Workflow approval management — controls approval state and actions
NotificationMgtCodeunitSends in-client notifications when pending changes have no active approval request
OpenApprovalEntriesExistCurrUserBooleantrue when the current user has approval entries to act on — controls visibility of Approve/Reject/Delegate
OpenApprovalEntriesExistBooleantrue when any open approval entries exist for the record
CanCancelApprovalForRecordBooleantrue when the current user is allowed to cancel the approval request
EnabledApprovalWorkflowsExistBooleantrue when an active workflow is configured for this table
ShowChangeFactBoxBooleanControls visibility of the Changes to Approve FactBox
ShowWorkflowStatusBooleanControls visibility of the Workflow Status FactBox
ShowStandardWFActionsBooleanControls visibility of the standard BC approval actions (used on pages that have built-in approval groups)
important

The EnableControls procedure is called on every page trigger (OnOpenPage, OnAfterGetCurrRecord) and after every approval action to keep the UI state up to date.


Step 6 — Handle Primary Key Renames on Editable Pages

If the page allows the user to modify the primary key field (e.g. the "No." field), add a modify trigger to clean up notifications when the record is renamed. This applies to card pages as well as editable list pages such as Chart of Accounts and Item List:

layout
{
modify("No.")
{
trigger OnAfterValidate()
begin
NotificationMgt.RecallNotificationsForRenamedRecord(xRec.RecordId);
end;
}
}

The NotificationMgt codeunit variable is already declared as part of the standard variables (see Step 5). This ensures that workflow notifications for the old record ID are recalled when the primary key changes.


Step 7 — (Optional) Hide Standard Approval Actions

If the page already has standard Business Central approval actions (e.g. the Vendor Card or Customer Card), you can hide them to avoid duplicate buttons:

actions
{
modify(Approval)
{
Visible = ShowStandardWFActions;
}
modify("Request Approval")
{
Visible = ShowStandardWFActions;
}
}

The ShowStandardWFActions variable is set by EnableControls — it will hide the default actions when a Generic Workflow is active for the table, and show them when it is not.


Complete Example — Vendor Card

Below is the full page extension for the Vendor Card, combining all the steps above:

Click to expand the full Vendor Card page extension
pageextension 58107 "Vendor Card_WF_TSL" extends "Vendor Card"
{
layout
{
modify("No.")
{
trigger OnAfterValidate()
begin
NotificationMgt.RecallNotificationsForRenamedRecord(xRec.RecordId);
end;
}
addfirst(factboxes)
{
part(WorkflowStatus_WF_TSL; "Workflow Status FactBox")
{
ApplicationArea = All;
Editable = false;
Enabled = false;
ShowFilter = false;
Visible = ShowWorkflowStatus;
}
part(Change_WF_TSL; RecChangeListFactBox_WF_TSL)
{
ApplicationArea = All;
Editable = false;
Enabled = false;
ShowFilter = false;
UpdatePropagation = SubPart;
Visible = ShowChangeFactBox;
}
}
}
actions
{
modify(Approval)
{
Visible = ShowStandardWFActions;
}
modify("Request Approval")
{
Visible = ShowStandardWFActions;
}
addlast(reporting)
{
group(Approval_WF_TSL)
{
Caption = 'Approval';
action(Approve_WF_TSL)
{
ApplicationArea = All;
Caption = 'Approve';
Image = Approve;
ToolTip = 'Approve the requested changes for this record. The record will be unlocked for editing once all approvers have approved.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.ApproveRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}
action(Reject_WF_TSL)
{
ApplicationArea = All;
Caption = 'Reject';
Image = Reject;
ToolTip = 'Reject the approval request and return the record to the requester for correction or cancellation.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.RejectRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}
action(Delegate_WF_TSL)
{
ApplicationArea = All;
Caption = 'Delegate';
Image = Delegate;
ToolTip = 'Delegate the approval request to a substitute approver who will act on your behalf.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit ApprovalsMgt_WF_TSL;
begin
ApprovalsMgmt.DelegateRecordApprovalRequest(Rec.RecordId);
EnableControls();
end;
}
action(Comment_WF_TSL)
{
ApplicationArea = All;
Caption = 'Comments';
Image = ViewComments;
ToolTip = 'View or add comments related to the approval request for this record.';
Visible = OpenApprovalEntriesExistCurrUser;

trigger OnAction()
var
ApprovalsMgmt: Codeunit "Approvals Mgmt.";
begin
ApprovalsMgmt.GetApprovalComment(Rec);
EnableControls();
end;
}
}
group(RequestApproval_WF_TSL)
{
Caption = 'Request Approval';
Image = SendApprovalRequest;
action(SendApprovalRequest_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Send A&pproval Request';
Enabled = (not OpenApprovalEntriesExist and EnabledApprovalWorkflowsExist);
Image = SendApprovalRequest;
ToolTip = 'Send an approval request for the pending changes on this record. The record will be locked until the request is approved, rejected, or cancelled.';

trigger OnAction()
begin
ApprovalsMgt.SendApprovalRequest(Rec.RecordId);
EnableControls();
end;
}
action(CancelApprovalRequest_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Cancel Approval Re&quest';
Enabled = CanCancelApprovalForRecord;
Image = CancelApprovalRequest;
ToolTip = 'Cancel the open approval request and unlock the record, allowing further changes without approval.';

trigger OnAction()
begin
ApprovalsMgt.CancelApprovalRequest(Rec.RecordId);
EnableControls();
end;
}
action(RevertChanges_WF_TSL)
{
ApplicationArea = Basic, Suite;
Caption = 'Revert Changes';
Enabled = (not OpenApprovalEntriesExist and EnabledApprovalWorkflowsExist);
Image = Undo;
ToolTip = 'Revert all pending changes on this record back to the last approved values and remove any usage restrictions.';

trigger OnAction()
var
RecordRestrictionMgt: Codeunit RecordRestrictionMgt_WF_TSL;
RecordRestrictionMgt2: Codeunit "Record Restriction Mgt.";
begin
if ApprovalsMgt.HasOpenApprovalEntries(Rec.RecordId) then
RecordRestrictionMgt2.CheckRecordHasUsageRestrictions(Rec);

RecordRestrictionMgt2.AllowRecordUsage(Rec);
RecordRestrictionMgt.UpdateRecordValues(Rec, true);
EnableControls();
end;
}
}
}
addfirst(Category_Category4)
{
actionref(Approve_WF_TSL_Promoted; Approve_WF_TSL) { }
actionref(Reject_WF_TSL_Promoted; Reject_WF_TSL) { }
}
addlast(Category_Category4)
{
actionref(Delegate_WF_TSL_Promoted; Delegate_WF_TSL) { }
actionref(Comment_WF_TSL_Promoted; Comment_WF_TSL) { }
}
addlast(Category_Category5)
{
actionref(SendApprovalRequest_WF_TSL_Promoted; SendApprovalRequest_WF_TSL) { }
actionref(CancelApprovalRequest_WF_TSL_Promoted; CancelApprovalRequest_WF_TSL) { }
actionref(RevertChanges_WF_TSL_Promoted; RevertChanges_WF_TSL) { }
}
}

trigger OnModifyRecord(): Boolean
begin
CurrPage.Activate(false);
end;

trigger OnAfterGetCurrRecord()
begin
EnableControls();
NotificationMgt.SendRecordChangeNotificationIfNoActiveApprovals(Rec);
end;

trigger OnOpenPage()
begin
EnableControls();
end;

local procedure EnableControls()
var
TempWorkflowRecordChange: Record "Workflow - Record Change" temporary;
begin
if (Rec."No." = '') then
exit;
ApprovalsMgt.GetApprovalControlState(
Rec.RecordId, TempWorkflowRecordChange,
ShowChangeFactBox, OpenApprovalEntriesExistCurrUser, OpenApprovalEntriesExist,
CanCancelApprovalForRecord, EnabledApprovalWorkflowsExist, ShowStandardWFActions);
CurrPage.Change_WF_TSL.Page.SetTempRecord(TempWorkflowRecordChange);
ShowWorkflowStatus :=
CurrPage.WorkflowStatus_WF_TSL.Page.SetFilterOnWorkflowRecord(Rec.RecordId);
end;

var
ApprovalsMgt: Codeunit ApprovalsMgt_WF_TSL;
NotificationMgt: Codeunit NotificationMgt_WF_TSL;
CanCancelApprovalForRecord: Boolean;
EnabledApprovalWorkflowsExist: Boolean;
OpenApprovalEntriesExist: Boolean;
OpenApprovalEntriesExistCurrUser: Boolean;
ShowChangeFactBox: Boolean;
ShowStandardWFActions: Boolean;
ShowWorkflowStatus: Boolean;
}

Checklist

Use this checklist when creating your own page extension:

  • Add the Generic Workflow dependency to app.json
  • Add WorkflowStatus_WF_TSL and Change_WF_TSL FactBox parts
  • Add Approve, Reject, Delegate, and Comments actions
  • Add Send Approval Request, Cancel Approval Request, and Revert Changes actions
  • Promote all actions to the action bar
  • Add OnModifyRecord, OnAfterGetCurrRecord, and OnOpenPage triggers
  • Implement the EnableControls procedure with all required variables
  • (Editable pages) Handle primary key rename notification recall
  • (Pages with existing approval actions) Hide standard actions using ShowStandardWFActions
  • Configure the workflow table setting in Generic Workflow Setup
  • Create and activate a workflow template for the table

Frequently Asked Questions

Can I add Generic Workflows to a custom page?

Yes. The pattern works with any page backed by a table that has been configured in the Generic Workflow Table Setup. Replace Rec.RecordId references with the appropriate record identifier for your source table.

Why don't the approval actions appear on my page?

Check the following:

  1. The Generic Workflow feature is enabled in Generic Workflow Setup
  2. A workflow table setting exists for your table and has an active workflow code
  3. The page extension is compiled and published
  4. The EnableControls procedure is being called in OnAfterGetCurrRecord and OnOpenPage

What is the difference between card and list page extensions?

The logic is identical. The only differences are:

  • Card pages use UpdatePropagation = SubPart on the Changes FactBox; list pages use UpdatePropagation = Both
  • Any editable page where the primary key field can be modified needs the rename notification recall logic (Step 6) — this includes card pages and editable list pages such as Chart of Accounts and Item List
  • Action promotion syntax differs — card pages may have existing Category_Category4/Category_Category5 groups; list pages typically need new promoted groups