REFRACT Platform for Dynamics 365 Business Central

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.

Overview

REFRACT PDF supports four ways to trigger document generation. All four paths funnel into the same Generation Queue - a persistent table that the VPF Gen Processor job drains asynchronously.

TriggerWhen to use
Interactive galleryUser opens a document and clicks Generate - standard on-demand use
Auto-generate on postTemplate flag queues generation automatically when a BC document is posted
External APIAn integrated system (ERP, custom app, integration platform) calls the BC API after creating or posting a document
AL codeA partner or customer extension calls the queue table helper directly

The first three scenarios are covered in detail below. The interactive gallery is described in Processing Documents.


Architecture: The Generation Queue

The Generation Queue (table 73524588 "VPF Gen Request", caption "REFRACT Generation Request") decouples the act of requesting PDF generation from the act of executing it. Any trigger path inserts a row; the Gen Processor (codeunit 73524611 "VPF Gen Processor"), running as a recurring Job Queue entry, picks up Pending rows in batches (up to 50 per run) and drives each one through the full pipeline (JSON build → SharePoint upload → Dispatcher call → Record Link).

Trigger (Post event / API call / AL code)
        ↓
  Gen Request row (Status = Pending)
        ↓
  Job Queue → VPF Gen Processor (codeunit 73524611)
        ↓
  SharePoint Upload + Dispatcher Call
        ↓
  Status = Completed  (or Failed / Dead after retries)

Key design properties:

  • Trigger-agnostic - the processor does not know or care how a row was created
  • Fixed retry count - failed rows are retried up to 3 times (hardcoded when the row is queued) before being marked Dead; there is no backoff between attempts
  • Concurrent-safe - the processor marks a row Processing before committing, so parallel Job Queue workers cannot double-process the same request
  • Non-blocking for posting - the posting subscriber inserts Gen Request rows in a new transaction after posting commits, so a subscriber failure never rolls back a completed posting

Scenario 1: Auto-Generate on Post

Enable this flag on a template to have REFRACT PDF automatically queue a generation request whenever a Business Central document from that template's source table is posted.

Supported posting events:

  • Sales Invoice posted from a Sales Order or Sales Invoice
  • Sales Credit Memo
  • Purchase Invoice
  • Purchase Credit Memo

Enable Auto Generate on Post on the Template

Open REFRACT Templates and open the template you want to automate.

In the Variants subform, select the variant to automate (usually the default variant) and click Edit Variant Settings. In the Dispatch section, enable the Auto Generate on Post toggle.

Standard Dispatch Mode Only

Auto Generate on Post is only available for variants with Dispatch Mode = Standard. Batch modes (Per Group, Per Record, Single Array, Grouped Array) are not supported for posting automation because each posted document generates exactly one header record - batch modes require a filtered record set selected at dispatch time.

Verify that the template's Source Table ID maps to the correct Business Central document table. The posting subscriber matches on this table ID:

Document typeSource Table ID
Sales Invoice112 (Sales Invoice Header)
Sales Credit Memo114 (Sales Cr.Memo Header)
Purchase Invoice122 (Purch. Inv. Header)
Purchase Credit Memo124 (Purch. Cr. Memo Hdr.)

Create the VPF Gen Processor Job Queue Entry

The posting subscriber inserts queue rows, but nothing processes them unless the Gen Processor job is running.

Search for Job Queue Entries in Business Central and click New.

Configure the entry with these values:

FieldValue
Object Type to RunCodeunit
Object ID to Run73524611
Object Caption to RunVPF Gen Processor
Recurring JobYes
Run on Mondays ... SundaysEnable all days (or the days matching your posting schedule)
Starting Time00:00:00
Ending Time23:59:59
No. of Minutes between Runs2 (poll every 2 minutes) or 5 for lower-frequency environments
StatusReady

One Job Entry is Sufficient

A single recurring Job Queue entry processes all pending generation requests for all templates. Do not create one entry per template - this wastes Job Queue worker capacity and can cause contention.

Post a Document and Verify

Post any Sales Order or Purchase Invoice that uses a template with Auto Generate on Post enabled.

After posting, open REFRACT Generation Queue from the Tell Me bar (or via the Generation Queue action on the Template Card).

You should see a new row with:

  • Source = Posting Event
  • Status = Pending (or Processing if the job picked it up quickly, or Completed if already done)
  • Template Code = the template that matched the posted document

