REFRACT Platform for Dynamics 365 Sales

Developer Guide - REFRACT Platform for Dynamics 365 Sales

Technical architecture, detailed action flows, Dataverse table schemas, Custom API and plugin reference, batch dispatch modes, workflow enforcement, and deployment/ALM for the REFRACT Platform CRM integration.

Integration Architecture

The REFRACT Platform for Dynamics 365 Sales uses a native server-side processing layer within Dataverse. Each Command Bar action routes through a Custom Dataverse API to server-side logic that builds the document payload, uploads it to SharePoint, and calls the REFRACT Dispatcher.

LayerDynamics 365 Sales
UI triggerCommand Bar button
Template galleryJavaScript dialog
Server-side processingDataverse Plugin (runs within Dataverse sandbox)
Template and field mapping storageDataverse custom tables
SharePoint uploadMicrosoft Graph API
PDF generationREFRACT Dispatcher
Document link storageDataverse refract_documentlink table
Workflow enforcementDataverse pre-operation plugin
Credential storageDataverse Environment Variables

No Azure Function or Power Automate relay

There is no intermediate Azure Function or Power Automate flow in the processing path. The server-side plugin runs directly on the Dataverse platform. JavaScript handles UI only - it reads context, calls the Custom API, and displays results.


Dispatcher Contract

The Dispatcher is source-agnostic. It is a pure request-response service with four operations, and it only reads and writes SharePoint.

ActionEndpointMethodRequestResponse
Generate Document/renderPOST{ templatePath, payloadPath, options, templateSettings }PDF bytes + response headers X-Refract-BlobPath, X-Refract-CorrelationId, X-Refract-WorkflowId (this header set predates the REFRACT rebrand and is kept frozen by the dispatcher on purpose - do not expect X-REFRACTPDF-* names)
Open Document/docGET?blobPath=...Raw PDF bytes, Content-Type/Content-Disposition headers. No JSON body - the dispatcher used to return { url } but now streams bytes directly, since production storage is VNet-locked and a browser-facing SAS URL would be unreachable
Workflow Status/workflow/{id}/statusGET{id} in path{ state, title, currentStepIndex, totalSteps }
Workflow Sealed PDF/workflow/{id}/export?format=pdfGET{id} in path, ?format=pdfRaw PDF bytes, same streaming behavior as Open Document. No { downloadUrl } JSON

The Dispatcher never calls back to Dynamics 365. There is no push, no webhook, and no write to Dataverse. All state updates in CRM happen when a CRM user explicitly triggers an action - pull model throughout.

The plugin's app needs Render.Access, and Lifecycle.Reader for reads

The plugin calls the Dispatcher with an app token. The app it authenticates as, the Dynamics 365 Sales connector app named by refract_dispatcher_clientid, must hold the Render.Access application permission for every call, Generate Document included, and Lifecycle.Reader for Open Document (/doc) and Workflow Status (/workflow/{id}/status), both with admin consent. Without them the calls return 403 with code ROLE_MISSING naming the missing role. Workflow Sealed PDF also needs Lifecycle.Reader: an app is never a workflow participant, so the role is what lets it download the sealed PDF. See Integrations that use app tokens.


Action Summary

Each Command Bar button routes to a dedicated Custom Dataverse API. The table below describes what each action does and when to use it.

ActionWhat it doesWhen to use
Process DocumentReads the CRM record, uploads a data payload to SharePoint, calls the Dispatcher to render the PDF, and stores a document link on the recordFirst time generating a document for a record, or to regenerate
Open DocumentFetches the most recently generated PDF's bytes from the Dispatcher, base64-encodes them, and opens them directly in the browser via Xrm.Navigation.openFile (documents over 25 MB show an error directing the user to SharePoint instead)Viewing a document that has already been generated
Sync Workflow StatusQueries the Dispatcher for the current workflow state and displays it in a dialog; saves the outcome against the record on completion or rejectionChecking whether a workflow has been approved, rejected, or is still in progress
Get Signed DocumentSame byte-fetch-and-openFile mechanism as Open Document, against the sealed workflow export endpointDownloading the final signed document

Pull Model - No Background Sync

Workflow status is never updated automatically. It is only refreshed when a user clicks Sync Workflow Status. There is no Dispatcher callback or background sync into Dynamics 365.


