File uploads in Dynamics 365 real-time forms

A creative solution for Dynamics 365 Customer Insights.

Anyone who works with Dynamics 365 Customer Insights knows that sending files via real-time forms is a frequent requirement – and yet this function is missing from the standard system. In this blog article, I’ll show you how you can close this gap with creativity and the right tools. Using a combination of SharePoint, Power Automate and some JavaScript, I develop an easily customizable solution that integrates seamlessly with Dynamics 365.

The article is based on a more complex customer solution, but has been simplified for this blog to present the core functionality in an understandable way without unnecessary complexity. You’ll get step-by-step instructions and practical tips on how to add a powerful feature to Dynamics 365. And if you have alternative ways or feedback, I’d love to hear about it!

Are you ready to add an exciting feature to Dynamics 365? Then let’s get started!

Table of Contents

SharePoint Integration

Typically, documents/files are mapped in CRM using SharePoint integration, not least to take advantage of the cost-efficient SharePoint storage. Files associated with a record in this way can be displayed and used in CRM through the “Documents” tab within the record. In this approach, I use SharePoint integration to store the file in a location associated with the lead.

To make this possible, SharePoint integration must be initialized once for the corresponding Dataverse environment if it is not already active. This can be done by creating a new SharePoint site and connecting it to the environment via the wizard in the document settings of the advanced settings in Dynamics 365. Detailed instructions can be found in the Microsoft documentation.

In our example, we use a lead form. The lead table is enabled for SharePoint integration by default. Here, I use the default SharePoint integration, meaning there are no complex dependencies on document locations or similar structures. Each lead gets its document location, whose parent document location is the general folder for leads. This parent document location can be created manually during the initial setup if it does not already exist. It is important that the “Parent Site” is set to the standard website and the relative URL is defined with the value “lead.”

Unmapped File Input Field

In the future, it will be easier to add so-called “unmapped fields” to real-time forms. Until then, this can already be achieved today by manipulating the HTML code. As long as these fields are standard text input fields, the results are even transmitted to Dynamics 365. This fantastic article by Peter Krause shows how this works: Enhanced data collection and journey personalization with unmapped form fields. Similarly, we integrate a custom field of the type="file". Since this field type is ignored by the real-time form, we need to process its content ourselves using a script.

To get a properly styled field, we can add a random text field to the form in the editor and then modify the HTML code in the marked places to turn it into a file input field and remove the original mapping.

In this example, I use the “Address 2 Line 3” field and modify it according to the requirements.

// Before
<div
  class="textFormFieldBlock"
  data-editorblocktype="TextFormField"
  data-targetaudience="lead"
  data-targetproperty="address2_line3"
  data-prefill="false">
  <label title="Adresse 2: Straße 3" for="address2_line3-1735898935877">
    <div
      id="editorIdPrefixc402ba0e025bf1735899029321"
      aria-readonly="false"
      tabindex="0"
      spellcheck="true"
      aria-multiline="true"
      aria-describedby="cke_217"
      aria-controls="cke_218"
      aria-activedescendant=""
      aria-autocomplete="list"
      aria-expanded="false">
      File upload field
    </div>
  </label>
  <input
    id="address2_line3-1735898935877"
    type="text"
    name="address2_line3"
    placeholder=""
    title="Adresse 2: Straße 3"
    maxlength="250" />
</div>

// After
<div
  class="textFormFieldBlock"
  data-editorblocktype="TextFormField"
  data-prefill="false">
  <label title="File Upload" for="fileupload">
    <div
      id="editorIdPrefixc402ba0e025bf1735899029321"
      aria-readonly="false"
      tabindex="0"
      spellcheck="true"
      aria-multiline="true"
      aria-describedby="cke_217"
      aria-controls="cke_218"
      aria-activedescendant=""
      aria-autocomplete="list"
      aria-expanded="false">
      File upload field
    </div>
  </label>
  <input
    id="fileupload"
    type="file"
    accept=".pdf"
    name="fileupload_field"
    title="File Upload" />
</div>

After the field has been customised in this form, it looks like this in the form editor:

Script for File Submission

When the file is processed during the workflow and sent to the Power Automate flow, the browser window must not be closed. To alert the user, we add a modal that will be displayed via the script while the process is running.

Adjustments to the HTML of the form

First, we insert the following <div>-element before the closing </body>-tag.

<div id="uploadOverlay"></div>
<div id="uploadModal">
  <p>Please wait until file is uploaded.</p>
  <p id="uploadResponseText"></p>
</div>

Adjustments to the CSS of the form

We then add the following classes to the styles of the form. This results in a white background and a grey modal with our hint text. At this time, the modal is invisible and will be made visible by the script.

/* Styles for the overlay */
#uploadOverlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: white;
  z-index: 1000;
}