Within the next Job Queue cycle, the row transitions to Completed and a Record Link (PDF link) is attached to the posted document.


Scenario 2: External System Triggers Generation via API

Use this pattern when an external application - a custom ERP, an integration platform (Power Automate, Logic Apps, Boomi, MuleSoft), or a BC API client - creates or posts a BC document and then wants to trigger REFRACT PDF generation without requiring a user to open Business Central.

API Endpoint

POST /api/refractlogic/vaultpdf/v1.0/companies({companyId})/generationRequests

This is page 73524613 "VPF Gen Request API" (APIPublisher='refractlogic', APIGroup='vaultpdf', APIVersion='v1.0', EntityName='generationRequest'), reading and inserting into table 73524588 "VPF Gen Request".

Authentication: Standard BC OData API authentication (OAuth 2.0 with Dynamics 365 Business Central scope, or Basic auth for on-premises).

Request body:

{
  "templateCode": "PINV-STANDARD",
  "tableId": 122,
  "docNo": "PINV-00001",
  "externalRef": "ERP-JOB-REF-789"
}
FieldTypeRequiredDescription
templateCodestringYesREFRACT PDF template code, e.g. PINV-STANDARD. Must exist and be enabled.
tableIdintegerYesBusiness Central table ID for the posted document. See table IDs below.
docNostringYesThe document number (No. field) of the posted document.
externalRefstringNoFree-text reference from the calling system (e.g. correlation ID, job number). Stored on the queue row for traceability.

Common table IDs:

DocumentTable ID
Sales Invoice Header112
Sales Credit Memo Header114
Purchase Invoice Header122
Purchase Credit Memo124
Sales Order Header (pre-post)36
Purchase Header (pre-post)38

Response (201 Created):

{
  "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "templateCode": "PINV-STANDARD",
  "tableId": 122,
  "docNo": "PINV-00001",
  "status": "Pending",
  "externalRef": "ERP-JOB-REF-789",
  "createdAt": "2026-06-20T14:32:00Z"
}

Polling for Completion

Poll the returned requestId to track processing:

GET /api/refractlogic/vaultpdf/v1.0/companies({companyId})/generationRequests({requestId})

Response when complete:

{
  "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "Completed",
  "blobPath": "RefractLogic/PurchaseInvoices/PINV-00001_PINV-STANDARD.pdf",
  "correlationId": "vpdf-corr-xyz",
  "processedAt": "2026-06-20T14:33:15Z"
}
StatusMeaning
PendingQueued; not yet picked up by the processor
ProcessingProcessor is actively working on this request
CompletedPDF generated and stored; blobPath and correlationId are populated
FailedProcessing failed; errorMessage contains the reason; will be retried
Dead (Max Retries Reached)All retry attempts exhausted; manual intervention required

Polling Interval Recommendation

The Gen Processor runs every 2 - 5 minutes by default. Poll at a 30 - 60 second interval and implement an overall timeout of 15 - 30 minutes for large documents or documents involving workflow routing.

End-to-End Example: External System Posts an AP Invoice

This is the canonical scenario: an external procurement system creates and posts a Purchase Invoice via the BC standard API, then triggers REFRACT PDF generation.

Create and post the Purchase Invoice using the standard BC purchaseInvoices API (or equivalent custom API). Retrieve the posted invoice's No. from the response.

Request PDF generation by calling the REFRACT generation API with the invoice number and the appropriate template code.

POST /api/refractlogic/vaultpdf/v1.0/companies({companyId})/generationRequests
Content-Type: application/json

{
  "templateCode": "PINV-STANDARD",
  "tableId": 122,
  "docNo": "PINV-00001",
  "externalRef": "PROC-SYSTEM-JOB-42"
}

Store the requestId from the 201 response. Use it to poll for completion.

Poll until status = Completed and use the blobPath or open the REFRACT Lifecycle portal via correlationId to retrieve the generated PDF.

Power Automate / Logic Apps Integration

To integrate with Power Automate or Azure Logic Apps, use the Business Central connector's Call a Bound Action action - or make a direct HTTP call to the OData endpoint using the HTTP with Microsoft Entra ID connector.