Dataverse Custom Tables

The managed solution installs six custom tables. All table names are prefixed with refract_.

refract_template

Stores template definitions - one row per template.

ColumnTypeDescription
refract_templateidGUID (PK)Unique template identifier
refract_nameTextHuman-readable display name
refract_entitylogicalnameTextCRM entity, e.g. quote, opportunity
refract_templatevariantidTextTemplate ID in the REFRACT template library
refract_dispatchmodeChoiceStandard, PerRecord, SingleArray, GroupedArray - defined for parity with the Business Central schema, but the CRM plugin does not read this field; every Process Document call processes exactly one record regardless of its value
refract_workflowenabledBooleanActivate workflow routing for this template
refract_workflowenforcementmodeChoiceBlock, Validate, or None
refract_watermarkenabledBooleanPrint a watermark on generated PDFs
refract_watermarktextTextWatermark text, e.g. CONFIDENTIAL
refract_isactiveBooleanOnly active templates appear in the gallery

refract_templatefield

Stores field mappings for each template - one row per mapped field.

ColumnTypeDescription
refract_templatefieldidGUID (PK)Unique identifier
refract_nameTextDescriptive label for this mapping
refract_templateidLookup → refract_templateParent template
refract_crmfieldnameTextCRM field logical name, e.g. name, totalamount
refract_placeholderkeyTextKey in the REFRACT payload JSON, e.g. documentTitle
refract_sectiontypeChoice (OptionSet)Header (100000000), Line (100000001), or Footer (100000002)
refract_jsongroupTextOptional nesting key, e.g. billing, shipping
refract_sequencenumberIntegerField ordering within a section

refract_templateassignment

Controls which templates appear on which entities and contexts.

ColumnTypeDescription
refract_templateidLookup → refract_templateThe template to expose
refract_entitylogicalnameTextTarget entity
refract_contextChoiceform, view, or both

refract_templatesigner

Stores e-signature signer configuration per template. Fields reference CRM field logical names resolved at generation time - they are not hardcoded email addresses.

ColumnTypeDescription
refract_templateidLookup → refract_templateParent template
refract_emailfieldnameTextCRM field providing the signer's email, e.g. emailaddress1
refract_namefieldnameTextCRM field providing the signer's display name, e.g. fullname
refract_signerroleTexte.g. Signer, Witness, Approver
refract_sequencenumberIntegerSigning order for sequential mode

refract_templaterecipient

Stores delivery and workflow recipient configuration per template. Recipients can be either dynamic (resolved from the CRM record) or static (hardcoded email address).

ColumnTypeDescription
refract_templateidLookup → refract_templateParent template
refract_recipienttypeChoiceDelivery or Workflow
refract_emailfieldnameTextCRM field logical name to resolve the recipient email from the record (mutually exclusive with refract_staticemailaddress)
refract_staticemailaddressTextHardcoded recipient email - used when refract_emailfieldname is not set
refract_sequencenumberIntegerRecipient ordering

The runtime document link table. One row per generated document per record per link type.

ColumnTypeDescription
refract_documentlinkidGUID (PK)Unique document link
refract_entitynameTextSource CRM entity
refract_recordidTextSource CRM record GUID
refract_linktypeChoicestandard or workflow - see write rules below
refract_correlationidTextJob reference from Dispatcher
refract_blobpathTextSharePoint path to the generated PDF
refract_workflowidTextWorkflow instance ID (only for workflow linktype)
refract_workflowstateTextcomplete or rejected - written only on terminal states
refract_enforcementmodeChoiceBlock, Validate, or None (only for workflow linktype)
refract_generatedonDateTimeTimestamp of generation
refract_generatedbyuserTextCRM user who triggered the action

Write rules:

ActionWritten columns
Process DocumentblobPath, correlationId, linkType; also workflowId + enforcementMode when template is a workflow type
Sync Workflow StatusworkflowState - only when state = complete or rejected
Open DocumentRead only
Get Signed DocumentRead only
DispatcherNever writes to this table

Link type policy: A record can hold one standard link and one workflow link simultaneously. A new standard document never deletes a workflow link, and vice versa. The latest document of each type overwrites the previous.