/* Styles for the Modal */
#uploadModal {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #ededed;
  padding: 60px;
  border-radius: 10px;
  z-index: 1001;
  text-align: center;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  max-width: 400px;
  width: 90%;
}

/* Message text in modal */
#uploadModal p {
  margin: 0px;
  padding: 0px;
  line-height: 125%;
  line-height: 1.25;
  font-family: 'Segoe UI', Arial, sans-serif;
  font-size: 18px;
}

Script for File Uploads in Dynamics 365 Forms

The following script is as simple an approach as possible, which nevertheless takes into account the essential aspects of file processing. Below the script, I briefly explain the individual paragraphs. This script must be placed directly in the <body>-tag in the first position (more information).

// Author: Johannes Fleischhut
// Date: January 2025
// Description: Simplified class to handle file uploads

class FileUploadHandler {
  constructor() {
    this.fileInput = document.querySelector('#fileupload');
    this.uploadUrl =
      '{YOUR FLOW REQUEST URL}'; // File upload endpoint from Power Automate Flow

    this.events();
  }

  events() {
    if (this.fileInput) {
      this.fileInput.addEventListener('change', () =>
        this._validateFile(this.fileInput)
      ); // Validate file on every change of the field

      document.addEventListener(
        'd365mkt-formsubmit',
        this._handleFormSubmit.bind(this)
      ); // Initiate event listener on form submission to start upload logic
    }
  }

  _validateFile(fileInput) {
    const file = fileInput.files[0];
    if (!file) return;

    const allowedExtension = 'pdf';
    const maxSize = 5 * 1024 * 1024; // Allowed file type and size (5MB)
    const fileExtension = fileName.split('.').pop(); // Extract the file extension from the file name
    const fileSize = file.size; // Get file details
    if (fileExtension !== allowedExtension || fileSize > maxSize)
      fileInput.value = ''; // Clear input if validation fails
  }

  async _handleFormSubmit(event) {
    const file = this.fileInput?.files[0]; // Get selected file

    const emailField = document.querySelector('input[name="emailaddress1"]');
    const lastNameField = document.querySelector('input[name="lastname"]');

    if (!file || !lastNameField || !emailField) return; // Exit if no file or no sufficient lead information

    this._showUploadModal(); // Display upload modal

    const createFilePayload = async () => {
      // Generate payload with file details
      const base64File = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result.split(',')[1]); // Extract base64 content
        reader.onerror = reject;
        reader.readAsDataURL(file);
      });
      return {
        leadLastName: lastNameField?.value,
        leadEmail: emailField?.value,
        fileName: file.name,
        fileType: file.type,
        fileSize: file.size,
        fileContent: base64File
      }; // Return formatted payload
    };

    try {
      const jsonPayload = await createFilePayload(); // Prepare JSON payload

      const response = await fetch(this.uploadUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(jsonPayload)
      }); // Send file to server

      const responseTextElement = document.getElementById('uploadResponseText'); // Get response text element

      if (response.ok) {
        const responseData = await response.json();
        responseTextElement.textContent = responseData.message;
      } // Show success message
      else
        responseTextElement.textContent = `Error during upload: ${response.statusText}`; // Show error message
      responseTextElement.style.display = 'block';
    } catch (error) {
      const responseTextElement = document.getElementById('uploadResponseText'); // Handle upload error
      responseTextElement.textContent = `Error during upload: ${error.message}`;
      responseTextElement.style.display = 'block';
    } finally {
      setTimeout(() => {
        this._hideUploadModal();
      }, 3000); // Hide modal after delay
    }
  }

  _showUploadModal() {
    document.getElementById('uploadOverlay').style.display = 'block';
    document.getElementById('uploadModal').style.display = 'block'; // Show modal
  }

  _hideUploadModal() {
    document.getElementById('uploadOverlay').style.display = 'none';
    document.getElementById('uploadModal').style.display = 'none'; // Hide modal
  }
}

// Initialize the class after the form is loaded
document.addEventListener('d365mkt-afterformload', function () {
  new FileUploadHandler();
}); // Create instance of FileUploadHandler

Explanation of the code