Recommended flow pattern:

  1. Trigger: When a record is created/modified in Business Central (or on a schedule)
  2. Action: POST to generationRequests using HTTP connector
  3. Action: Do-Until loop polling GET generationRequests({requestId}) until status = Completed or timeout
  4. Action: Send Teams/email notification with blobPath

Scenario 3: AL Developer Queues Programmatically

A partner or customer extension can queue generation requests directly from AL code without going through a user action or a posting event.

Using the Public API: QueueGenerationForPosting

QueueGenerationForPosting(TableId: Integer; var SourceRecRef: RecordRef) on codeunit 73524612 "VPF Posting Subscriber" is the same path the built-in posting subscriber uses. It does not take a template code - it queues one generation request for every enabled template variant configured with Auto Generate on Post whose source table matches TableId. Use it when you want "everything configured for this table" behavior from your own posting hook:

local procedure TriggerGenerationForMyDoc(PostedHeader: Record "My Posted Header")
var
    PostingSubscriber: Codeunit "VPF Posting Subscriber";
    RecRef: RecordRef;
begin
    RecRef.GetTable(PostedHeader);
    PostingSubscriber.QueueGenerationForPosting(Database::"My Posted Header", RecRef);
end;

Calling it fires OnBeforeQueueForTable (described below) before any variant is queued.

OnBeforeQueueForTable

Published by codeunit 73524612 "VPF Posting Subscriber". Fires once per table before the posting subscriber queues any generation requests for it. The real signature has no template-code parameter - at this point no specific template or variant has been chosen yet, so a subscriber can only skip the default behavior entirely, not redirect it to a substitute template:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"VPF Posting Subscriber",
  'OnBeforeQueueForTable', '', false, false)]
local procedure OnBeforeQueueForTable(
    TableId: Integer;
    var SourceRecRef: RecordRef;
    var IsHandled: Boolean)
begin
    // Skip the default queue behavior entirely for a specific custom table;
    // handle queuing yourself (e.g. call QueueInternal with your own logic).
    if TableId = Database::"My Custom Header" then
        IsHandled := true;
end;

Setting IsHandled := true prevents the default posting subscriber from queuing anything for that table.

Queue a Specific Template Internally

Use QueueInternal(TemplateCode: Code[20]; SourceRecRef: RecordRef) on table 73524588 "VPF Gen Request" when you want to queue one named template for a record, from a background codeunit, upgrade routine, or any non-posting context:

local procedure ReprocessDocument(DocNo: Code[20])
var
    InvHeader: Record "Sales Invoice Header";
    GenRequest: Record "VPF Gen Request";
    RecRef: RecordRef;
begin
    InvHeader.Get(DocNo);
    RecRef.GetTable(InvHeader);
    GenRequest.QueueInternal('SINV-STANDARD', RecRef);
end;

Queue from a Custom Posting Flow (Low-Level)

QueueFromPosting(TemplateVariantId: Code[50]; SourceRecRef: RecordRef) on table 73524588 "VPF Gen Request" is the low-level entry point the posting subscriber itself calls, once per matched template variant. TemplateVariantId must match an existing row in table "VPF Template Setup"; the procedure resolves that variant's parent Template Code automatically:

local procedure AfterMyDocumentPosting(var PostedHeader: Record "My Posted Header")
var
    GenRequest: Record "VPF Gen Request";
    RecRef: RecordRef;
begin
    RecRef.GetTable(PostedHeader);
    GenRequest.QueueFromPosting('SINV-STANDARD-DEFAULT', RecRef);
end;

It sets Source = Posting Event, captures the Record ID directly from the RecordRef, and resolves Doc No. from the record's No. field. It is safe to call within a posting transaction - the Gen Request insert is committed independently.

Queue from an API (Server-Side Validation)

If you need to add validation logic before accepting an API-originated request, create a page extension on page 73524613 "VPF Gen Request API" and add your logic in OnInsertRecord.


Maximum Retries

The Gen Processor retries a failed request up to 3 times before marking it Dead (Max Retries Reached). This count is hardcoded on the "VPF Gen Request" table's OnInsert trigger (if "Max Retries" = 0 then "Max Retries" := 3) and re-applied defensively by the Gen Processor itself. There is no backoff between attempts and no system setting that changes this number for automatic generation.

REFRACTPDF_GEN_QUEUE_MAX_RETRIES does something different

