Developer Guide - VaultPDF Business Central
Extend VaultPDF with custom AL code. Subscribe to integration events, override templates, customize storage paths, and integrate with third-party systems.
Extension Architecture
VaultPDF 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 VaultPDF must declare a dependency in their app.json:
{
"dependencies": [
{
"appId": "b6f75cab-2b7a-4998-ac28-9acc767b8b4d",
"name": "VaultPDF",
"publisher": "Refract Logic LLC",
"version": "1.0.0.0"
}
]
}Versioning Strategy
VaultPDF 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 VaultPDF:
- Pin to the current Major version only, e.g.
"version": "2.0". Minor upgrades install automatically without the customer's dependency changing - When VaultPDF releases a Major version, update the customer's dependency and test thoroughly
- If a Major release changes field types (e.g.,
Text[20]toEnum), VaultPDF 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
VaultPDF publishes integration events via the codeunit "VaultPDF Template Header" (77100) and "VaultPDF 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. VaultPDF 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::"VaultPDF 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. VaultPDF 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::"VaultPDF 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 VaultPDF'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::"VaultPDF 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::"VaultPDF 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 "VaultPDF 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::"VaultPDF Batch Job Runner",
'OnAfterSendCompletionNotification', '', false, false)]
local procedure OnBatchJobComplete(BatchJobEntry: Record "VaultPDF 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 VaultPDF 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 "VaultPDF 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::"VaultPDF 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 VaultPDF 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::"VaultPDF 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 "VaultPDF 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::"VaultPDF 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::"VaultPDF 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::"VaultPDF 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::"VaultPDF 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 "VaultPDF 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::"VaultPDF 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 "VaultPDF 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::"VaultPDF JSON Builder",
'OnResolveQuerySection', '', false, false)]
local procedure HandleMyQuery(
QueryObjectId: Integer;
ParentRecRef: RecordRef;
TemplateCode: Code[20];
GroupName: Text;
Section: Enum "VaultPDF 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 VaultPDF 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::"VaultPDF JSON Builder",
'OnResolveQuerySection', '', false, false)]
local procedure HandleVendorBankDetails(
QueryObjectId: Integer; ParentRecRef: RecordRef;
TemplateCode: Code[20]; GroupName: Text;
Section: Enum "VaultPDF Section";
var SectionJson: JsonObject; var IsHandled: Boolean)
var
BankQuery: Query "VaultPDF Vendor Bank Details";
VendorNo: Code[20];
AccountObj: JsonObject;
AccountsArray: JsonArray;
FldRef: FieldRef;
begin
if QueryObjectId <> Query::"VaultPDF 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 |
|---|---|---|
VaultPDF Template Type | 77102 | Template, Workflow |
VaultPDF Workflow Enforcement | 77104 | None, Validate, Block |
VaultPDF ESign Mode | 77105 | Sequential, Parallel |
VaultPDF 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 "VaultPDF Template Type"
{
value(100; "Custom Approval")
{
Caption = 'Custom Approval Process';
}
}After extending the enum, you can:
- Select
Custom Approvalas a template type in VaultPDF 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 VaultPDF Actions to Custom Pages
VaultPDF ships page extensions for standard Business Central document pages (Sales Order, Purchase Invoice, etc.). To enable VaultPDF 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 VaultPDF 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.VaultPDF;
using Contoso.MyModule; // your namespace
pageextension 77201 "VaultPDF 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 "VaultPDF 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 "VaultPDF 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 "VaultPDF 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 "VaultPDF Platform Actions";
RecRef: RecordRef;
begin
RecRef.GetTable(Rec);
PlatformActions.SyncWorkflowStatus(RecRef);
end;
}
action(VaultLifecyclePortal)
{
ApplicationArea = All;
Caption = 'Open Lifecycle Workspace';
Image = InteractionLog;
Visible = VaultActionsVisible;
trigger OnAction()
var
PlatformActions: Codeunit "VaultPDF 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(VaultLifecyclePortal_Promoted; VaultLifecyclePortal) { }
}
}
}
var
VaultActionsVisible: Boolean;
trigger OnOpenPage()
var
PlatformActions: Codeunit "VaultPDF 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 VaultPDF 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 "VaultPDF 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, VaultPDF 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
VaultPDF 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 "VaultPDF 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 "VaultPDF 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 VaultPDF link on the record | Passes silently - enforcement does not apply | Same |
| VaultPDF 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::"VaultPDF 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::"VaultPDF 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::"VaultPDF 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 := 'VaultPDF-' + 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 VaultPDF 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 VaultPDF Templates table
end;
}Maintain separate VaultPDF credentials per company without modifying the global System Settings.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"VaultPDF Settings Mgt", 'OnBeforeGetSetting', '', false, false)]
local procedure OnBeforeGetSetting_MultiCompany(SettingKey: Code[100]; var Value: Text; var IsHandled: Boolean)
var
CompanySettings: Record "My Company VaultPDF 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 VaultPDF 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 VaultPDF
Automated Document Generation - VaultPDF Business Central
Trigger VaultPDF 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 - VaultPDF Business Central
Complete integration event reference for all 22 VaultPDF AL events, plus the full dispatcher payload structure with all fields documented.