Lines 6-11Assignment of the file input field and the individual Flow Request URL, which is generated during the creation of the flow (more on this later). Additionally, the events() method is called as the starting point for the instance of the class.
Lines 14-25Check if the input field exists, and if so, add event listeners. The first event listener ensures that the file in the input field is validated every time the field is changed. The second event listener ensures that the upload logic is initiated when the form is submitted.
Lines 27-37Validation of the uploaded file. If the size exceeds the limit or the allowed file extension is not recognized, the file is removed from the input field. I have tried to upload files up to 10MB without any problems.
Lines 39-94First, the code checks whether the file and necessary information about the lead (for later identification in the flow) exist. If not, the code exits (line 45). Then, the modal is made visible (line 47). Lines 49-65 define a function that gathers all the necessary information for transmission into a JSON payload and converts the attached PDF file into a base64 string. In the following try block, the previously defined function is called, and as a result, the necessary payload for the upcoming POST request to the Power Automate flow is obtained. If the flow responds with “OK,” the response text is added to our modal (lines 78-81). If the flow returns an error, this text is displayed accordingly (lines 82-84). If something already goes wrong on the frontend side, the catch() block is executed (lines 85-88), and a corresponding error message is shown. Finally, a 3-second counter runs to close the modal afterward (lines 89-93).
Lines 96-104Here, the methods for showing and hiding the modal are defined.
Lines 107-110Here, after the real-time form has successfully loaded, a new instance of the previously defined class is created, initializing the code.

Flow for processing the file

Triggering the Flow

The second part of processing the file takes place in a Power Automate flow. We use the trigger “When an HTTP request is received” as the starting point. In my case, I was not able to find the trigger when creating a new flow (for some reason). If this phenomenon occurs, use any other trigger to access the flow editor, delete the trigger, and create a new one. I have always found the required trigger this way.

To allow the trigger to be called from anywhere, “Who can trigger the flow” must be set to “Anyone.”

To create the ‘Request Body JSON Schema’, I use a simple trick: In the first step, I only create a ‘Compose’ action in the flow and define the value with the ‘body’ of the trigger. Now I test the flow for the first time by submitting the form. If everything runs correctly, I receive the JSON payload created by my script in my compose action. I can now use this to automatically generate the JSON schema.

The following JSON schema applies to the script created above:

{
    "type": "object",
    "properties": {
        "leadLastName": {
            "type": "string"
        },
        "leadEmail": {
            "type": "string"
        },
        "fileName": {
            "type": "string"
        },
        "fileType": {
            "type": "string"
        },
        "fileSize": {
            "type": "integer"
        },
        "fileContent": {
            "type": "string"
        }
    }
}

Validating the Files

The transmitted file must now be checked. This is especially necessary because the logic for sending the file occurs in the browser, which theoretically makes it visible and manipulable for anyone. We must also ensure on the server side that the file complies with our guidelines. To keep this as simple as possible, the following condition serves to validate the file and send a response back to the script. If the file validation fails, the flow is terminated.

The condition checks the file size, which is transmitted in bytes. 5MB corresponds to 5 * 1024 * 1024 = 5242880 bytes. The accepted file type is “application/pdf.”

Response 200 OK

Status Code200
HeadersContent-Type: application/json
Body{
“message”: [WHATEVER MESSAGE YOU WANT TO SEND]
}

Response 403 Forbidden

Status Code403
HeadersContent-Type: application/json
Body{
“message”: [WHATEVER MESSAGE YOU WANT TO SEND]
}

Identifying the Lead

At this point, it is important to understand that submitting the real-time form and the resulting lead creation is an asynchronous process that takes some time. We have no way of determining the exact moment the lead was created in the flow. Therefore, I like to use a “Do until” loop here, which repeats a series of actions until the desired result is achieved.

We need a new boolean variable called “Lead exists,” initially set to “false.” The “Do until” loop will continue running until this variable is set to “true.”

Within the loop, I search for a lead that matches the values submitted. In this simple example, this is the combination of last name and email address. In a more advanced scenario, a dedicated submission ID field could be used, which is generated by the script in the form and then stored in the lead record.

Find lead

Table nameLeads
Selected columnsleadid,lastname,emailaddress1
Filter rowsemailaddress1 eq ‘@{triggerBody()?[‘leadEmail’]}’ and lastname eq ‘@{triggerBody()?[‘leadLastName’]}’
Row count1

If the lead is found, the variable ‘Lead exists’, which was previously initialised as ‘false’, is set to ‘true’ and I also save the lead ID and the lead’s surname for later use. If the lead is not found, I set a delay of 30 seconds and then repeat the process.

If the lead is not found after one hour or 60 repetitions, the flow is terminated. To do this, I create a Terminate action after the ‘Do until’ loop and configure ‘Run after’ so that it is only executed in the event of an error or a timeout.

// Condition
length(outputs('Find_lead')?['body/value']) eq 1

// Compose lead id
@{first(outputs('Find_lead')?['body/value'])?['leadid']}

// Compose lead lastname
@{first(outputs('Find_lead')?['body/value'])?['lastname']}

// Terminate missing lead
Status: Failes
Code: 404
Message: Lead could not be found.

Creating the Document Location

First, we need to check whether a document location already exists to avoid creating a duplicate, which could lead to inconsistencies. Normally, the document location will not yet exist since the lead has just been created. However, it is theoretically possible that due to user behavior or other automations, a document location was automatically created within the short time frame after the lead creation. In such a case, we want to use the existing document location instead of creating a new one.