The REFRACTPDF_GEN_QUEUE_MAX_RETRIES system setting exists, but it only prefills the "Max Attempts" field when you schedule a batch job from the Template Card - that value feeds Business Central's own Job Queue Entry retry mechanism for the scheduled task, not the per-record generation retries described above.


Log Retention and Auto-Purge

The Gen Processor (codeunit 73524611 "VPF Gen Processor") purges old log data on every run, controlled by the REFRACTPDF_LOG_RETENTION_DAYS setting (default 90; set to 0 to disable).

What gets purged, older than the retention cutoff:

  • "VPF Batch Job Entry" rows with status Completed, Failed, or Partial, and their linked "VPF Batch Log Detail" rows
  • "VPF Batch Log Entry" rows, filtered by their own Log DateTime (independent of any batch job's status)
  • "VPF Gen Request" rows with status Completed, filtered by Created At

What is not purged:

  • Batch jobs still Queued or Running, regardless of age
  • "VPF Gen Request" rows with status Failed, Dead, Pending, or Processing, regardless of age

There is no separate "Upload Log" page or table - generation and batch history are recorded on "VPF Batch Log Entries" (page 73524600) and its detail sub-page "VPF Batch Log Detail" (page 73524610).


Monitoring the Generation Queue

Generation Queue Page

Open REFRACT Generation Queue from the Tell Me bar to view all requests across all templates.

From the Template Card, the Generation Queue action in the action bar opens the same list pre-filtered to the current template.

Page columns:

ColumnDescription
Request IDUnique identifier (GUID) for this generation request
Template CodeThe REFRACT PDF template used
Doc No.Document number from the source BC record
StatusCurrent state: Pending, Processing, Completed, Failed, Dead
SourceHow the request was created: Internal, API, Posting Event, Batch
Retry CountNumber of processing attempts so far
Error MessageError text from the last failed attempt
Blob PathSharePoint path of the generated PDF (populated on Completed)
Created AtWhen the request was queued
Processed AtWhen processing completed or last failed

Status color coding:

StatusStyle
CompletedGreen (Favorable)
FailedRed (Unfavorable)
Dead (Max Retries Reached)Red (Unfavorable)
PendingStandard (Ambiguous)
ProcessingYellow (Attention)

Actions

ActionPurpose
Retry SelectedResets selected Failed or Dead rows back to Pending for reprocessing. Only enabled when selected rows are in a retryable state.
Delete SelectedRemoves selected rows. Use for cleaning up rows that are no longer needed.
Purge CompletedRemoves all Completed rows for the current template filter. Use for routine maintenance of the queue table.
Filter: PendingApplies a quick filter showing only rows awaiting processing
Filter: Failed / DeadShows rows that require attention
Clear FilterRemoves the quick filter

Dead Rows Require Manual Intervention

A row enters Dead (Max Retries Reached) status when all retry attempts have failed. Investigate the Error Message column to identify the root cause (connectivity, template misconfiguration, missing data) before using Retry Selected to requeue the row. Retrying without fixing the underlying issue will exhaust retries again.


Troubleshooting

SymptomLikely causeResolution
Queue rows stay Pending indefinitelyGen Processor Job Queue entry is not running or is on holdOpen Job Queue Entries, find codeunit 73524611, confirm status is Ready and not on hold
All rows show Failed immediatelyDispatcher or SharePoint connection errorRun System Status Check in System Settings; check Error Message on a failed row
Auto Generate on Post not queuing any rowsTemplate's Source Table ID does not match the posted document table, or no variant has Auto Generate on Post enabledVerify the template's Source Table ID; check that a variant is Enabled with Auto Generate on Post on
API returns 401 or 403OData authentication missing or caller lacks permissionsEnsure the API caller has the VPF-USER permission set; verify OAuth scope includes Dynamics 365 Business Central
API returns 400 with "Template Code not found"templateCode value in the request body does not match any enabled templateCheck REFRACT Templates - template must exist and have Enabled = true
Row completes but no PDF link on the documentRecord Link creation failedCheck the Blob Path field on the queue row - if populated, the PDF exists in SharePoint; the Record Link attachment may have been skipped due to a missing Record ID on the request
Dead rows accumulatingA systemic error is failing all requests within their fixed 3 attemptsInvestigate the root cause from Error Message, fix it, then use Retry Selected to requeue

On this page