Enterprise Workflow Automation: Integrating Azure Functions, Power Automate, and SharePoint

Introduction

Modern enterprises require sophisticated workflow automation that seamlessly integrates cloud services, business logic, and collaboration tools. In this deep dive, we'll architect and implement a complete enterprise solution that demonstrates the power of combining Azure Functions, Power Automate, and SharePoint Online.

This integrated solution addresses a real-world scenario: automated invoice processing and approval workflow. We'll build a system that ingests invoices from email, extracts data using Azure AI services, stores documents in SharePoint, routes approvals through Power Automate, and tracks everything in a centralized dashboardβ€”all while maintaining security, auditability, and scalability.

What You'll Build:

  • Azure Functions for document processing and AI integration
  • Power Automate flows for approval orchestration
  • SharePoint document libraries with custom metadata
  • Azure Service Bus for reliable messaging
  • Application Insights for monitoring
  • Security integration with Azure AD and SharePoint permissions

Architecture Overview

Solution Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Enterprise Workflow Solution                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                   β”‚
β”‚  Email (Invoice) ──► Azure Logic Apps ──► Service Bus Queue     β”‚
β”‚                                                β”‚                  β”‚
β”‚                                                β–Ό                  β”‚
β”‚                                      Azure Functions              β”‚
β”‚                                      β”œβ”€ Process Document          β”‚
β”‚                                      β”œβ”€ AI Document Intelligence  β”‚
β”‚                                      └─ Data Validation           β”‚
β”‚                                                β”‚                  β”‚
β”‚                                                β–Ό                  β”‚
β”‚                              SharePoint Document Library          β”‚
β”‚                              β”œβ”€ Store Invoice PDF                 β”‚
β”‚                              β”œβ”€ Metadata (Amount, Vendor, etc)    β”‚
β”‚                              └─ Trigger Approval Flow             β”‚
β”‚                                                β”‚                  β”‚
β”‚                                                β–Ό                  β”‚
β”‚                                        Power Automate             β”‚
β”‚                                        β”œβ”€ Approval Routing        β”‚
β”‚                                        β”œβ”€ Notifications           β”‚
β”‚                                        └─ Status Updates          β”‚
β”‚                                                β”‚                  β”‚
β”‚                                                β–Ό                  β”‚
β”‚                                    Power BI Dashboard             β”‚
β”‚                                    (Analytics & Reporting)        β”‚
β”‚                                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Technology Stack

