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.
| Layer | Dynamics 365 Sales |
|---|---|
| UI trigger | Command Bar button |
| Template gallery | JavaScript dialog |
| Server-side processing | Dataverse Plugin (runs within Dataverse sandbox) |
| Template and field mapping storage | Dataverse custom tables |
| SharePoint upload | Microsoft Graph API |
| PDF generation | REFRACT Dispatcher |
| Document link storage | Dataverse refract_documentlink table |
| Workflow enforcement | Dataverse pre-operation plugin |
| Credential storage | Dataverse 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.
| Action | Endpoint | Method | Request | Response |
|---|---|---|---|---|
| Generate Document | /render | POST | { 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 | /doc | GET | ?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}/status | GET | {id} in path | { state, title, currentStepIndex, totalSteps } |
| Workflow Sealed PDF | /workflow/{id}/export?format=pdf | GET | {id} in path, ?format=pdf | Raw 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.
| Action | What it does | When to use |
|---|---|---|
| Process Document | Reads the CRM record, uploads a data payload to SharePoint, calls the Dispatcher to render the PDF, and stores a document link on the record | First time generating a document for a record, or to regenerate |
| Open Document | Fetches 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 Status | Queries the Dispatcher for the current workflow state and displays it in a dialog; saves the outcome against the record on completion or rejection | Checking whether a workflow has been approved, rejected, or is still in progress |
| Get Signed Document | Same byte-fetch-and-openFile mechanism as Open Document, against the sealed workflow export endpoint | Downloading 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.
| Column | Type | Description |
|---|---|---|
refract_templateid | GUID (PK) | Unique template identifier |
refract_name | Text | Human-readable display name |
refract_entitylogicalname | Text | CRM entity, e.g. quote, opportunity |
refract_templatevariantid | Text | Template ID in the REFRACT template library |
refract_dispatchmode | Choice | Standard, 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_workflowenabled | Boolean | Activate workflow routing for this template |
refract_workflowenforcementmode | Choice | Block, Validate, or None |
refract_watermarkenabled | Boolean | Print a watermark on generated PDFs |
refract_watermarktext | Text | Watermark text, e.g. CONFIDENTIAL |
refract_isactive | Boolean | Only active templates appear in the gallery |
refract_templatefield
Stores field mappings for each template - one row per mapped field.
| Column | Type | Description |
|---|---|---|
refract_templatefieldid | GUID (PK) | Unique identifier |
refract_name | Text | Descriptive label for this mapping |
refract_templateid | Lookup → refract_template | Parent template |
refract_crmfieldname | Text | CRM field logical name, e.g. name, totalamount |
refract_placeholderkey | Text | Key in the REFRACT payload JSON, e.g. documentTitle |
refract_sectiontype | Choice (OptionSet) | Header (100000000), Line (100000001), or Footer (100000002) |
refract_jsongroup | Text | Optional nesting key, e.g. billing, shipping |
refract_sequencenumber | Integer | Field ordering within a section |
refract_templateassignment
Controls which templates appear on which entities and contexts.
| Column | Type | Description |
|---|---|---|
refract_templateid | Lookup → refract_template | The template to expose |
refract_entitylogicalname | Text | Target entity |
refract_context | Choice | form, 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.
| Column | Type | Description |
|---|---|---|
refract_templateid | Lookup → refract_template | Parent template |
refract_emailfieldname | Text | CRM field providing the signer's email, e.g. emailaddress1 |
refract_namefieldname | Text | CRM field providing the signer's display name, e.g. fullname |
refract_signerrole | Text | e.g. Signer, Witness, Approver |
refract_sequencenumber | Integer | Signing 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).
| Column | Type | Description |
|---|---|---|
refract_templateid | Lookup → refract_template | Parent template |
refract_recipienttype | Choice | Delivery or Workflow |
refract_emailfieldname | Text | CRM field logical name to resolve the recipient email from the record (mutually exclusive with refract_staticemailaddress) |
refract_staticemailaddress | Text | Hardcoded recipient email - used when refract_emailfieldname is not set |
refract_sequencenumber | Integer | Recipient ordering |
refract_documentlink
The runtime document link table. One row per generated document per record per link type.
| Column | Type | Description |
|---|---|---|
refract_documentlinkid | GUID (PK) | Unique document link |
refract_entityname | Text | Source CRM entity |
refract_recordid | Text | Source CRM record GUID |
refract_linktype | Choice | standard or workflow - see write rules below |
refract_correlationid | Text | Job reference from Dispatcher |
refract_blobpath | Text | SharePoint path to the generated PDF |
refract_workflowid | Text | Workflow instance ID (only for workflow linktype) |
refract_workflowstate | Text | complete or rejected - written only on terminal states |
refract_enforcementmode | Choice | Block, Validate, or None (only for workflow linktype) |
refract_generatedon | DateTime | Timestamp of generation |
refract_generatedbyuser | Text | CRM user who triggered the action |
Write rules:
| Action | Written columns |
|---|---|
| Process Document | blobPath, correlationId, linkType; also workflowId + enforcementMode when template is a workflow type |
| Sync Workflow Status | workflowState - only when state = complete or rejected |
| Open Document | Read only |
| Get Signed Document | Read only |
| Dispatcher | Never 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 API | Dispatcher Endpoint | Triggered by |
|---|---|---|
refract_ProcessDocument | POST /render | [Process Document] button |
refract_OpenDocument | GET /doc | [Open Document] button |
refract_SyncWorkflowStatus | GET /workflow/{id}/status | [Sync Workflow Status] button |
refract_GetSealedDocument | GET /workflow/{id}/export | [Get Signed Document] button |
Admin APIs
| Custom API | Purpose |
|---|---|
refract_BrowseSharePointFolder | Lists 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_ImportTemplate | Downloads the template file chosen in the browser (a .vpdf file) from SharePoint and creates or updates the matching refract_template record. |
refract_InitializeDefaults | Creates default templates and field mappings for all eight supported entities. Run once after installation. |
refract_ValidateSettings | Checks 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:
| Parameter | Type | Description |
|---|---|---|
overwriteExisting (input, optional) | Boolean | If true, regenerates field mappings even if templates already exist |
templatesCreated (output) | Integer | Number of new templates created |
fieldMappingsCreated (output) | Integer | Number of new field mapping records created |
assignmentsCreated (output) | Integer | Number of new template assignments created |
summary (output) | String | Human-readable summary |
refract_ValidateSettings outputs:
| Parameter | Type | Description |
|---|---|---|
isConfigured | Boolean | True if all required variables are set |
missingSettings | String | JSON array of missing variable names |
configuredCount | Integer | Number of the required variables that have values |
totalCount | Integer | The count of required variables checked, not every variable the solution defines |
dispatcherCheckStatus | String | skipped (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 |
dispatcherCheckDetail | String | Detail 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 |
dispatcherScope | String | The scope the plugin requests: refract_dispatcher_scope when set, else refract_dispatcher_clientid + /.default. Empty when neither is set. |
summary | String | Human-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):
| Mode | Behaviour |
|---|---|
Block | CRM operation is prevented with an error until the workflow reaches complete |
Validate | Also 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 |
None | No 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:
| Entity | Message | Stage |
|---|---|---|
| Quote | Update | Pre-operation, synchronous |
| Quote | CloseQuote | Pre-operation, synchronous |
| Opportunity | Update | Pre-operation, synchronous |
| Opportunity | WinOpportunity | Pre-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
| Stage | Method |
|---|---|
| Development | Unmanaged solution; plugins registered via Plugin Registration Tool |
| QA / UAT | Export managed solution → import to target environment; set environment variables per environment |
| Production | Import managed solution |
| Upgrade | Import new managed solution version - tables and data are preserved |
| Uninstall | Delete managed solution - full clean removal, no residual customisations left in the environment |
| Credential rotation | Update 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.
REFRACT Platform for Dynamics 365 Sales
Complete integration guide for Dynamics 365 Sales consultants and developers. Install, configure, and use REFRACT PDF with on-demand PDF generation, SharePoint storage, e-signature workflows, and audit trails - directly from CRM records.
Troubleshooting - REFRACT Platform for Dynamics 365 Sales
Diagnose and resolve common REFRACT Platform CRM issues. Security role errors, environment variable misconfigurations, plugin failures, SharePoint connectivity, workflow enforcement, and batch operation problems.