Custom APIs

The solution includes eight Custom Dataverse APIs. The four command actions are invoked by Command Bar buttons. The four admin APIs are intended for one-time or periodic administrative use.

Command Actions

Custom APIDispatcher EndpointTriggered by
refract_ProcessDocumentPOST /render[Process Document] button
refract_OpenDocumentGET /doc[Open Document] button
refract_SyncWorkflowStatusGET /workflow/{id}/status[Sync Workflow Status] button
refract_GetSealedDocumentGET /workflow/{id}/export[Get Signed Document] button

Admin APIs

Custom APIPurpose
refract_BrowseSharePointFolderLists folders and .vpdf files in the templates SharePoint drive via Microsoft Graph. Used by the template browser dialog. It lists .vpdf files only; .rpkg support is planned.
refract_ImportTemplateDownloads the template file chosen in the browser (a .vpdf file) from SharePoint and creates or updates the matching refract_template record.
refract_InitializeDefaultsCreates default templates and field mappings for all eight supported entities. Run once after installation.
refract_ValidateSettingsChecks that the required environment variables (every variable except the two batch thresholds, refract_graph_templatessourcefolder, and refract_dispatcher_scope) are non-empty, and returns a missing-settings report. When the refract_dispatcher_* variables are all set, it also signs in with a fresh token (never a cached one), reads Render.Access and Lifecycle.Reader from the token's roles claim, and calls the Dispatcher's GET /lifecycle-access as the live check that the Dispatcher accepts the token. It fails if either role is missing, even though document generation may still work with Render.Access alone: Open Document and Sync Workflow Status need Lifecycle.Reader. It reports the scope it used and the token's aud, appid, and roles, never the token.

refract_InitializeDefaults parameters:

ParameterTypeDescription
overwriteExisting (input, optional)BooleanIf true, regenerates field mappings even if templates already exist
templatesCreated (output)IntegerNumber of new templates created
fieldMappingsCreated (output)IntegerNumber of new field mapping records created
assignmentsCreated (output)IntegerNumber of new template assignments created
summary (output)StringHuman-readable summary

refract_ValidateSettings outputs:

ParameterTypeDescription
isConfiguredBooleanTrue if all required variables are set
missingSettingsStringJSON array of missing variable names
configuredCountIntegerNumber of the required variables that have values
totalCountIntegerThe count of required variables checked, not every variable the solution defines
dispatcherCheckStatusStringskipped (dispatcher variables not all set), tokenAcquisitionFailed, dispatcherRejected (the Dispatcher did not accept the token or could not be reached), roleMissing (the token lacks Render.Access or Lifecycle.Reader), or success
dispatcherCheckDetailStringDetail message for the dispatcher sign-in check: the verdict, any missing role, the live check's answer, the token's roles, and its aud and appid
dispatcherScopeStringThe scope the plugin requests: refract_dispatcher_scope when set, else refract_dispatcher_clientid + /.default. Empty when neither is set.
summaryStringHuman-readable summary

Workflow Enforcement

A pre-operation plugin checks whether a pending or rejected workflow should block certain CRM operations. It reads the last-known workflow state cached on refract_documentlink.refract_workflowstate - it never calls the Dispatcher directly. If a user has not recently synced the workflow status, enforcement acts on the most recently saved state.

Enforcement modes (configured per template):

ModeBehaviour
BlockCRM operation is prevented with an error until the workflow reaches complete
ValidateAlso prevents the operation with an error. A Dataverse pre-operation plugin has no mechanism to show a dismissible warning and then allow the save to continue, so Validate currently behaves identically to Block
NoneNo enforcement - workflow state is tracked but does not affect CRM operations

If the workflow state is complete, the operation is always allowed regardless of mode. If the enforcer's own logic throws an unexpected error (not the deliberate enforcement error), that error is caught and logged, and the operation is allowed to proceed - an enforcer-internal failure fails open, not closed.

Registered Enforcement Steps

Unlike the four Command Bar actions, which are registered on all eight entities, the enforcement plugin itself is registered on exactly four steps, covering only Quote and Opportunity:

EntityMessageStage
QuoteUpdatePre-operation, synchronous
QuoteCloseQuotePre-operation, synchronous
OpportunityUpdatePre-operation, synchronous
OpportunityWinOpportunityPre-operation, synchronous