Azure Services:

  • Azure Functions (C# .NET 8)
  • Azure Service Bus
  • Azure Cognitive Services (Document Intelligence)
  • Azure Key Vault
  • Application Insights

Microsoft 365:

  • SharePoint Online
  • Power Automate
  • Power BI

Prerequisites

Azure Resources Required

  • Azure subscription with Contributor access
  • Resource group for all components
  • Azure AD app registration for authentication

Development Tools

  • Visual Studio 2022 or VS Code
  • Azure Functions Core Tools v4
  • Azure CLI
  • PowerShell 7+
  • Postman (for API testing)

Microsoft 365 Requirements

  • SharePoint Online site with full control
  • Power Automate premium license (for HTTP actions)
  • Power BI Pro license (for dashboard publishing)

Skills Needed

  • C# programming
  • REST API development
  • Power Automate flow design
  • SharePoint administration
  • Basic Azure networking

Step 1: Provision Azure Infrastructure

Deploy Resources via Azure CLI

# Variables
$resourceGroup = "rg-invoice-workflow"
$location = "eastus"
$storageAccount = "stinvoiceproc$(Get-Random -Minimum 1000 -Maximum 9999)"
$functionApp = "func-invoice-processor"
$serviceBusNamespace = "sb-invoice-workflow"
$keyVault = "kv-invoice-secrets"
$appInsights = "ai-invoice-monitoring"
$cognitiveService = "cog-document-intel"

# Create resource group
az group create --name $resourceGroup --location $location

# Create storage account for Functions
az storage account create `
  --name $storageAccount `
  --resource-group $resourceGroup `
  --location $location `
  --sku Standard_LRS

# Create Application Insights
az monitor app-insights component create `
  --app $appInsights `
  --location $location `
  --resource-group $resourceGroup `
  --application-type web

# Create Azure Functions app
az functionapp create `
  --resource-group $resourceGroup `
  --consumption-plan-location $location `
  --runtime dotnet-isolated `
  --runtime-version 8 `
  --functions-version 4 `
  --name $functionApp `
  --storage-account $storageAccount `
  --app-insights $appInsights

# Create Service Bus namespace and queue
az servicebus namespace create `
  --resource-group $resourceGroup `
  --name $serviceBusNamespace `
  --location $location `
  --sku Standard

az servicebus queue create `
  --resource-group $resourceGroup `
  --namespace-name $serviceBusNamespace `
  --name invoice-processing-queue `
  --max-delivery-count 10 `
  --lock-duration PT5M

# Create Cognitive Services for Document Intelligence
az cognitiveservices account create `
  --name $cognitiveService `
  --resource-group $resourceGroup `
  --kind FormRecognizer `
  --sku S0 `
  --location $location `
  --yes

# Create Key Vault
az keyvault create `
  --name $keyVault `
  --resource-group $resourceGroup `
  --location $location `
  --enable-rbac-authorization false

# Enable managed identity for Function App
az functionapp identity assign `
  --name $functionApp `
  --resource-group $resourceGroup

# Get the managed identity principal ID
$principalId = az functionapp identity show `
  --name $functionApp `
  --resource-group $resourceGroup `
  --query principalId -o tsv

# Grant Key Vault access to Function App
az keyvault set-policy `
  --name $keyVault `
  --object-id $principalId `
  --secret-permissions get list

Store Secrets in Key Vault

# Get Service Bus connection string
$serviceBusConn = az servicebus namespace authorization-rule keys list `
  --resource-group $resourceGroup `
  --namespace-name $serviceBusNamespace `
  --name RootManageSharedAccessKey `
  --query primaryConnectionString -o tsv

# Get Cognitive Services key
$cognitiveKey = az cognitiveservices account keys list `
  --name $cognitiveService `
  --resource-group $resourceGroup `
  --query key1 -o tsv

# Get Cognitive Services endpoint
$cognitiveEndpoint = az cognitiveservices account show `
  --name $cognitiveService `
  --resource-group $resourceGroup `
  --query properties.endpoint -o tsv

# Store secrets
az keyvault secret set --vault-name $keyVault --name "ServiceBusConnection" --value $serviceBusConn
az keyvault secret set --vault-name $keyVault --name "CognitiveServicesKey" --value $cognitiveKey
az keyvault secret set --vault-name $keyVault --name "CognitiveServicesEndpoint" --value $cognitiveEndpoint

Step 2: Configure SharePoint Site

Create Document Library with Custom Columns

# Install PnP PowerShell if not already installed
Install-Module -Name PnP.PowerShell -Force -AllowClobber

# Connect to SharePoint
$siteUrl = "https://yourtenant.sharepoint.com/sites/InvoiceProcessing"
Connect-PnPOnline -Url $siteUrl -Interactive

# Create document library
New-PnPList -Title "InvoiceDocuments" -Template DocumentLibrary -OnQuickLaunch

# Add custom columns
Add-PnPField -List "InvoiceDocuments" -DisplayName "Vendor Name" -InternalName "VendorName" -Type Text -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Invoice Number" -InternalName "InvoiceNumber" -Type Text -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Invoice Date" -InternalName "InvoiceDate" -Type DateTime -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Total Amount" -InternalName "TotalAmount" -Type Currency -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Currency" -InternalName "Currency" -Type Text -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Approval Status" -InternalName "ApprovalStatus" -Type Choice -Choices "Pending","Approved","Rejected" -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Approver" -InternalName "Approver" -Type User -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Processing Status" -InternalName "ProcessingStatus" -Type Choice -Choices "Received","Processing","Completed","Failed" -AddToDefaultView
Add-PnPField -List "InvoiceDocuments" -DisplayName "Confidence Score" -InternalName "ConfidenceScore" -Type Number -AddToDefaultView

# Create approval view
Add-PnPView -List "InvoiceDocuments" -Title "Pending Approval" -Fields "FileLeafRef","VendorName","InvoiceNumber","TotalAmount","InvoiceDate","ApprovalStatus" -Query '<Where><Eq><FieldRef Name="ApprovalStatus"/><Value Type="Choice">Pending</Value></Eq></Where>'

Configure SharePoint App Registration

# Register Azure AD App for SharePoint access
$appName = "InvoiceProcessorApp"

# Create app registration (requires Azure AD admin)
az ad app create --display-name $appName

# Get app ID
$appId = az ad app list --display-name $appName --query [0].appId -o tsv

# Grant SharePoint permissions
az ad app permission add `
  --id $appId `
  --api 00000003-0000-0ff1-ce00-000000000000 `
  --api-permissions 2cfdc887-d7b4-4798-9b33-3d98d6b95dd9=Role

# Create client secret
$clientSecret = az ad app credential reset --id $appId --query password -o tsv

# Store SharePoint credentials in Key Vault
az keyvault secret set --vault-name $keyVault --name "SharePointClientId" --value $appId
az keyvault secret set --vault-name $keyVault --name "SharePointClientSecret" --value $clientSecret
az keyvault secret set --vault-name $keyVault --name "SharePointSiteUrl" --value $siteUrl

Step 3: Build Azure Functions

Function App Structure

InvoiceProcessorFunctions/
β”œβ”€β”€ InvoiceProcessorFunctions.csproj
β”œβ”€β”€ host.json
β”œβ”€β”€ local.settings.json
β”œβ”€β”€ Models/
β”‚   β”œβ”€β”€ InvoiceDocument.cs
β”‚   └── ProcessingResult.cs
β”œβ”€β”€ Services/
β”‚   β”œβ”€β”€ IDocumentIntelligenceService.cs
β”‚   β”œβ”€β”€ DocumentIntelligenceService.cs
β”‚   β”œβ”€β”€ ISharePointService.cs
β”‚   └── SharePointService.cs
└── Functions/
    β”œβ”€β”€ ProcessInvoiceFunction.cs
    └── UpdateApprovalStatusFunction.cs

Invoice Document Model

// Models/InvoiceDocument.cs
namespace InvoiceProcessorFunctions.Models;

public class InvoiceDocument
{
    public string FileName { get; set; } = string.Empty;
    public string VendorName { get; set; } = string.Empty;
    public string InvoiceNumber { get; set; } = string.Empty;
    public DateTime? InvoiceDate { get; set; }
    public decimal TotalAmount { get; set; }
    public string Currency { get; set; } = "USD";
    public double ConfidenceScore { get; set; }
    public byte[] FileContent { get; set; } = Array.Empty<byte>();
    public string ContentType { get; set; } = "application/pdf";
}

public class ProcessingResult
{
    public bool Success { get; set; }
    public string Message { get; set; } = string.Empty;
    public string SharePointItemId { get; set; } = string.Empty;
    public Dictionary<string, object> ExtractedData { get; set; } = new();
}

Document Intelligence Service

// Services/DocumentIntelligenceService.cs
using Azure;
using Azure.AI.FormRecognizer.DocumentAnalysis;
using InvoiceProcessorFunctions.Models;
using Microsoft.Extensions.Logging;

namespace InvoiceProcessorFunctions.Services;

public interface IDocumentIntelligenceService
{
    Task<InvoiceDocument> AnalyzeInvoiceAsync(byte[] documentContent, string fileName);
}

public class DocumentIntelligenceService : IDocumentIntelligenceService
{
    private readonly DocumentAnalysisClient _client;
    private readonly ILogger<DocumentIntelligenceService> _logger;

    public DocumentIntelligenceService(string endpoint, string apiKey, ILogger<DocumentIntelligenceService> logger)
    {
        _client = new DocumentAnalysisClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
        _logger = logger;
    }

    public async Task<InvoiceDocument> AnalyzeInvoiceAsync(byte[] documentContent, string fileName)
    {
        try
        {
            using var stream = new MemoryStream(documentContent);
            
            var operation = await _client.AnalyzeDocumentAsync(
                WaitUntil.Completed,
                "prebuilt-invoice",
                stream);

            var result = operation.Value;
            var invoice = new InvoiceDocument
            {
                FileName = fileName,
                FileContent = documentContent,
                ContentType = "application/pdf"
            };

            if (result.Documents.Count > 0)
            {
                var document = result.Documents[0];
                
                // Extract vendor name
                if (document.Fields.TryGetValue("VendorName", out var vendorField))
                {
                    invoice.VendorName = vendorField.Content ?? string.Empty;
                }

                // Extract invoice number
                if (document.Fields.TryGetValue("InvoiceId", out var invoiceIdField))
                {
                    invoice.InvoiceNumber = invoiceIdField.Content ?? string.Empty;
                }

                // Extract invoice date
                if (document.Fields.TryGetValue("InvoiceDate", out var dateField) && 
                    dateField.FieldType == DocumentFieldType.Date)
                {
                    invoice.InvoiceDate = dateField.Value.AsDate();
                }

                // Extract total amount
                if (document.Fields.TryGetValue("InvoiceTotal", out var totalField) && 
                    totalField.FieldType == DocumentFieldType.Currency)
                {
                    var currencyValue = totalField.Value.AsCurrency();
                    invoice.TotalAmount = (decimal)currencyValue.Amount;
                    invoice.Currency = currencyValue.CurrencyCode ?? "USD";
                }

                // Calculate average confidence score
                invoice.ConfidenceScore = document.Fields.Values
                    .Where(f => f.Confidence.HasValue)
                    .Average(f => f.Confidence!.Value);

                _logger.LogInformation(
                    "Successfully analyzed invoice {FileName}. Vendor: {Vendor}, Amount: {Amount} {Currency}",
                    fileName, invoice.VendorName, invoice.TotalAmount, invoice.Currency);
            }

            return invoice;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error analyzing document {FileName}", fileName);
            throw;
        }
    }
}

SharePoint Service

// Services/SharePointService.cs
using InvoiceProcessorFunctions.Models;
using Microsoft.Extensions.Logging;
using PnP.Core.Services;
using PnP.Core.Model.SharePoint;

namespace InvoiceProcessorFunctions.Services;

public interface ISharePointService
{
    Task<string> UploadInvoiceAsync(InvoiceDocument invoice);
    Task UpdateApprovalStatusAsync(string itemId, string status, string approver);
}

public class SharePointService : ISharePointService
{
    private readonly IPnPContextFactory _pnpContextFactory;
    private readonly string _siteUrl;
    private readonly ILogger<SharePointService> _logger;

    public SharePointService(
        IPnPContextFactory pnpContextFactory, 
        string siteUrl,
        ILogger<SharePointService> logger)
    {
        _pnpContextFactory = pnpContextFactory;
        _siteUrl = siteUrl;
        _logger = logger;
    }

    public async Task<string> UploadInvoiceAsync(InvoiceDocument invoice)
    {
        try
        {
            using var context = await _pnpContextFactory.CreateAsync(new Uri(_siteUrl));
            var documentLibrary = context.Web.Lists.GetByTitle("InvoiceDocuments");

            // Upload file
            var uploadedFile = await documentLibrary.RootFolder.Files.AddAsync(
                invoice.FileName,
                new MemoryStream(invoice.FileContent),
                true);

            // Get list item and update metadata
            var listItem = await uploadedFile.GetListItemAsync();
            
            listItem["VendorName"] = invoice.VendorName;
            listItem["InvoiceNumber"] = invoice.InvoiceNumber;
            listItem["InvoiceDate"] = invoice.InvoiceDate;
            listItem["TotalAmount"] = invoice.TotalAmount;
            listItem["Currency"] = invoice.Currency;
            listItem["ApprovalStatus"] = "Pending";
            listItem["ProcessingStatus"] = "Completed";
            listItem["ConfidenceScore"] = invoice.ConfidenceScore;

            await listItem.UpdateAsync();

            _logger.LogInformation(
                "Successfully uploaded invoice {FileName} to SharePoint. Item ID: {ItemId}",
                invoice.FileName, listItem.Id);

            return listItem.Id.ToString();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error uploading invoice {FileName} to SharePoint", invoice.FileName);
            throw;
        }
    }

    public async Task UpdateApprovalStatusAsync(string itemId, string status, string approver)
    {
        try
        {
            using var context = await _pnpContextFactory.CreateAsync(new Uri(_siteUrl));
            var documentLibrary = context.Web.Lists.GetByTitle("InvoiceDocuments");
            var listItem = await documentLibrary.GetItemByIdAsync(int.Parse(itemId));

            listItem["ApprovalStatus"] = status;
            listItem["Approver"] = approver;
            
            await listItem.UpdateAsync();

            _logger.LogInformation(
                "Updated approval status for item {ItemId} to {Status}",
                itemId, status);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating approval status for item {ItemId}", itemId);
            throw;
        }
    }
}

Process Invoice Function

// Functions/ProcessInvoiceFunction.cs
using Azure.Messaging.ServiceBus;
using InvoiceProcessorFunctions.Models;
using InvoiceProcessorFunctions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace InvoiceProcessorFunctions.Functions;

public class ProcessInvoiceFunction
{
    private readonly IDocumentIntelligenceService _documentService;
    private readonly ISharePointService _sharePointService;
    private readonly ILogger<ProcessInvoiceFunction> _logger;

    public ProcessInvoiceFunction(
        IDocumentIntelligenceService documentService,
        ISharePointService sharePointService,
        ILogger<ProcessInvoiceFunction> logger)
    {
        _documentService = documentService;
        _sharePointService = sharePointService;
        _logger = logger;
    }

    [Function("ProcessInvoice")]
    public async Task Run(
        [ServiceBusTrigger("invoice-processing-queue", Connection = "ServiceBusConnection")] 
        ServiceBusReceivedMessage message)
    {
        _logger.LogInformation("Processing invoice message: {MessageId}", message.MessageId);

        try
        {
            var messageBody = message.Body.ToArray();
            
            // Deserialize message (contains file name and blob URL or base64 content)
            var invoiceMessage = JsonSerializer.Deserialize<InvoiceMessage>(messageBody);
            
            if (invoiceMessage == null)
            {
                _logger.LogError("Failed to deserialize message");
                return;
            }

            // Get document content (simplified - in production, retrieve from Blob Storage)
            byte[] documentContent = Convert.FromBase64String(invoiceMessage.FileContentBase64);

            // Analyze document with AI
            var invoice = await _documentService.AnalyzeInvoiceAsync(
                documentContent, 
                invoiceMessage.FileName);

            // Upload to SharePoint
            var itemId = await _sharePointService.UploadInvoiceAsync(invoice);

            _logger.LogInformation(
                "Successfully processed invoice {FileName}. SharePoint Item ID: {ItemId}",
                invoiceMessage.FileName, itemId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing invoice message {MessageId}", message.MessageId);
            throw; // Trigger retry
        }
    }
}

public class InvoiceMessage
{
    public string FileName { get; set; } = string.Empty;
    public string FileContentBase64 { get; set; } = string.Empty;
}

Program.cs Configuration

// Program.cs
using Azure.Identity;
using InvoiceProcessorFunctions.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PnP.Core.Auth;
using PnP.Core.Services.Builder.Configuration;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureAppConfiguration((context, config) =>
    {
        var keyVaultName = Environment.GetEnvironmentVariable("KeyVaultName");
        if (!string.IsNullOrEmpty(keyVaultName))
        {
            config.AddAzureKeyVault(
                new Uri($"https://{keyVaultName}.vault.azure.net/"),
                new DefaultAzureCredential());
        }
    })
    .ConfigureServices((context, services) =>
    {
        var configuration = context.Configuration;

        // Document Intelligence Service
        services.AddSingleton<IDocumentIntelligenceService>(sp =>
        {
            var logger = sp.GetRequiredService<ILogger<DocumentIntelligenceService>>();
            var endpoint = configuration["CognitiveServicesEndpoint"]!;
            var apiKey = configuration["CognitiveServicesKey"]!;
            return new DocumentIntelligenceService(endpoint, apiKey, logger);
        });

        // PnP Core SDK for SharePoint
        services.AddPnPCore(options =>
        {
            options.DefaultAuthenticationProvider = new ClientCredentialsAuthenticationProvider(
                configuration["SharePointClientId"]!,
                configuration["SharePointTenantId"]!,
                configuration["SharePointClientSecret"]!);
        });

        // SharePoint Service
        services.AddScoped<ISharePointService, SharePointService>(sp =>
        {
            var pnpContextFactory = sp.GetRequiredService<IPnPContextFactory>();
            var logger = sp.GetRequiredService<ILogger<SharePointService>>();
            var siteUrl = configuration["SharePointSiteUrl"]!;
            return new SharePointService(pnpContextFactory, siteUrl, logger);
        });

        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();

Step 4: Create Power Automate Flow

Approval Flow Trigger

  1. Trigger: When a file is created or modified (properties only) in SharePoint

    • Site Address: Your SharePoint site
    • Library Name: InvoiceDocuments
    • Folder: Leave blank for entire library
  2. Condition: Check if Approval Status is "Pending"

    ApprovalStatus is equal to Pending
    

If Yes Branch - Route for Approval

  1. Get file metadata: Get detailed invoice information

    • Site Address: Your SharePoint site
    • Library Name: InvoiceDocuments
    • ID: Use dynamic content from trigger
  2. Determine Approver: Based on amount

    if(TotalAmount > 5000, 'senior.manager@company.com', 'manager@company.com')
    
  3. Start and wait for approval: Send approval request

    • Approval type: Approve/Reject - First to respond
    • Title: Approve Invoice @{triggerOutputs()?['body/InvoiceNumber']}
    • Assigned to: Use expression from step 4
    • Details:
      Vendor: @{triggerOutputs()?['body/VendorName']}
      Amount: @{triggerOutputs()?['body/TotalAmount']} @{triggerOutputs()?['body/Currency']}
      Invoice Date: @{triggerOutputs()?['body/InvoiceDate']}
      Confidence: @{triggerOutputs()?['body/ConfidenceScore']}%
      
      [View Document](@{triggerOutputs()?['body/{Link}']})
      
  4. Update SharePoint item: After approval response

    • Site Address: Your SharePoint site
    • Library Name: InvoiceDocuments
    • ID: Use trigger ID
    • Approval Status: @{outputs('Start_and_wait_for_approval')?['body/outcome']}
    • Approver: @{outputs('Start_and_wait_for_approval')?['body/responder/displayName']}
  5. Send email notification: Notify submitter

    • To: (Get from item metadata or use fixed address)
    • Subject: Invoice @{triggerOutputs()?['body/InvoiceNumber']} - @{outputs('Start_and_wait_for_approval')?['body/outcome']}
    • Body:
      Your invoice has been @{outputs('Start_and_wait_for_approval')?['body/outcome']}.
      
      Details:
      - Vendor: @{triggerOutputs()?['body/VendorName']}
      - Amount: @{triggerOutputs()?['body/TotalAmount']}
      - Approver: @{outputs('Start_and_wait_for_approval')?['body/responder/displayName']}
      - Comments: @{outputs('Start_and_wait_for_approval')?['body/responderComments']}
      

Flow JSON Export (simplified)

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "triggers": {
      "When_a_file_is_created_or_modified": {
        "type": "ApiConnection",
        "inputs": {
          "host": {
            "connection": {
              "name": "@parameters('$connections')['sharepointonline']['connectionId']"
            }
          },
          "method": "get",
          "path": "/datasets/@{encodeURIComponent('https://yourtenant.sharepoint.com/sites/InvoiceProcessing')}/tables/@{encodeURIComponent('InvoiceDocuments')}/onupdatedfile"
        }
      }
    },
    "actions": {
      "Condition_Check_Pending": {
        "type": "If",
        "expression": {
          "equals": ["@triggerOutputs()?['body/ApprovalStatus']", "Pending"]
        },
        "actions": {
          "Start_and_wait_for_approval": {
            "type": "ApiConnectionWebhook",
            "inputs": {
              "host": {
                "connection": {
                  "name": "@parameters('$connections')['approvals']['connectionId']"
                }
              },
              "path": "/approvalflows/basicawaitall"
            }
          }
        }
      }
    }
  }
}

Step 5: Testing the Solution

End-to-End Test

  1. Send test invoice to Service Bus:
var connectionString = "<your-service-bus-connection>";
var client = new ServiceBusClient(connectionString);
var sender = client.CreateSender("invoice-processing-queue");

var testInvoice = new InvoiceMessage
{
    FileName = "test-invoice-001.pdf",
    FileContentBase64 = Convert.ToBase64String(File.ReadAllBytes("sample-invoice.pdf"))
};

var message = new ServiceBusMessage(JsonSerializer.Serialize(testInvoice));
await sender.SendMessageAsync(message);
  1. Monitor Function execution in Application Insights
  2. Verify SharePoint upload and metadata extraction
  3. Check Power Automate approval request
  4. Approve/Reject and verify status update

Monitoring Queries (Application Insights)

// Failed function executions
requests
| where success == false
| where operation_Name startswith "ProcessInvoice"
| project timestamp, operation_Name, resultCode, duration, customDimensions
| order by timestamp desc

// Average processing time
requests
| where operation_Name == "ProcessInvoice"
| summarize avg(duration), percentile(duration, 95) by bin(timestamp, 1h)

// Invoice processing volume
customEvents
| where name == "InvoiceProcessed"
| summarize count() by bin(timestamp, 1d)

Best Practices

1. Security

  • Use managed identities for Azure service authentication
  • Store all secrets in Azure Key Vault
  • Implement least-privilege access for SharePoint permissions
  • Enable audit logging for all approval actions
  • Use Azure AD Conditional Access for sensitive operations

2. Error Handling

  • Implement retry policies with exponential backoff
  • Use dead-letter queues for failed messages
  • Log all exceptions with correlation IDs
  • Create alerts for processing failures
  • Maintain error dashboards in Application Insights

3. Performance

  • Use Service Bus sessions for ordered processing
  • Implement parallel processing where possible
  • Cache SharePoint metadata to reduce API calls
  • Optimize document upload with chunking for large files
  • Monitor and adjust Function timeout settings

4. Scalability

  • Design for horizontal scaling with stateless functions
  • Use Premium Functions plan for predictable performance
  • Implement throttling and rate limiting
  • Partition Service Bus queues by priority/region
  • Consider Azure Durable Functions for complex orchestrations

5. Governance

  • Document approval workflows and SLAs
  • Implement version control for Power Automate flows
  • Create runbooks for common issues
  • Establish change management procedures
  • Regular security and compliance audits

Cost Optimization

Estimated Monthly Costs (1000 invoices/month):

  • Azure Functions (Consumption): ~$5
  • Service Bus (Standard): ~$10
  • Cognitive Services (S0): ~$150
  • Storage Account: ~$2
  • Application Insights: ~$10
  • Total: ~$177/month

Cost Reduction Strategies:

  • Use Reserved Instances for predictable workloads
  • Implement document batching to reduce API calls
  • Archive old invoices to cool storage
  • Use Free tier of Power Automate where possible
  • Monitor and optimize Cognitive Services usage

Troubleshooting

Common Issues

Issue: Document Intelligence returns low confidence scores
Solution: Ensure invoices are high-quality PDFs, train custom models for specific formats

Issue: SharePoint upload fails with permission errors
Solution: Verify app registration permissions and consent grants

Issue: Power Automate approval not triggered
Solution: Check flow run history, verify trigger conditions match metadata values

Issue: Service Bus messages timing out
Solution: Increase lock duration, optimize function processing time

Key Takeaways

  1. Integrated solutions leverage strengths of each platformβ€”Azure for compute, Power Platform for workflows, SharePoint for collaboration
  2. AI services like Document Intelligence dramatically reduce manual data entry with high accuracy
  3. Serverless architecture with Azure Functions provides cost-effective, scalable processing
  4. Power Automate excels at approval routing and human-in-the-loop workflows
  5. Monitoring and observability are critical for enterprise-grade reliability
  6. Security and governance must be designed in from the start, not bolted on later

Additional Resources

Next Steps

Enhance this solution:

  • Add Power BI dashboard for invoice analytics
  • Implement ML.NET for fraud detection
  • Create custom Document Intelligence models
  • Add OCR for scanned invoices
  • Integrate with ERP systems (SAP, Dynamics)
  • Build mobile approval app with Power Apps

Explore related patterns:

  • Event-driven architectures with Event Grid
  • Workflow orchestration with Durable Functions
  • Multi-tenant SaaS applications
  • Real-time dashboards with SignalR

Ready to transform your business processes with integrated cloud solutions? Start with this foundation and customize it for your specific workflows. Share your implementation experiences and lessons learned!

Expanded Architecture (ASCII – End-to-End Data & Control Flow)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                      Enterprise Workflow Platform                                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                                                        β”‚
β”‚  Incoming Invoices                                                                                     β”‚
β”‚    β€’ PDF / Image / Email Attachments                                                                   β”‚
β”‚    β€’ EDI (Future)                                                                                      β”‚
β”‚          β”‚                                                                                             β”‚
β”‚          β–Ό                                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  (1) Ingestion Function (HTTP / Graph)                                        β”‚
β”‚  β”‚ Ingestion Function  │──Extract basic metadata (supplier, date)                                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  Push raw blob to Storage                                                     β”‚
β”‚          β”‚                                                                                             β”‚
β”‚          β–Ό                                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  (2) Document Intelligence (Prebuilt / Custom Model)                          β”‚
β”‚  β”‚  Doc Intelligence   │──Extract structured fields (InvoiceId, Total, Tax, Currency, DueDate…)        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  Confidence thresholds + field normalization                                  β”‚
β”‚          β”‚                                                                                             β”‚
β”‚          β–Ό  Low confidence? ──────────────────────────────────┐                                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚                                        β”‚
β”‚  β”‚  Validation Queue   │◄──────────────────────┐               β”‚                                        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   (Durable Orchestrator waits)        β”‚                                        β”‚
β”‚          β”‚                                                    β”‚                                        β”‚
β”‚          β–Ό                                                    β”‚                                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚                                        β”‚
β”‚  β”‚ Durable Orchestrator (InvoiceLifecycleOrch)          β”‚β”€β”€β”€β”€β”€β”˜                                        β”‚
β”‚  β”‚  β€’ Fan-out classification                           β”‚                                                β”‚
β”‚  β”‚  β€’ Await human validation (external event)          β”‚                                                β”‚
β”‚  β”‚  β€’ Fan-in approvals                                 β”‚                                                β”‚
β”‚  β”‚  β€’ Timeout / escalation                             β”‚                                                β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                               β”‚
β”‚          β”‚                                                                                             β”‚
β”‚          β–Ό                                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  (3) Service Bus Topics                                                      β”‚
β”‚  β”‚  Routing Topic      │──Subscriptions: Finance / Compliance / Analytics                              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  Dead-letter handling / retry                                                 β”‚
β”‚     β”‚           β”‚             β”‚                                                                         β”‚
β”‚     β–Ό           β–Ό             β–Ό                                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”                                                                   β”‚
β”‚  β”‚Finance β”‚  β”‚Complianceβ”‚ β”‚Analyticsβ”‚  (Functions / Logic Apps)                                        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                                   β”‚
β”‚     β”‚                           β”‚                                                                      β”‚
β”‚     β–Ό                           β–Ό                                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                     β”‚
β”‚  β”‚ SharePoint Library  β”‚   β”‚ Dataverse (Vendor)  β”‚                                                     β”‚
β”‚  β”‚  β€’ PDF + JSON       β”‚   β”‚  β€’ Supplier Master  β”‚                                                     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                     β”‚
β”‚     β”‚                           β”‚                                                                      β”‚
β”‚     β–Ό                           β–Ό                                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                     β”‚
β”‚  β”‚ Power Automate Flow β”‚   β”‚ Power BI Dataset     β”‚ (Streaming / Scheduled refresh)                    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                     β”‚
β”‚     β”‚                                                                                                  β”‚
β”‚     β–Ό                                                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                β”‚
β”‚  β”‚ Observability: App Insights, Log Analytics, Dashboard β”‚                                              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                β”‚
β”‚                                                                                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Full Azure Functions Implementation (C# .NET 8 Isolated)

InvoiceIngestionFunction.cs (HTTP trigger – receives file upload or URL reference)

using System.Net;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

public class InvoiceIngestionFunction
{
  private readonly BlobContainerClient _rawContainer;
  private readonly ILogger _logger;

  public InvoiceIngestionFunction(BlobServiceClient blobServiceClient, ILoggerFactory loggerFactory)
  {
    _rawContainer = blobServiceClient.GetBlobContainerClient("invoices-raw");
    _rawContainer.CreateIfNotExists();
    _logger = loggerFactory.CreateLogger<InvoiceIngestionFunction>();
  }

  [Function("InvoiceIngestion")]
  public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
  {
    var form = await req.ReadFormAsync();
    var file = form.Files.GetFile("file");
    if (file is null)
    {
      var bad = req.CreateResponse(HttpStatusCode.BadRequest);
      await bad.WriteStringAsync("File not provided.");
      return bad;
    }

    var blobName = $"{DateTime.UtcNow:yyyyMMddHHmmss}-{file.FileName}";
    var blobClient = _rawContainer.GetBlobClient(blobName);
    using (var stream = file.OpenReadStream())
    {
      await blobClient.UploadAsync(stream, overwrite: true);
    }
    _logger.LogInformation("Stored raw invoice {BlobName}", blobName);

    // Enqueue message to processing queue
    return await CreateAccepted(req, new { blobName });
  }

  private async Task<HttpResponseData> CreateAccepted(HttpRequestData req, object payload)
  {
    var response = req.CreateResponse(HttpStatusCode.Accepted);
    await response.WriteAsJsonAsync(payload);
    return response;
  }
}

InvoiceAnalyzeQueueFunction.cs (Queue trigger calling Document Intelligence)

using Azure.AI.FormRecognizer.DocumentAnalysis;
using Azure.Identity;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;

public class InvoiceAnalyzeQueueFunction
{
  private readonly BlobContainerClient _raw;
  private readonly BlobContainerClient _extracted;
  private readonly DocumentAnalysisClient _client;
  private readonly ILogger _logger;

  public InvoiceAnalyzeQueueFunction(BlobServiceClient blobServiceClient, ILoggerFactory lf)
  {
    _raw = blobServiceClient.GetBlobContainerClient("invoices-raw");
    _extracted = blobServiceClient.GetBlobContainerClient("invoices-extracted");
    _extracted.CreateIfNotExists();
    _client = new DocumentAnalysisClient(new Uri(Environment.GetEnvironmentVariable("FORMRECOGNIZER_ENDPOINT")!), new DefaultAzureCredential());
    _logger = lf.CreateLogger<InvoiceAnalyzeQueueFunction>();
  }

  [Function("InvoiceAnalyzeQueue")]
  public async Task Run([QueueTrigger("invoice-process", Connection = "STORAGE_CONN")] string message)
  {
    var meta = JsonSerializer.Deserialize<InvoiceMessage>(message)!;
    var blobClient = _raw.GetBlobClient(meta.BlobName);
    using var stream = await blobClient.OpenReadAsync();
    var operation = await _client.AnalyzeDocumentAsync(WaitUntil.Completed, "prebuilt-invoice", stream);
    var doc = operation.Value.Documents.First();
    var extracted = new InvoiceExtracted
    {
      InvoiceId = doc.Fields["InvoiceId"].Content,
      VendorName = doc.Fields["VendorName"].Content,
      InvoiceDate = doc.Fields["InvoiceDate"].Content,
      DueDate = doc.Fields.GetValueOrDefault("DueDate")?.Content,
      Subtotal = doc.Fields["SubTotal"].Value.AsFloat(),
      Total = doc.Fields["Total"].Value.AsFloat(),
      Tax = doc.Fields.GetValueOrDefault("Tax")?.Value.AsFloat(),
      Currency = doc.Fields.GetValueOrDefault("Currency")?.Content,
      BlobName = meta.BlobName,
      Confidence = doc.Fields["InvoiceId"].Confidence
    };

    var extractedBlob = _extracted.GetBlobClient(Path.ChangeExtension(meta.BlobName, ".json"));
    await extractedBlob.UploadAsync(new BinaryData(JsonSerializer.Serialize(extracted)), overwrite: true);
    _logger.LogInformation("Extracted invoice {InvoiceId} total {Total}", extracted.InvoiceId, extracted.Total);

    // Publish to Service Bus for routing
  }

  private record InvoiceMessage(string BlobName);
  private record InvoiceExtracted(string InvoiceId, string VendorName, string InvoiceDate, string? DueDate, double Subtotal, double Total, double? Tax, string? Currency, string BlobName, double Confidence);
}

InvoiceRoutingTopicFunction.cs (Service Bus Topic trigger – routes to downstream processors)

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class InvoiceRoutingTopicFunction
{
  private readonly ILogger _logger;
  public InvoiceRoutingTopicFunction(ILoggerFactory lf) => _logger = lf.CreateLogger<InvoiceRoutingTopicFunction>();

  [Function("InvoiceRoutingTopic")]
  public void Run([ServiceBusTrigger(topicName: "invoices", subscriptionName: "finance", Connection = "SB_CONN")] string body)
  {
    _logger.LogInformation("Finance subscription received invoice {Body}", body);
    // Finance enrichment logic (e.g., cost center mapping)
  }
}

Durable Orchestrator Pattern (Human Validation Flow)

[Function("InvoiceLifecycleOrchestrator")]
public static async Task RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{
  var input = context.GetInput<string>(); // invoice id
  var extracted = await context.CallActivityAsync<InvoiceExtracted>("LoadExtracted", input);

  if (extracted.Confidence < 0.85)
  {
    // Send validation request (Teams card / SharePoint list item) and wait external event
    await context.CallActivityAsync("SendValidationRequest", extracted.InvoiceId);
    extracted = await context.WaitForExternalEvent<InvoiceExtracted>("InvoiceValidated", TimeSpan.FromHours(24));
  }

  await context.CallActivityAsync("PersistToSharePoint", extracted);
  await context.CallActivityAsync("UpdateAnalyticsStore", extracted);
}

Infrastructure as Code (Bicep Core Excerpt)

param location string = resourceGroup().location
param enableAnalytics bool = true

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'invwf${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: { allowBlobPublicAccess: false }
}

resource serviceBus 'Microsoft.ServiceBus/namespaces@2023-01-01' = {
  name: 'sb-invoicewf-${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard' tier: 'Standard' }
}

resource topic 'Microsoft.ServiceBus/namespaces/topics@2023-01-01' = {
  name: '${serviceBus.name}/invoices'
  properties: { }
}

resource financeSub 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2023-01-01' = {
  name: '${serviceBus.name}/invoices/finance'
}