We make the query with the ‘List rows’ action and can use the following fetch XML query. We defined the dynamic value for the lead ID in the previous scope, in my case in the ‘Compose lead id’ action.

// Fetch XML
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="SharePointdocumentlocation">
    <order attribute="name" descending="false" />
    <filter type="and">
      <condition attribute="regardingobjectid" operator="eq" uitype="lead" value="@{outputs('Compose_lead_id')}" />
    </filter>
  </entity>
</fetch>

In the next step, we check whether a result has been found. If no result was found, we must create a new document location. If a value was found, we set the relative URL that we need in the next step to create the file to the first value found.

The relative URL for the new document location can be freely generated according to your own requirements. In my case, I use the form [Surname]_[Lead ID]. This relative URL can later be found as a path in the URL to the SharePoint folder. As we still need the relative URL in various places, we bind the value in the variable ‘Relative URL Document Location’.

// Condition
length(outputs('Find_lead')?['body/value']) eq 0

// Set relative URL to default value (links)
@{outputs('Compose_lead_lastname')}_@{outputs('Compose_lead_id')}

// Set relative URL to first found value (rechts)
@{first(outputs('Find_document_location_of_lead')?['body/value'])?['relativeurl']}

Now we can create the document location. The following parameters are required for this:

Create document location

Table nameDocument Locations
Name@{variables(‘Relative URL Document Location’)}
ServicetypSharePoint
Regarding (Lead)leads(@{outputs(‘Compose_lead_id’)})
Relative URL@{variables(‘Relative URL Document Location’)}
Parent Website or Document LocationSharePointdocumentlocations({GUID des übergeordneten Lead Dokumentorts})*

Create new folder

The corresponding SharePoint folder must then be created accordingly. We use the following parameters for this:

Site AddressSelect the SharePoint page that either already existed or was created initially.
List or LibraryLead
Folder Path@{variables(‘Relative URL Document Location’)}

*The GUID of the parent lead document location can most easily be taken from the browser URL.

Saving the File in SharePoint

Now that we have identified the document location in the CRM and the corresponding SharePoint folder, the final step is to create the file in this folder using the base64 string transmitted to the flow. Using a “Compose” action and the built-in base64ToBinary() function, we restore the original binary code of the file.

We then use the SharePoint action “Create file” to create the file.

Compose binary

Inputs@{base64ToBinary(triggerBody()?[‘fileContent’])}

Create file in SharePoint

Site AddressSelect the SharePoint page that either already existed or was created initially.
Folder Path/lead/@{variables(‘Relative URL Document Location’)}
File Name@{triggerBody()?[‘fileName’]}
File Content@{outputs(‘Compose_binary’)}

Done! As soon as the lead is opened in Dynamics 365, the submitted file can be viewed in the ‘Related’ -> ‘Documents’ area.

Alternatively, the file can also be found directly in the SharePoint folder. To do this, go to ‘Site Contents’ – ‘Lead’ – ‘[Surname]_[Lead ID]’.

Extensions

What an extensive article…the process presented here works and offers a basic possibility to save files via real-time forms in the SharePoint folder of the created lead. However, while writing this article, I have removed various features from the reference project so as not to go completely beyond the scope and have focussed on the basics. The removed points include above all the ALM suitability of the flow, but also some functions in the frontend, above all visualised user feedback during the file check. Below I list a few points that can refine the process presented above and should serve as inspiration.

  • Add visual feedback for frontend validation, such as displaying red text if the file does not meet the requirements.
  • Extend the process to handle multiple files simultaneously.
  • Make the flow ALM-compatible so that it can be easily imported from the development environment into the production environment.
    • Use environment variables for SharePoint sites and SharePoint libraries.
    • Generate required GUIDs within the flow itself (e.g., searching for the parent document location for leads based on the relative URL) or use environment variables.
  • Use XMLHttpRequest instead of Fetch API for improved user feedback.
    • In this script, I use the somewhat simpler Fetch API method to send the POST request. Alternatively, XMLHttpRequest can be used, which has a significant advantage: it can provide progress feedback during the process, allowing for the inclusion of features such as a progress bar or percentage display.
  • Create the modal, including its styling, entirely through the script to make the code easier to transport and avoid manual adjustments in multiple places in the form.
  • Improve lead identification with a dedicated form submission ID field, which is generated live (e.g., using the email address and the current timestamp) and stored both in the lead record and passed to the flow for unique identification.
  • Add additional security features, such as an external virus scanner (although SharePoint already has one integrated) or move the code to the server-side of the website to avoid exposing the flow URL.

Image by pikisuperstar on Freepik


Stay informed

Never miss a new post and simply subscribe to the newsletter.

Leave a Comment

Your email address will not be published. Required fields are marked *