There is no registered enforcement step for Account, Contact, Order, Invoice, Case, or Lead. A workflow-enabled template on one of those entities still tracks workflow state on refract_documentlink, but no CRM operation on those entities is blocked by it.

Enforcement mode (Block / Validate / None) is configured per template and stored in refract_documentlink.refract_enforcementmode at the time of document generation.


No Batch or List-View Processing

refract_ProcessDocument takes exactly one entityName + recordId + templateCode per call, and the Command Bar buttons are registered form-only (single-record context) on all eight entities - there is no list-view multi-select action and no batch payload shape. The refract_template.refract_dispatchmode field defines Standard/Per Record/Single Array/Grouped Array options mirroring the Business Central template schema, but nothing in the CRM plugin code branches on this field or fans a single Process Document call out to multiple records - leave it at Standard.

The two refract_batchwarningthreshold/refract_batchmaxthreshold environment variables are likewise defined but not read by any plugin code.


Managed Solution Contents

RefractPlatformD365Sales (Managed Solution)
├── Dataverse Tables (6)
│   ├── refract_template
│   ├── refract_templatefield
│   ├── refract_templateassignment
│   ├── refract_templatesigner
│   ├── refract_templaterecipient
│   └── refract_documentlink
├── Custom Dataverse APIs (8)
│   ├── refract_ProcessDocument         - Command Bar action
│   ├── refract_OpenDocument            - Command Bar action
│   ├── refract_SyncWorkflowStatus      - Command Bar action
│   ├── refract_GetSealedDocument       - Command Bar action
│   ├── refract_BrowseSharePointFolder  - Admin: browse template library
│   ├── refract_ImportTemplate          - Admin: import .vpdf from SharePoint
│   ├── refract_InitializeDefaults      - Admin: seed default templates (8 entities)
│   └── refract_ValidateSettings        - Admin: check environment variable config
├── Web Resources
│   ├── refract_/scripts/refract_actions.js    - Command Bar button handlers
│   ├── refract_/scripts/refract_gallery.js    - Template gallery + progress dialog
│   ├── refract_/scripts/refract_browser.js    - Template browser + import dialog
│   ├── refract_/stylesheets/refract_gallery.css
│   ├── refract_/stylesheets/refract_browser.css
│   ├── refract_/pages/refract_gallery.html
│   └── refract_/pages/refract_browser.html
├── Command Bar Customisations (8 entities)
│   ├── quote, opportunity, account, contact
│   ├── salesorder, invoice, incident, lead
│   └── Buttons: [Process Document] [Open Document] [Sync Workflow Status] [Get Signed Document]
├── Security Roles
│   └── REFRACT User
└── Environment Variables (13)
    ├── refract_graph_tenantid
    ├── refract_graph_clientid
    ├── refract_graph_clientsecret           (secret)
    ├── refract_graph_driveid                (output document library)
    ├── refract_graph_templatesdriveid       (templates library)
    ├── refract_graph_templatessourcefolder  (folder path within templates library)
    ├── refract_dispatcher_baseurl
    ├── refract_dispatcher_tenantid
    ├── refract_dispatcher_clientid
    ├── refract_dispatcher_clientsecret      (secret)
    ├── refract_dispatcher_scope             (api://<dispatcher-app-client-id>/.default)
    ├── refract_batchwarningthreshold        (default: 25)
    └── refract_batchmaxthreshold            (default: 100)

Deployment and ALM

StageMethod
DevelopmentUnmanaged solution; plugins registered via Plugin Registration Tool
QA / UATExport managed solution → import to target environment; set environment variables per environment
ProductionImport managed solution
UpgradeImport new managed solution version - tables and data are preserved
UninstallDelete managed solution - full clean removal, no residual customisations left in the environment
Credential rotationUpdate the relevant Environment Variable secret values only - no code change or solution reimport required

Entity Extension Pattern

Command Bar customisations are the only entity-specific artefacts in the solution. Adding support for a new CRM entity requires only a new Command Bar entry in a solution update - no plugin code changes. The plugin reads the entity name from the Custom API request parameters, so it works generically for any entity with an assigned template.

On this page