Power BI Dataset Automation (REST + PowerShell)

$tenantId = "<tenant>"
$clientId = "<appId>"
$clientSecret = "<secret>"
$resource = "https://analysis.windows.net/powerbi/api"
$token = (Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body @{client_id=$clientId; scope="$resource/.default"; client_secret=$clientSecret; grant_type="client_credentials"}).access_token

$headers = @{Authorization = "Bearer $token"}
Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups" -Headers $headers

Security Hardening

  • Use Managed Identity for Functions (no secrets in settings)
  • Key Vault references in Function App configuration (@Microsoft.KeyVault(SecretUri=https://...))
  • Service Bus RBAC: assign Azure Service Bus Data Sender & Receiver roles instead of SAS keys
  • Enforce private endpoints for Storage and Key Vault
  • Enable Defender for Cloud recommendations (vulnerability scanning)

Observability & Alerting Enhancements

- task: AzureCLI@2
  displayName: Configure Log Analytics Query Alert
  inputs:
  scriptType: bash
  scriptLocation: inlineScript
  inlineScript: |
    az monitor scheduled-query create \
    --name HighInvoiceFailures \
    --resource-group rg-workflow-prod \
    --scopes "/subscriptions/xxx/resourceGroups/rg-workflow-prod/providers/Microsoft.OperationalInsights/workspaces/logwfinv" \
    --condition "count > 5" \
    --description "Invoice failures exceeded threshold"

Web Picture References (Add real images under images/workflow/)

  • Architecture Overview: ![Architecture Overview](images/workflow/architecture.png)
  • Durable Orchestrator Flow: ![Durable Flow](images/workflow/durable-flow.png)
  • Document Intelligence Result: ![Extraction Fields](images/workflow/extraction-sample.png)
  • Power Automate Approval Card: ![Approval Card](images/workflow/approval-card.png)
  • Power BI Dashboard: ![Analytics Dashboard](images/workflow/powerbi-dashboard.png)
  • Service Bus Metrics: ![Service Bus Metrics](images/workflow/servicebus-metrics.png)

Extended Troubleshooting Table

Symptom Possible Cause Resolution Preventative Measure
Durable instance stuck External event never raised Raise manual event via Functions CLI Add escalation timer activity
Low extraction confidence Poor scan quality Re-run with enhanced image preprocessing Implement image cleaning (deskew, contrast)
Service Bus dead-letter growth Poison messages not handled Add dead-letter processing Function Implement retry with jitter & circuit breaker
Approval delays Power Automate throttling Optimize flow triggers / reduce concurrency Split flow into smaller child flows
Blob 429 errors High parallel uploads Enable TransferOptions concurrency control Shard ingestion across containers
BI dataset refresh failure Token expired / permissions Renew service principal / verify workspace access Implement refresh monitoring webhook

KPI & Metrics Dashboard Targets

Metric Target Rationale
Avg Extraction Confidence > 90% Ensures minimal human validation
Invoice Processing Time < 2 min Maintains operational SLA
Approval Cycle Time < 1 business day Speeds financial close
Failure Rate < 1% Reliability objective
Cost per Invoice (Compute) < $0.05 Optimize serverless spend
Human Touch Ratio < 15% Automation maturity

Future Enhancements Roadmap

  1. Multi-language invoice models (French, Spanish).
  2. Adaptive Cards in Teams for direct field correction.
  3. Vector search in vendor knowledge base (Azure AI Search).
  4. Data lineage tracking (Purview integration).
  5. Real-time anomaly detection (Azure Stream Analytics).
  6. Integration with Dynamics 365 Finance (Dataverse sync).
  7. Policy-as-code for governance (Azure Policy custom definitions).
  8. Automated load tests before month-end peak.
  9. Sustainability metrics (carbon intensity per 1000 invoices).
  10. Cost anomaly alerts (FinOps tagging + budgets API).

Final Best Practice Additions

  • Treat extraction JSON as immutable event; append-only storage.
  • Prefer Durable Functions external events over polling for human validation.
  • Centralize retry policies (exponential backoff + max attempts) in a helper.
  • Encrypt sensitive invoice fields at rest (Key Vault + Storage encryption).
  • Tag all resources with Environment, CostCenter, DataClass.
  • Use infrastructure drift detection (az deployment group what-if) nightly.
  • Maintain a schema contract for extracted invoices (JSON Schema with validation step).