Author: David Hölkeskamp

  • Auto-Complete / Escalate approvals in Power Automate

    Approvals are a core component in many Power Automate flows – I’ve personally created numerous approval workflows over time.

    While using Nintex Workflows for Office 365, you could easily configure fallback actions when approvals are missed, such as when the responsible person is on vacation or simply overlooks the notification.

    However, handling these scenarios in Power Automate requires a different approach.

    The screenshot provided shows how you would configure an approval action in Nintex Workflow for Office 365 that would automatically resolve to Agree (approval) after three days without the input of the responsible user.

    Power Automate is capable of providing the same functionality but not as easy as Nintex. In this post I am going to show you how you can configure the same auto-completing behaviour in Power Automate.

    Hint: This is not only useful for letting your flow work on but also in regards to the 30 days limit that would end the flow no matter what.

    Setting up an approval

    First, we’ll need to add a Start and wait for an approval action. Complete all the necessary fields in the Parameters tab to configure it according to your specific requirements.

    The next step is defining the timeout-duration – meaning the time the approval should wait until it continues the flow execution.


    To do so open the Settings-tab in the action editor.
    The neccessary configuration is Action timeout (as you might have guessed by the name).

    The value of Action timoeut is written in a ISO8601-Duration-Notation notation.


    To let the action wait for three days before it gets automatically concluded, we are setting the value to P3D meaning, wait a Period of three days.

    Next, define the timeout duration – the time the approval should wait before continuing the flow execution.

    Open the Settings tab in the action editor.

    Look for the Action timeout configuration (which is aptly named). This value uses ISO 8601 duration notation.

    To set the action to wait three days before automatically concluding, enter P3D, which means ‘Period of three days’

    Handling the auto-completion

    Now that the approval action is set up, it’s time to handle its outcomes.

    First, add a condition to respond to the user-triggered completion of the approval action.

    Now that we have the expected execution covered, its time to handle the unexpected (the auto-completion).
    Add a scope and call it something like Handle auto-completion.
    Open the scope editor, navigate to Settings -> Run after.

    By default the previous action will be shown with a selected state of Is successful.
    Now add the Start and wati for an approval action and set the selected state to Has timed out-

    Every time the approval action times out, the scope will be triggered.

    Now that we have the expected execution covered, it’s time to handle the unexpected scenario – auto-completion.

    Add a Scope and name it something like ‘Handle auto-completion’.

    Open the scope editor and navigate to Settings → Run after. By default, the previous action will be shown with a selected state of ‘Is successful’.

    Add the Start and wait for an approval action and change its selected state to Has timed out. This ensures the scope will be triggered whenever the approval action times out.

    Wrap up

    That’s it! You can now handle both the regular completion of approval tasks and the exception where an approval is neither granted nor rejected.

    Typically, this exception would trigger some form of escalation – perhaps renotifying the responsible user about the pending approval, or rolling back changes made by the flow prior to the timeout.

  • Exploring AI in Power Automate – Auto-Translation

    SharePoint Online enables multilanguage support for pages within a site. When activated, you can create copies of any page in all configured languages for your site. These copies initially mirror your original page’s content in the root language. By combining Power Automate with AI capabilities, we can create an automated workflow that translates the content in these language copies.

    Setup

    First of all we are setting up Multilanguage Support

    1. Navigate to site settings -> Language settings
    2. Enable “Enable translation into multiple languages”
    3. Select your required languages and assign a translator for each language. These translators should review and refine the automatic translations.

    When you create a new page, you can generate translations using the translation button in the pages menu.
    Clicking this button will create a copy of your page in the language-appropriate folder (e.g. SitePages/de for german translations)

    We are going to hook into that process and as soon as a new translation gets created, trigger a Power Automate Flow, that automatically translates the pages content.

    Since auto-translation is not 100% accurate, the responsible person (which still gets the mail), should review the translation

    Creating the flow

    The trigger

    Create a new flow, name it Autotranslate Pages and select When a file is created (properties only) as trigger.
    Select your website and set the library as Site Pages manually as it is not selectable.

    Furthermore we are adding a trigger condition, so the flow only gets triggered, when a new translation page has been created.

    "Action":
    When a file is created (properties only)
    
    "Site Address":
    Use the site where multilanguage support has been enabled
    
    "Library Name":
    Site Pages
    
    "Trigger conditions":
    - @equals(triggerBody()?['OData__SPIsTranslation'], true)
    

    Getting the page content

    SharePoint stores page content in a property called CanvasContent1. The SharePoint Online Page Editor structures this content using multiple controls (Canvas Controls). Each control functions like a div element, representing a block that contains webpart configuration – for example, text size settings for Text-Webparts or URLs for image webparts.

    To process these controls effectively, we’ll store them in an array. This array will hold all controls and allow us to transform them in later steps of our automation.

    ```
    "Action":
    Initialize variable
    
    "Name":
    Page Controls
    
    "Type":
    Array
    ```

    To properly translate a page we have to extract all the controls, and to extract the controls, we have to load them first.

    Add a Send an HTTP request to SharePoint and configure it as follows

    "Action":
    Send an HTTP request to SharePoint
    
    "Site Address":
    Same as in the trigger
    
    "Method":
    GET
    
    "Uri (use insert expression)":
    _api/web/lists/getbytitle('Site Pages')/items(@{triggerBody()?['ID']})?$select=CanvasContent1

    After retrieving the page content, we have to decode it. Add the following Compose action

    "Action":
    Compose
    
    "Name":
    Parse Page Body
    
    "Inputs":
    decodeUriComponent(body('Send_an_HTTP_request_to_SharePoint')['d/CanvasContent1'])

    After decoding the content, we are going to strip the surrounding div, as this holds no information neccessary for our flow.
    Add another Compose action, configured as follows

    "Action":
    Compose
    
    "Name":
    Remove Wrapping div
    
    "Inputs":
    slice(outputs('Parse_Page_Body'),5,sub(length(outputs('Parse_Page_Body')),6))

    The next step is one more Compose action. This one splits all controls in the page content up into a nice array – makes further processing a lot easier.

    "Action":
    Compose
    
    "Name":
    Split Page Controls
    
    "Inputs":
    skip(split(outputs('Remove_Wrapping_div'),'<div data-sp-canvascontrol='),1)

    You can combine those three Compose into just one by combining the formulas. Since PowerFX formulas are hard to read especially inside the Power Automate Editor I highly suggest keeping them separated

    Due to the nature of how the split function works, all our controls are now missing the part we splitted on (<div data-sp-canvascontrol=).

    So we need to add a Apply to each-Action, readd the splat-Part and store the control in our previously created array variable.

    "Action":
    Apply to each
    
    "Select an output form previous step":
    outputs('Split_Page_Controls')

    So far we are reacting to new translation pages being created, getting the info of what language we are targeting and how the page has been set up.
    Your flow should look like this.

    Translating

    Let’s dive into the core functionality of our Power Automate flow – the translation process. While it sounds straightforward (extract text, translate it, update the page), we need several precise steps to make it work correctly.

    Here’s what we’ll accomplish:

    1. Extract text content from page controls
    2. Process this content for translation
    3. Update the page with translated content

    Let’s start by creating a new variable called “Update Page Controls” – this will be crucial for our next steps.

    "Action":
    Initialize variable
    
    "Name":
    Update Page Controls
    
    "Type":
    Array

    The array variable will store our processed controls before we add them back to the updated page. To handle each control individually, we’ll use an Apply to each action in Power Automate. This action lets us iterate through all controls and process them one by one.

    "Action":
    Apply to each
    
    "Name":
    Iterate Page Controls
    
    "Select an output form previous step":
    variables('Page Controls')

    To process the controls, we need to transform their data format. Each control is stored as an HTML string, but Power Automate needs a different format to work with them. We’ll follow these steps:

    1. Convert the HTML string to XML
    2. Extract the essential attribute values
    3. Transform the control’s configuration string into JSON format

    Convert the HTML to XML

    "Action":
    Compose
    
    "Name":
    Convert Control To XML
    
    "Inputs":
    xml(items('Iterate_Page_Controls'))

    Extract value from attribute data-sp-canvascontrol

    "Action":
    Compose
    
    "Name":
    Get Attribute Canvascontrol 
    
    "Inputs":
    xpath(outputs('Convert_Control_To_XML'), 'string(/*/@data-sp-canvascontrol)')

    Extract value from attribute data-sp-canvasdataversion

    "Action":
    Compose
    
    "Name":
    Get Attribute Canvasdataversion
    
    "Inputs":
    xpath(outputs('Convert_Control_To_XML'), 'string(/*/@data-sp-canvasdataversion)')

    Extract value from attribute data-sp-controldata

    "Action":
    Compose
    
    "Name":
    Get Attribute Controldata
    
    "Inputs":
    xpath(outputs('Convert_Control_To_XML'), 'string(/*/@data-sp-controldata)')

    The data-sp-controldata attribute contains JSON that defines the control’s (webpart’s) position and type on the page. To work with this information in Power Automate, we need to parse this JSON.

    "Action":
    Parse JSON
    
    "Name":
    Parse Controldata
    
    "Content":
    outputs('Get Attribute Controldata')
    
    "Schema":
    {
        "type": "object",
        "properties": {
            "controlType": {
                "type": "integer"
            }
        }
    }

    The schema is stripped down, as we only ever need the controlType-property

    We need to identify which controls require translation and which can remain unchanged. The controlType property helps us make this decision. When controlType equals 4, we’re dealing with a text control that needs translation. We’ll add a condition to our loop:

    • If controlType is NOT 4: Add the control directly to our Updated Page Controls array
    • If controlType is 4: The control will need translation (which we’ll handle next)
    "Action":
    Condition
    
    "Name":
    Is Textcontrol
    
    "Condition expression":
    body('Parse Controldata')?['controlType'] is equal to 4

    Add following action to the false branch of the condition

    "Action":
    Append to array variable
    
    "Name (Property)":
    Updated Page Controls
    
    "Condition expression":
    items('Iterate_Page_Controls')

    The workflow preserves all non-text controls in their original order on the page. Next, we focus on translating the text controls and inserting them back into the Updated Page Controls array. To begin the translation process, add an action within the true branch of the condition to extract the content from the text control.

    "Action":
    Compose
    
    "Name":
    Get Textcontent
    
    "Inputs":
    xpath(outputs('Convert_Control_To_XML'), 'string(/)')

    Now we are finally at the point, where AI comes into play. Translating the text is done by a model provided by Microsoft.
    Add the following action to the flow

    "Action":
    Translate text into another language
    
    "Name":
    Translate text into another language
    
    "Translate from":
    Detect automatically
    
    "Text":
    outputs('Get Textcontent')
    
    "Translate to":
    substring(triggerBody()?['OData__SPTranslationLanguage/Value'], 0, 2)

    With the translated content at our hands, we need to recreate the text control, add it to Updated Page Controls and finally update the page.

    The first step in recreating the text control is HTML-Encoding the JSON configuration. By reading the configuration, Power Automate automatically decodes the JSON but when recreating the control, the JSON config will NOT be automatically encoded, which will lated on create issues with the page.

    Add the following action

    "Action":
    Compose
    
    "Name":
    HTML Encode JSON Config
    
    "Inputs":
    replace(replace(replace(replace(replace(outputs('Get_Attribute_Controldata'),
    '{', '&#123;'),
    '}', '&#125;'),
    ':', '&#58;'),
    '"', '&quot;'),
    ',', '&#44;')

    The next step is putting it all together as a new text control

    "Action":
    Compose
    
    "Name":
    Create new Textcontrol
    
    "Inputs":
    concat('<div data-sp-canvascontrol="', outputs('Get_Attribute_Canvascontrol_'), '" data-sp-canvasdataversion="', outputs('Get_Attribute_Canvasdataversion'), '" data-sp-controldata="',outputs('HTML_Encode_JSON_Config'),'"><div data-sp-rte="">' , outputs('Translate_text_into_another_language')?['body/responsev2/predictionOutput/text'] , '</div></div>')

    And finally add it to Updated Page Controls

    "Action":
    Append to array variable
    
    "Name (Property)":
    Updated Page Controls
    
    "Condition expression":
    outputs('Create new Textcontrol')

    At this point, our loop through the controls is complete. We’ve preserved all non-text controls and updated the text controls with their translations. The final step is to update the page in the SharePoint pages library using an HTTP request action.

    "Action":
    Send an HTTP request to SharePoint
    
    "Site Address":
    Same as in the trigger
    
    "Method":
    POST
    
    "Uri (use insert expression)":
    _api/web/lists/getbytitle('Site Pages')/items(@{triggerBody()?['ID']})
    
    "Headers":
    - Content-Type: application/json
    - IF-MATCH: *
    - X-HTTP-METHOD: MERGE
    
    "Body":
    {'CanvasContent1':'<FORMULA-HERE>'}
    
    Add the following formula in place of <FORMULA-HERE>:
    replace(concat('<div>',join(variables('Updated Page Controls'),''),'</div>'), '''', '\''')

    The replace function escapes single quotes (') in the new text control, as unescaped ones will lead to errors while running the update.

    Thats it – the flow should look like this now.

    All that is left to do is testing.
    Create new pages, add some text (and some other web parts) and create some translation pages.
    The new pages should contain the translated text as well as all other web parts from the root-page.

  • Exploring AI in Power Automate – Sentiment Analysis

    Sentiment analysis is a powerful technique that helps businesses understand the emotional undertones of customer feedback. By automatically classifying text as positive, negative, or neutral, companies can gain insights beyond simple written words. In this guide, we’ll explore how to build a sentiment analysis workflow using Power Automate that processes customer reviews submitted via email.

    Traditional survey methods (like Microsoft Forms) often constrain customer expression, whereas an open email approach offers more flexibility. By allowing customers to share their experiences in their own words, we can capture more nuanced and authentic feedback. This method not only reduces barriers to providing input but also increases the likelihood of meaningful customer participation.

    The setup

    Setting up an effective sentiment analysis system requires careful infrastructure planning.
    We’ll create three essential components to streamline our customer feedback process: a shared inbox to receive reviews, a SharePoint teamsite for centralized oversight, and a SharePoint list that holds the customer reviews and their analysed scores.

    The teamsite

    The first step in our sentiment analysis project involves creating a SharePoint team site, which offers integration with Microsoft Teams. Here’s how to set up your collaborative workspace:

    Navigate to the site management in the (https://<tenantname>-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx#/siteManagement/) and create a new teamsite.
    You are free to chose the name and the template type, for this guide I am going to use the Microsoft Standard Teamsite template and set the name to Customer Review Team

    After our teamsite has been created, open its settings and click on the Add Teams-Button in the banner that says
    Would you like to add Microsoft Teams to this group?

    The list

    To store our review data and scores, we are going to need a list.
    Open the previously created teamsite and create a new list (Site contents → New → List).
    Use the blank list template and name it Customer Reviews.

    The list gets created containing only the Title column. We are going to add the more columns, to hold the From-Address, the Review-Text (Body) and the score.
    The following screenshots will show you how to configure the columns

    The shared mailbox

    The last setup step we have to take is creating a shared mailbox where our customers can send the reviews to.
    Open the Microsoft 365 Admin Center (https://admin.microsoft.com/#/SharedMailbox) and create a new shared mailbox.

    Click the Add a shared mailbox button and fill the required fileds.
    I have named my mailbox Cusomter Review Mailbox and set the adress to feedback@tenantname.onmicrosoft.com.

    Now you need to add members to the mailbox. Currently, the only member necessary is the user in whose context the flow will run.
    Creating the mailbox completes the setup step. We are now ready to tackle the automation.

    The automation

    Now it is time to create the flow that runs a sentiment analysis on a customer submitted review.

    Got to make.powerautomate.com and create a new Automated cloud flow.

    My flows → New flow → Automated cloud flow

    Give it any name, I have named mine Customer Review Sentiment Analysis.

    As trigger select When a new email arrives in a shared mailbox (V2)

    After the flow creation has been completed, configure the trigger and set our shared mailbox address as Original Mailbox Address.
    Further trigger configuration is not required.

    Detect the language

    The first part of analysing customer reviews is finding out in what language they where submitted. To do just that, create a new variable, call it obj_BodyLanguage, give it the type Object and set the value to the following JSON.

    {
      "score": 0,
      "language": "en"
    }

    Add the Detect the language being used in text action to your flow and pass the Body of your trigger action as text.
    The result of Detect the language being used in text is an array of languages each with a confidence score indicating how “sure” the AI is about this language.
    We are going to iterate through all the results and if a results confidence score is higher than the score of our current obj_BodyLanguage, overwrite obj_BodyLanguage with the result.
    In the end we will have the language with the highest confidence score set for further usage

    Analyse the sentiment

    Now that we know what language the text was written in, its time to run a sentiment analysis.
    Add the Analyze positive or negative sentiment in text-action to the flow, set the language parameter to variables('obj_BodyLanguage')['language'] and the text parameter to the triggers Body.

    The Analyze positive or negative sentiment in text action has multiple outcomes, and we are working with Probability overall text is negative, Probability overall text is positive and Probability overall text is neutral.
    Scores are returned as two-digit decimals between 0 and 1 and all scores (negative, postive and neutral) summed up equal 1.
    We are calculating the overal sentiment score using the following formular

    Positive Score + (Neutral Score / 2)

    We are dividing the neutral score by 2 to add its weight to both positive and negative.
    This calculation results in a decimal number between 0 and 1 – representing the sentiment percentage.

    Add a Compose action that runs the following formula to calculate the overal score.

    add(outputs('Analyze_positive_or_negative_sentiment_in_text')?['body/responsev2/predictionOutput/result/documentScores/positive'],div(outputs('Analyze_positive_or_negative_sentiment_in_text')?['body/responsev2/predictionOutput/result/documentScores/neutral'],2))

    Store the data

    With our sentiment analysis complete, let’s store the results in SharePoint for future analysis and reporting. We’ll add a Create item action to our flow that saves each review with its calculated sentiment score.

    Configure the action as follows:

    Select your Customer Reviews site as the target SharePoint site
    Choose the Customer Reviews List we created earlier
    Map the following email data to their respective columns:

    Title: Mail subject line
    From: Sender’s email address
    Body: Full review text
    Score: Calculated sentiment score from our previous step

    Remember to save your workflow and test it by sending a few sample reviews to ensure everything works as expected.

    Wrapping up

    The complete workflow should look like the following screenshot

    Thats it. Every mail received will be processed and the sentiment score will be safed.
    There are a lot improvements that can be done to improve our application but thats no content for this guide.

    You could

    Add custom column formatting
    to the score column in the reviews list, to render the percentage a different way, maybe as stars that get filled based on the score.

    Notify all team members
    Make the teamsite available in teams and add notifications.

    Incorporate planner
    Bad reviews could lead to automatic planner task creation for our customer review team.

    …and so forth.

  • Programmatically working with SharePoint Online Image Renditions

    What are SharePoint Online Image Renditions?

    Image Renditions were a popular feature in SharePoint On-Premises that automatically generated different image resolutions directly within the system. The advantage of this approach was that optimized images were available for various use cases – for example, smaller thumbnails to avoid loading large original images. This feature not only significantly improved performance by allowing smaller images to load faster in the browser but also reduced network load, which in turn sped up the loading of other resources.

    Microsoft provides a module that allows Image Renditions to be requested from the server using JavaScript.

    The Image Helper API is part of the module with the same name and must first be integrated into the existing SPFx application.

    npm i @microsoft/sp-image-helper

    Once the module is installed, the convertToImageUrl function can be used to generate a rendition for a given Image-URL.

    import { 
        ImageHelper, 
        IImageHelperRequest 
    } from "@microsoft/sp-image-helper";
    
    const renditionSrc = ImageHelper.convertToImageUrl({
        sourceUrl: 'absolute/path/to/image.png',
        width: 1280,
        height: 1024 //optional
    });

    How does this work?

    SharePoint Online generates Image Renditions for images stored within a tenant. Since the API operates synchronously, the requested Rendition seems to already exist when the API is called.

    It’s important to note that Renditions are not available for every possible size. Instead, they are generated for a predefined set of image dimensions. When you request a specific size, the API will return the closest available Rendition to match your request.

    Using Image Renditions the React-way

    To use SharePoint Image Renditions in React, we’ll start by creating a component that encapsulates the Image Rendition API. This will provide us with a reusable and convenient way to work with image renditions.

    ImageRendition.tsx

    import * as React from "react";
    import { useState, useEffect } from "react";
    import { 
        ImageHelper, 
        IImageHelperRequest 
    } from "@microsoft/sp-image-helper";
    
    type ImageRenditionProps = {
      src: string;
      width?: number;
      height?: number;
      altText?: string;
    };
    
    export const ImageRendition = (props: ImageRenditionProps) => {
      const [renditionSrc, setRenditionSrc] = useState<string>("");
    
      function getRenditionSrc(originSrc: string) {
        if (props.width === undefined || props.width === 0) { 
            return originSrc;
        }
    
        const opts: IImageHelperRequest = {
          sourceUrl: originSrc,
          width: props.width!,
        };
    
        if (props.height) {
          opts.height = props.height;
        }
        return ImageHelper.convertToImageUrl(opts);
      }
    
      useEffect(() => {
        setRenditionSrc(getRenditionSrc(props.src));
      }, [props.src, props.width, props.height]);
    
      return (
        <img
          src={renditionSrc}
          alt={props.altText}
        />
      );
    };

    This React component can then be easily used as shown in the following example:

    MyComponent.tsx

    import { ImageRendition } from "./ImageRendition";
    
    <ImageRendition
        src="https://mytenant.sharepoint.com/Images1/916-4000x3000.jpg"
        width={400}
        height={800}
    />

    For a detailed explanation of the Image Rendition API, refer to the official Microsoft article on the topic.
    https://learn.microsoft.com/en-us/sharepoint/dev/spfx/image-helper-api