Developer Guide - REFRACT PDF Business Central
Extend REFRACT PDF with custom AL code. Subscribe to integration events, override templates, customize storage paths, and integrate with third-party systems.
Extension Architecture
REFRACT PDF is a standard Microsoft Dynamics 365 Business Central extension. It occupies object ID range 77100-77199 and uses published integration events to allow partners and customers to extend its functionality.
Extension Dependency
Partner and customer extensions that build on REFRACT PDF must declare a dependency in their app.json:
{
"dependencies": [
{
"appId": "b6f75cab-2b7a-4998-ac28-9acc767b8b4d",
"name": "REFRACT PDF",
"publisher": "Refract Logic LLC",
"version": "1.0.0.0"
}
]
}Versioning Strategy
REFRACT PDF uses Major.Minor.Patch versioning. Current version: 1.0.0.0.
| Segment | Triggers Upgrade? | Breaking Changes? |
|---|---|---|
| Major | Always | Yes. Schema changes, removed objects, enum type changes. Upgrade codeunit required. |
| Minor | Sometimes | Rarely. New tables, new fields, new required settings keys. Usually no upgrade codeunit. |
| Patch | No | Never. Bug fixes, compiler warnings, UI-only changes. Can be skipped. |
Guidelines for extensions pinning REFRACT PDF:
- Pin to the current Major version only, e.g.
"version": "2.0". Minor upgrades install automatically without the customer's dependency changing - When REFRACT PDF releases a Major version, update the customer's dependency and test thoroughly
- If a Major release changes field types (e.g.,
Text[20]toEnum), REFRACT PDF includes an upgrade codeunit to migrate existing data. Do not rely on Business Central auto-sync - Call Initialize Default Keys as part of every upgrade runbook on customer systems. It is idempotent and safe
Integration Events
REFRACT PDF publishes integration events via the codeunit "REFRACT PDF Template Header" (77100) and "REFRACT PDF Dispatcher" (77119). All events use [IntegrationEvent(false, false)] - no global subscriptions or bind-on-demand.
Subscribe to events using [EventSubscriber(...)]. Set IsHandled := true to override default behavior.
Template Management Events
OnBeforeGetDefaultLinesTableId
Override which Business Central table stores the lines for a document header. REFRACT PDF auto-detects lines tables for most standard BC documents. The following mappings are built in:
| Header Table | Auto-detected Lines Table |
|---|---|
| Sales Header (36) | Sales Line (37) |
| Purchase Header (38) | Purchase Line (39) |
| Sales Invoice Header (112) | Sales Invoice Line (113) |
| Sales Cr.Memo Header (114) | Sales Cr.Memo Line (115) |
| Purch. Inv. Header (122) | Purch. Inv. Line (123) |
| Purch. Cr. Memo Hdr. (124) | Purch. Cr. Memo Line (125) |
| Service Header (5900) | Service Line (5902) |
| Job (167) | Job Planning Line (1003) |
| Transfer Header (5740) | Transfer Line (5741) |
For custom document tables not in this list, subscribe to override:
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Template Header",
'OnBeforeGetDefaultLinesTableId',
'',
false,
false
)]
local procedure OnBeforeGetDefaultLinesTableId(
HeaderTableId: Integer;
var LinesTableId: Integer;
var IsHandled: Boolean
)
begin
if HeaderTableId <> Database::"My Custom Header" then
exit;
LinesTableId := Database::"My Custom Lines";
IsHandled := true;
end;OnBeforeFindParentFieldId
Override the field ID that links a lines table back to its parent header. REFRACT PDF uses field-name conventions (Document No., No.) to find the parent link field automatically. For non-standard field names, subscribe to provide the correct field ID.
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Template Header",
'OnBeforeFindParentFieldId',
'',
false,
false
)]
local procedure OnBeforeFindParentFieldId(
LinesTableId: Integer;
HeaderTableId: Integer;
var ParentFieldId: Integer;
var IsHandled: Boolean)
begin
if LinesTableId <> Database::"My Custom Lines" then
exit;
// Field 1 on "My Custom Lines" is "Parent Code" - the FK to "My Custom Header"
ParentFieldId := 1;
IsHandled := true;
end;ParentFieldId must be the field number (integer) on the lines table whose value is the primary key of the header record. Setting IsHandled := true prevents REFRACT PDF's convention-based lookup from running.
Dispatcher Events
OnBeforeGetDefaultFolderName
Override the SharePoint folder name where the dispatcher stores generated PDFs and payloads.
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Dispatcher",
'OnBeforeGetDefaultFolderName',
'',
false,
false
)]
local procedure OnBeforeGetDefaultFolderName(
TableId: Integer;
var FolderName: Text;
var IsHandled: Boolean
)
begin
if TableId <> Database::"My Custom Header" then
exit;
FolderName := 'CustomDocuments';
IsHandled := true;
end;Folder naming context:
The folder structure in SharePoint is {SourceSystem}/{FolderName}. For example, if VAULTPDF_SOURCE_SYSTEM = "RefractLogic" and you set FolderName := 'Invoices', documents store at RefractLogic/Invoices/.
OnBeforeGetDocumentIdentifier
Override the document identifier, which controls the PDF filename and the documentId in the dispatcher payload. The default uses the source record's primary key.
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Dispatcher",
'OnBeforeGetDocumentIdentifier',
'',
false,
false
)]
local procedure OnBeforeGetDocumentIdentifier(
RecRef: RecordRef;
var DocNo: Text;
var IsHandled: Boolean
)
var
MyHeader: Record "My Custom Header";
begin
if RecRef.Number <> Database::"My Custom Header" then
exit;
RecRef.SetTable(MyHeader);
DocNo := MyHeader."No." + '-' + MyHeader."Reference No.";
IsHandled := true;
end;OnAfterSendCompletionNotification
Published by "REFRACT PDF Batch Job Runner" (77132). Fires after an async batch job finishes and the HTTP webhook notification has been sent (or skipped). Use this for AL-based post-completion logic - logging, audit entries, secondary notifications - without the overhead of an HTTP endpoint.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Batch Job Runner",
'OnAfterSendCompletionNotification', '', false, false)]
local procedure OnBatchJobComplete(BatchJobEntry: Record "REFRACT PDF Batch Job Entry")
begin
case BatchJobEntry.Status of
BatchJobEntry.Status::Completed:
MyAuditLog.WriteSuccess(
BatchJobEntry."Template Code",
BatchJobEntry."Entry No.",
BatchJobEntry."Completed At");
BatchJobEntry.Status::Partial,
BatchJobEntry.Status::Failed:
MyAlertMgt.SendUrgentAlert(
BatchJobEntry."Template Code",
BatchJobEntry."Status Message");
end;
end;The event receives the fully-populated REFRACT PDF Batch Job Entry record. All fields are available: Status, Template Code, Entry No., Status Message, and completion timestamps. This event does not support IsHandled - it is observation-only.
Settings Management Events
The "REFRACT PDF Settings Mgt" codeunit (77110) publishes events for reading and transforming settings. Use these to inject values from Azure Key Vault, environment variables, or custom configuration systems instead of Business Central Isolated Storage.
OnBeforeGetSetting
Intercept settings reads and provide custom values. This event fires before the normal Business Central storage lookup.
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Settings Mgt",
'OnBeforeGetSetting',
'',
false,
false
)]
local procedure OnBeforeGetSetting(
SettingKey: Code[100];
var Value: Text;
var IsHandled: Boolean
)
var
KeyVaultSecret: Text;
begin
if SettingKey <> 'VAULTPDF_CLIENT_SECRET' then
exit;
// Customer's implementation to fetch from Azure Key Vault
KeyVaultSecret := GetSecretFromKeyVault(SettingKey);
Value := KeyVaultSecret;
IsHandled := true;
end;Common use cases:
- Reading secrets from Azure Key Vault instead of Isolated Storage
- Fetching configuration from a custom settings table
- Overriding settings per company or per user
- Rotating credentials without modifying the REFRACT PDF System Settings page
OnAfterGetSetting
This event fires after the setting value is read. Use it for logging, auditing, or post-processing without blocking the read operation.
[EventSubscriber(
ObjectType::Codeunit,
Codeunit::"REFRACT PDF Settings Mgt",
'OnAfterGetSetting',
'',
false,
false
)]
local procedure OnAfterGetSetting(
SettingKey: Code[100];
var Value: Text
)
begin
// Audit the setting read
LogSettingAccess(SettingKey);
// Transform the value (e.g., decrypt or normalize)
Value := NormalizeValue(Value);
end;SharePoint Provider Events
The "REFRACT PDF SharePoint Provider" codeunit (77117) publishes events covering JSON payload building, folder resolution, token acquisition, and upload lifecycle. These are independent of the Dispatcher events - they control the payload JSON file written to SharePoint, not the dispatch request.
OnBeforeBuildJson / OnAfterBuildJson
OnBeforeBuildJson fires before field mapping runs. Set IsHandled := true and populate JsonText to replace the entire payload. Use this for documents with complex non-standard data structures (e.g. three-way match, multi-table aggregates) where template field-mapping lines would be impractical.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF SharePoint Provider",
'OnBeforeBuildJson', '', false, false)]
local procedure OnBeforeBuildJson(var RecRef: RecordRef; var TemplateCode: Code[20];
var JsonText: Text; var IsHandled: Boolean)
begin
if TemplateCode <> 'MY-CUSTOM' then
exit;
JsonText := BuildMyCustomJson(RecRef);
IsHandled := true;
end;OnAfterBuildJson fires after field mapping completes with JsonText fully built. Modify it to inject additional fields without replacing the entire payload.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF SharePoint Provider",
'OnAfterBuildJson', '', false, false)]
local procedure OnAfterBuildJson(RecRef: RecordRef; TemplateCode: Code[20]; var JsonText: Text)
var
Root: JsonObject;
begin
if not Root.ReadFrom(JsonText) then
exit;
Root.Add('customField', 'injected-value');
Root.WriteTo(JsonText);
end;OnBeforeGetFolderName
Overrides the SharePoint upload folder for a specific template-and-record combination, evaluated after Template Setup folder resolution and before the default table-name fallback. Use this for dynamic per-record folder routing that goes beyond the template's Source System / Source Module path.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF SharePoint Provider",
'OnBeforeGetFolderName', '', false, false)]
local procedure OnBeforeGetFolderName(TableId: Integer; var FolderName: Text; var IsHandled: Boolean)
begin
if TableId <> Database::"My Custom Header" then
exit;
FolderName := 'Archive/' + Format(Today, 0, '<Year4>');
IsHandled := true;
end;Distinction from Dispatcher's OnBeforeGetDefaultFolderName
The Dispatcher event (77119) controls where the generated PDF is stored. The SharePoint Provider event (77117) controls where the source payload JSON is uploaded. Both must resolve to the same logical folder path so the Dispatcher can locate the payload when rendering.
OnBeforeGetAccessToken / OnAfterGetAccessToken
Controls the OAuth token used for all Microsoft Graph calls in the SharePoint Provider (payload upload, template browser, template import).
OnBeforeGetAccessToken - set IsHandled := true and provide AccessToken to bypass the built-in client-credentials flow. Use for Managed Identity or Key Vault-backed token acquisition.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF SharePoint Provider",
'OnBeforeGetAccessToken', '', false, false)]
local procedure OnBeforeGetAccessToken(var AccessToken: Text; var IsHandled: Boolean)
begin
AccessToken := FetchManagedIdentityToken('https://graph.microsoft.com');
IsHandled := AccessToken <> '';
end;OnAfterGetAccessToken fires after the token is obtained (or bypassed). Use for logging or token transformation.
OnBeforeUpload / OnAfterUpload / OnUploadError
Three events bracket the SharePoint HTTP PUT:
| Event | When it fires | IsHandled |
|---|---|---|
OnBeforeUpload | Before the HTTP request - set IsHandled := true to skip the real upload | Yes |
OnAfterUpload | After a successful upload - receives FileName and computed SharePointUrl | No |
OnUploadError | After a failed upload - receives the raw error response body | No |
OnBeforeUpload is the primary seam for test extensions that need to mock SharePoint without real HTTP calls.
Gallery Context Events
The "REFRACT PDF Gallery Context" codeunit (77111) is a SingleInstance context carrier that holds the source record, document number, and page ID between the moment the user clicks Process Document and when the Template Gallery modal opens.
OnBeforeSetContext
Fires before the gallery context is stored. Set IsHandled := true to provide a completely different context - for example, to redirect the gallery to a parent record when triggered from a child record.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Gallery Context",
'OnBeforeSetContext', '', false, false)]
local procedure OnBeforeSetContext(var RecRef: RecordRef; var DocNo: Text;
var PageId: Integer; var IsHandled: Boolean)
var
ParentHeader: Record "My Parent Header";
begin
if RecRef.Number <> Database::"My Child Line" then
exit;
// Load and substitute the parent record
// RecRef.GetTable(ParentHeader); etc.
IsHandled := true;
end;OnAfterSetContext
Fires after the context is stored. Use for logging or injecting additional data into external systems without changing the context itself.
OnResolveQuerySection
Published by "REFRACT PDF JSON Builder", this is the most powerful extensibility point in the system. It allows a subscriber to inject an entire JSON section into the payload - built from any BC Query object - without field-mapping lines on the template.
When to use it: when you need to attach data from a multi-table join to the payload and individual field-mapping lines or hop chains would be impractical (e.g. all bank accounts for a vendor, all open invoices for a customer).
Setup on the template - add a template line with:
- Section =
Header(orLines) - JSON Group = the key name under which the section will appear in the payload (e.g.
vendor_bank) - Query Object ID = the BC Query object ID to resolve
- Leave Table ID, Field ID, Link Field ID blank
Subscriber signature:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF JSON Builder",
'OnResolveQuerySection', '', false, false)]
local procedure HandleMyQuery(
QueryObjectId: Integer;
ParentRecRef: RecordRef;
TemplateCode: Code[20];
GroupName: Text;
Section: Enum "REFRACT PDF Section";
var SectionJson: JsonObject;
var IsHandled: Boolean)Populate SectionJson and set IsHandled := true. The JSON Builder writes SectionJson into the payload under GroupName. When IsHandled = false, the section is skipped.
Worked example - Vendor Bank Details (Query 77150):
The reference implementation ships in REFRACT PDF Query Subscriber (77115). Configure a Purchase Order template with Query Object ID = 77150 and JSON Group = vendor_bank to attach all bank accounts for the order's vendor.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF JSON Builder",
'OnResolveQuerySection', '', false, false)]
local procedure HandleVendorBankDetails(
QueryObjectId: Integer; ParentRecRef: RecordRef;
TemplateCode: Code[20]; GroupName: Text;
Section: Enum "REFRACT PDF Section";
var SectionJson: JsonObject; var IsHandled: Boolean)
var
BankQuery: Query "REFRACT PDF Vendor Bank Details";
VendorNo: Code[20];
AccountObj: JsonObject;
AccountsArray: JsonArray;
FldRef: FieldRef;
begin
if QueryObjectId <> Query::"REFRACT PDF Vendor Bank Details" then
exit;
FldRef := ParentRecRef.Field(2); // "Buy-from Vendor No." on Purchase Header
VendorNo := CopyStr(Format(FldRef.Value), 1, 20);
if VendorNo = '' then exit;
BankQuery.SetFilter(BankQuery.VendorNo, '%1', VendorNo);
BankQuery.Open();
while BankQuery.Read() do begin
Clear(AccountObj);
AccountObj.Add('bank_code', BankQuery.BankCode);
AccountObj.Add('bank_account_no', BankQuery.BankAccountNo);
AccountObj.Add('iban', BankQuery.IBAN);
AccountObj.Add('currency_code', BankQuery.BankCurrencyCode);
AccountsArray.Add(AccountObj);
end;
BankQuery.Close();
SectionJson.Add('accounts', AccountsArray);
IsHandled := true;
end;Resulting payload structure:
{
"vendor_bank": {
"accounts": [
{ "bank_code": "HSBC", "bank_account_no": "12345678", "iban": "GB29NWBK...", "currency_code": "GBP" }
]
}
}Extensible Enums
The following enums are marked as extensible and can be extended by partner or customer extensions:
| Enum | Object ID | Shipped Values |
|---|---|---|
REFRACT PDF Template Type | 77102 | Template, Workflow |
REFRACT PDF Workflow Enforcement | 77104 | None, Validate, Block |
REFRACT PDF ESign Mode | 77105 | Sequential, Parallel |
REFRACT PDF Dispatch Mode | 77189 | Standard, Per Group, Per Record, Single Array, Grouped Array |
Extending an Enum
Example: Add a custom template type for internal approval workflows.
enumextension 77200 "My Template Types" extends "REFRACT PDF Template Type"
{
value(100; "Custom Approval")
{
Caption = 'Custom Approval Process';
}
}After extending the enum, you can:
- Select
Custom Approvalas a template type in REFRACT PDF Templates - Subscribe to
OnBeforeGetDocumentIdentifieror other events to implement custom behavior for this type - Surface new template settings fields specific to the customer's template type
Adding REFRACT PDF Actions to Custom Pages
REFRACT PDF ships page extensions for standard Business Central document pages (Sales Order, Purchase Invoice, etc.). To enable REFRACT PDF on a custom or non-standard page, you write a page extension - but you can use the Custom Page Registry to let an administrator toggle the buttons on or off at runtime without redeployment.
How the Registry Works
The registry does not inject UI dynamically. Business Central requires a compiled pageextension to add buttons to a page - that cannot be avoided. What the registry provides is a Visible property gate: the page extension reads IsPageRegistered(PageId) when the page opens and uses the result to show or hide the entire REFRACT Platform action group.
| Without registry | With registry |
|---|---|
| Buttons are always visible once the extension is deployed | Admin can enable/disable buttons per page from REFRACT PDF System Settings → Custom Page Registry |
| Removing buttons requires redeployment | Toggle Enabled = false on the registry record to hide buttons instantly, no redeploy |
| Different buttons for different page IDs requires separate binaries | One binary per page; registry controls visibility per environment |
The Complete Boilerplate Pattern
This is the recommended pattern for all custom page extensions. Copy and adapt it - only change the extends target, the PageId constant, and the RecRef usage:
namespace RefractLogic.REFRACT PDF;
using Contoso.MyModule; // your namespace
pageextension 77201 "REFRACT PDF My Custom Page Ext" extends "My Custom Page"
{
actions
{
addlast(Processing)
{
group(VaultPlatformGroup)
{
Caption = 'REFRACT Platform';
Image = Certificate;
Visible = VaultActionsVisible;
action(VaultProcessDocument)
{
ApplicationArea = All;
Caption = 'Process Document';
ToolTip = 'Generate a PDF for this record using the REFRACT Platform template gallery.';
Image = Approve;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.ProcessDocument(RecRef, Rec."No.", 77201);
end;
}
action(VaultOpenDocument)
{
ApplicationArea = All;
Caption = 'Open Document';
ToolTip = 'Open the latest PDF generated for this record.';
Image = Open;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.OpenDocument(RecRef);
end;
}
action(VaultOpenWorkflowDocument)
{
ApplicationArea = All;
Caption = 'Open Workflow Document';
Image = Certificate;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.OpenWorkflowDocument(RecRef);
end;
}
action(VaultSyncWorkflow)
{
ApplicationArea = All;
Caption = 'Sync Workflow Status';
Image = Refresh;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.SyncWorkflowStatus(RecRef);
end;
}
action(REFRACT LifecyclePortal)
{
ApplicationArea = All;
Caption = 'Open Lifecycle Workspace';
Image = InteractionLog;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.OpenLifecyclePortal(RecRef);
end;
}
}
}
addlast(Promoted)
{
group(Category_VaultPlatform)
{
Caption = 'REFRACT Platform';
Visible = VaultActionsVisible;
actionref(VaultProcessDocument_Promoted; VaultProcessDocument) { }
actionref(VaultOpenDocument_Promoted; VaultOpenDocument) { }
actionref(VaultOpenWorkflowDocument_Promoted; VaultOpenWorkflowDocument) { }
actionref(VaultSyncWorkflow_Promoted; VaultSyncWorkflow) { }
actionref(REFRACT LifecyclePortal_Promoted; REFRACT LifecyclePortal) { }
}
}
}
var
VaultActionsVisible: Boolean;
trigger OnOpenPage()
var
PlatformActions: Codeunit "REFRACT PDF Platform Actions";
begin
VaultActionsVisible := PlatformActions.IsPageRegistered(77201);
end;
}Key points in the boilerplate:
VaultActionsVisibleis a page-level Boolean set inOnOpenPageviaIsPageRegistered(PageId)- Every action and the group itself uses
Visible = VaultActionsVisible- nothing is hidden by accident PlatformActions.ProcessDocument(RecRef, Rec."No.", 77201)- pass the page ID (not the table ID) as the third argument so the Template Gallery filters to templates matching this page77201in theIsPageRegisteredcall must match the page ID of the page this extension extends
Registering the Page
Deploy the extension and register the page so buttons appear:
Open REFRACT PDF System Settings in Business Central
Click Custom Page Registry in the action bar to open the registry page
Click New and fill in:
| Field | Value |
|---|---|
| Page ID | The BC page ID this extension extends (e.g. 77201) |
| Action Group Label | Label for the action group in the action bar (e.g. REFRACT Platform) |
| Enabled | true - buttons are visible when this is enabled |
Page Name is auto-filled from the Page ID.
Reload the custom page. The REFRACT Platform action group appears.
To hide the buttons without uninstalling the extension, set Enabled = false on the registry record. Buttons disappear the next time any user opens the page - no redeploy, no downtime.
Seeding the Registry Entry Automatically
If your extension should register its own pages on install, call EnsureRegistryEntry from your upgrade codeunit or from the InitializeDefaultSettings subscriber:
// In your own setup codeunit or an event subscriber on InitializeDefaultSettings:
local procedure EnsureMyPageRegistered()
var
Registry: Record "REFRACT PDF Custom Page Registry";
begin
if Registry.Get(77201) then exit; // Already registered
Registry.Init();
Registry."Page ID" := 77201;
Registry."Action Group Label" := 'REFRACT Platform';
Registry.Enabled := true;
Registry.Insert(true);
end;This is idempotent - safe to call on every upgrade. The if Registry.Get(...) then exit guard prevents duplicate records.
Workflow Enforcement
Built-in Enforcement Points
When Workflow Enforcement Mode = Block or Validate on a template, REFRACT PDF automatically checks the workflow state before allowing certain Business Central operations. The following operations are covered out of the box:
| Operation | BC Event | Notes |
|---|---|---|
| Post a Sales document | Sales-Post.OnBeforePostSalesDoc | Sales Orders, Invoices, Credit Memos |
| Post a Purchase document | Purch.-Post.OnBeforePostPurchaseDoc | Purchase Orders, Invoices, Credit Memos |
| Post a Service Order | Service-Post.OnBeforePostWithLines | Fires before service invoice/credit memo creation |
| Ship a Transfer Order | TransferOrder-Post Shipment.OnBeforeTransferOrderPostShipment | Fires on shipment; add a separate subscriber for receipt if needed |
| Unblock a Vendor | Vendor.OnBeforeModifyEvent | Only fires when removing the block (blocked → unblocked) |
| Unblock a Customer | Customer.OnBeforeModifyEvent | Same direction guard |
| Unblock an Item | Item.OnBeforeModifyEvent | Same direction guard |
Extending Enforcement to Additional Operations
REFRACT PDF Workflow Enforcer.CheckBeforeOperation (Access = Public) is the single enforcement entry point. Call it from any AL event subscriber to apply the same Block/Validate rules to any operation.
The method is fully table-agnostic - it reads the workflow state from the Record Link attached to the passed RecRef and evaluates the enforcement mode configured on the matching template.
Signature:
procedure CheckBeforeOperation(RecRef: RecordRef; OperationLabel: Text)RecRef- aRecordRefpointing to the document record (e.g. the Service Header, Assembly Header, or custom record)OperationLabel- a short description used in the error/warning message shown to the user (e.g.'post this assembly order')
Example: Add enforcement for Assembly Order posting
// In a new subscriber codeunit in your extension:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Assembly-Post", 'OnBeforePostAssemblyDoc', '', false, false)]
local procedure CheckBeforeAssemblyPost(var AssemblyHeader: Record "Assembly Header"; var SuppressCommit: Boolean)
var
Enforcer: Codeunit "REFRACT PDF Workflow Enforcer";
RecRef: RecordRef;
begin
RecRef.GetTable(AssemblyHeader);
Enforcer.CheckBeforeOperation(RecRef, 'post this assembly order');
end;Example: Add enforcement for a custom document posting codeunit
[EventSubscriber(ObjectType::Codeunit, Codeunit::"My Custom Posting", 'OnBeforePostMyDoc', '', false, false)]
local procedure CheckBeforeMyDocPost(var MyHeader: Record "My Custom Header")
var
Enforcer: Codeunit "REFRACT PDF Workflow Enforcer";
RecRef: RecordRef;
begin
RecRef.GetTable(MyHeader);
Enforcer.CheckBeforeOperation(RecRef, 'post this document');
end;How enforcement behaves after you call CheckBeforeOperation:
| Condition | Block mode | Validate mode |
|---|---|---|
| No REFRACT PDF link on the record | Passes silently - enforcement does not apply | Same |
| REFRACT PDF link exists but not a workflow document | Passes silently | Same |
| Workflow complete | Passes unconditionally | Same |
| Workflow pending | Error - operation is blocked | Warning dialog - user can proceed or cancel |
| Workflow rejected | Error - operation is blocked | Warning dialog - user can proceed or cancel |
| No GUI (background Job Queue) | Error regardless of mode | Error (same as Block - no dialog possible in headless context) |
Conditionally Exempting Specific Records
Subscribe to OnBeforeWorkflowEnforcement and set IsHandled := true to bypass enforcement for specific records, operations, or conditions - without modifying the enforcement codeunit:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Workflow Enforcer",
'OnBeforeWorkflowEnforcement', '', false, false)]
local procedure ExemptInternalTransfers(RecRef: RecordRef; OperationLabel: Text; var IsHandled: Boolean)
var
TransferHeader: Record "Transfer Header";
begin
// Allow internal warehouse transfers to bypass enforcement
if RecRef.Number <> Database::"Transfer Header" then exit;
RecRef.SetTable(TransferHeader);
if TransferHeader."Transfer-from Code" = TransferHeader."Transfer-to Code" then
IsHandled := true; // Same location - no enforcement needed
end;Setting IsHandled := true causes CheckBeforeOperation to exit immediately without evaluating any workflow state. Use this pattern for:
- Records that should always be exempt (internal movements, system-generated documents)
- Company-specific bypass rules (e.g., a specific department code is always allowed to post)
- Temporary bypass during data migration or initial setup
Dispatcher Payload Reference
For the complete payload structure, all field descriptions, and templateSettings fields, see the Event & Payload Reference page.
Event Subscription Best Practices
Use Clear, Descriptive Procedure Names
// Good
local procedure OnBeforeGetDocumentIdentifier_InvoiceNumbering(...)
// Avoid
local procedure MyOverride(...)Log Errors Explicitly
begin
if not FetchValueFromKeyVault(SettingKey, KeyVaultValue) then begin
Session.LogMessage('0001',
StrSubstNo('Failed to fetch key vault secret: %1', SettingKey),
Verbosity::Error);
exit;
end;
end;Avoid Blocking Operations in OnAfterGetSetting
The OnAfterGetSetting event should not throw errors or prevent normal operation. Use it only for logging and non-critical transformations.
// Good - logging only
local procedure OnAfterGetSetting_Logging(SettingKey: Code[100]; var Value: Text)
begin
LogSettingRead(SettingKey); // Non-blocking
end;
// Avoid - blocking logic
local procedure OnAfterGetSetting_Bad(SettingKey: Code[100]; var Value: Text)
begin
if ValidateSecretExpiry(Value) = false then
Error('Secret expired'); // Blocks normal flow
end;Common Extension Scenarios
Override the default SharePoint folder naming convention to match the customer's document filing structure.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Dispatcher", 'OnBeforeGetDefaultFolderName', '', false, false)]
local procedure OnBeforeGetDefaultFolderName(TableId: Integer; var FolderName: Text; var IsHandled: Boolean)
var
CompanyInfo: Record "Company Information";
begin
CompanyInfo.Get();
case TableId of
Database::"Sales Invoice Header":
FolderName := 'AR/' + Format(Today, 0, '<Year4>') + '/' + Format(Today, 0, '<Month,2>');
Database::"Purchase Invoice Header":
FolderName := 'AP/' + Format(Today, 0, '<Year4>') + '/' + Format(Today, 0, '<Month,2>');
else
exit;
end;
IsHandled := true;
end;Fetch dispatcher credentials from Azure Key Vault instead of storing them in Business Central Isolated Storage.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Settings Mgt", 'OnBeforeGetSetting', '', false, false)]
local procedure OnBeforeGetSetting_KeyVault(SettingKey: Code[100]; var Value: Text; var IsHandled: Boolean)
var
AzureKeyVault: Codeunit "Azure Key Vault";
SecretName: Text;
begin
if SettingKey not in ['VAULTPDF_DISPATCHER_CLIENT_SECRET', 'VAULTPDF_CLIENT_SECRET'] then
exit;
SecretName := 'REFRACT PDF-' + SettingKey;
if not AzureKeyVault.GetSecret(SecretName, Value) then begin
Session.LogMessage('0002', StrSubstNo('Key Vault secret not found: %1', SecretName), Verbosity::Error);
exit;
end;
IsHandled := true;
end;Automatically register templates into REFRACT PDF from a JSON configuration file when a custom extension installs.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Upgrade BC Codeunit", 'OnRun', '', false, false)]
local procedure OnUpgrade_RegisterTemplates()
var
TemplateRegistration: Codeunit "My Template Registration";
begin
TemplateRegistration.RegisterDefaultTemplates();
end;
codeunit 77202 "My Template Registration"
{
procedure RegisterDefaultTemplates()
begin
// Read templates from JSON and populate REFRACT PDF Templates table
end;
}Maintain separate REFRACT PDF credentials per company without modifying the global System Settings.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"REFRACT PDF Settings Mgt", 'OnBeforeGetSetting', '', false, false)]
local procedure OnBeforeGetSetting_MultiCompany(SettingKey: Code[100]; var Value: Text; var IsHandled: Boolean)
var
CompanySettings: Record "My Company REFRACT PDF Settings";
begin
if not CompanySettings.Get(CompanyName, SettingKey) then
exit;
Value := CompanySettings."Setting Value";
IsHandled := true;
end;Complete Integration Event Reference
For the full 22-event table with codeunit IDs, IsHandled flags, and one-line purpose for every event, see the Event & Payload Reference page.
Troubleshooting Extension Issues
Event Not Firing
Check:
- Codeunit ID is correct (77100, 77110, 77119)
- Event name spelling matches exactly
- All filter parameters match (usually all empty strings)
- Extension publishes REFRACT PDF as a dependency in
app.json
IsHandled Not Working
Ensure you set IsHandled := true before setting the return variable:
// Correct
IsHandled := true;
DocNo := MyCustomValue;
// Incorrect - IsHandled set after
DocNo := MyCustomValue;
IsHandled := true;Changes Not Appearing
- Restart the Business Central service (or reload the browser)
- Verify the extension is installed in Extension Management
- Check that the customer's extension is published after REFRACT PDF
Automated Document Generation - REFRACT PDF Business Central
Trigger REFRACT PDF document generation automatically on posting, from external systems via API, or programmatically from AL code. Covers the Generation Queue, job setup, and monitoring.
Event & Payload Reference - REFRACT PDF Business Central
Complete integration event reference for all 22 REFRACT PDF AL events, plus the full dispatcher payload structure with all fields documented.