Back to Writing

How to Download Virtually Any SSRS Report from D365 Finance & Operations

Have you ever found yourself manually clicking through D365 Finance & Operations to download dozens of invoices, confirmations, or purchase orders? What if I told you there's a hidden gem that can automate this entire process using simple OData calls?

I discovered this capability quite by accident while building my D365 F&O MCP Server and playing around with the search data entities feature. What started as exploration turned into uncovering one of the most powerful document automation features I've seen in D365 F&O.

Contents

The Accidental Discovery

While testing my MCP Server's search functionality, I was exploring the SrsFinanceCopilots data entity when I stumbled upon an action called RunCopilotReport. The name intrigued me – what exactly was this "Copilot" doing in the SRS (SQL Server Reporting Services) context?

After some experimentation, I realized this action was the key to programmatically downloading virtually any SSRS report from D365 F&O. It felt like finding a secret passage in a familiar building – the functionality was always there, just waiting to be discovered.

Let me walk you through exactly how this works, starting with a real conversation I had with my MCP Server:

Me: "free text invoice for FTI-00000014 in USMF"

Copilot: "I'll help you download the free text invoice FTI-00000014 for legal entity USMF..."

Within seconds, I had the PDF document downloaded programmatically. No clicking through menus, no manual report generation – just pure automation.

Understanding the RunCopilotReport Action

The RunCopilotReport action is a powerful OData endpoint that acts as a universal gateway to D365 F&O's SSRS reporting engine. Here's what makes it special:

{
	"actionName": "RunCopilotReport",
	"bindingKind": "BoundToEntitySet",
	"entityName": "SrsFinanceCopilots",
	"parameters": {
		"_contractName": "SrsCopilotArgsContract",
		"_controllerArgsJson": "{\"DataTableName\":\"CustConfirmJour\",\"DataTableFieldName\":\"SalesId\",\"DataTableFieldValue\":\"000725\"}",
		"_controllerName": "SalesConfirmController",
		"_legalEntityName": "USMF",
		"_reportParameterJson": "{}"
	}
}

Key Components:

  • Controller Name: Determines which report type to generate
  • Controller Args: Specifies the data table, field, and value to filter on
  • Legal Entity: The company context for the report
  • Contract Name: Always SrsCopilotArgsContract

Supported Document Types

Through my testing, I've mapped out the major document types you can download:

Document Type Controller Data Table Field Name Example Value
Sales Invoice SalesInvoiceController CustInvoiceJour InvoiceId FTI-000001
Free Text Invoice FreeTextInvoiceController CustInvoiceJour InvoiceId FTI-000002
Sales Confirmation SalesConfirmController CustConfirmJour ConfirmId or SalesId 000725
Purchase Order PurchPurchaseOrderController VendPurchOrderJour PurchId PO-000001
Credit Note CustDebitCreditNoteController CustInvoiceJour InvoiceId CN-000001

The beauty is that each controller handles the specific business logic for that document type, while the action provides a consistent interface.

Real-World Example: Free Text Invoice Download

Let me show you the exact sequence that worked in my conversation with the MCP Server:

Me: "free text invoice for FTI-00000014 in USMF"

Copilot: "I'll help you download the free text invoice FTI-00000014 for legal entity USMF..."

Here's the exact OData action call that was executed:

{
	"actionName": "RunCopilotReport",
	"bindingKind": "BoundToEntitySet",
	"entityName": "SrsFinanceCopilots",
	"parameters": {
		"_contractName": "SrsCopilotArgsContract",
		"_controllerArgsJson": "{\"DataTableName\":\"CustInvoiceJour\",\"DataTableFieldName\":\"InvoiceId\",\"DataTableFieldValue\":\"FTI-00000014\"}",
		"_controllerName": "FreeTextInvoiceController",
		"_legalEntityName": "USMF",
		"_reportParameterJson": "{}"
	}
}

Result: Perfect! The action completed successfully in just 17.8 seconds and returned a base64-encoded PDF of the free text invoice.

Key Success Factors

  1. Correct Controller: Used FreeTextInvoiceController for free text invoices
  2. Proper Data Table: Referenced CustInvoiceJour table where invoice data is stored
  3. Right Field Name: Used InvoiceId to match the invoice identifier
  4. Valid Document: The invoice FTI-00000014 existed in the USMF legal entity

This demonstrates the power and simplicity of the RunCopilotReport action – a single API call that handles all the complexity of SSRS report generation behind the scenes.

PowerShell Automation Script

Based on my discovery, I created a complete PowerShell automation script. Here's the core function that makes the magic happen:

function Invoke-D365FOAction {
    param(
        [string]$Environment,
        [string]$LegalEntity,
        [string]$InvoiceId
    )

    # Build the controller args JSON
    $controllerArgs = @{
        DataTableName = "CustInvoiceJour"
        DataTableFieldName = "InvoiceId"
        DataTableFieldValue = $InvoiceId
    } | ConvertTo-Json -Compress

    # Build the action parameters
    $actionPayload = @{
        "_contractName" = "SrsCopilotArgsContract"
        "_controllerArgsJson" = $controllerArgs
        "_controllerName" = "SalesInvoiceController"
        "_legalEntityName" = $LegalEntity
        "_reportParameterJson" = "{}"
    } | ConvertTo-Json -Compress

    # Construct the OData action URL
    $actionUrl = "$Environment/data/SrsFinanceCopilots/Microsoft.Dynamics.DataEntities.RunCopilotReport"

    # Make the REST call using az rest
    $response = az rest --method POST `
                        --url $actionUrl `
                        --resource $Environment `
                        --body $actionPayload `
                        --headers "Content-Type=application/json"

    return $response | ConvertFrom-Json
}

Base64 to PDF Conversion

The action returns the PDF as base64-encoded data. Here's how to convert it:

function Convert-Base64ToPdf {
    param(
        [string]$Base64Data,
        [string]$InvoiceId,
        [string]$OutputPath
    )

    # Generate safe filename
    $safeInvoiceId = Get-SafeFileName -fileName $InvoiceId
    $fileName = "$safeInvoiceId.pdf"
    $fullPath = Join-Path -Path $OutputPath -ChildPath $fileName

    # Convert Base64 to bytes and save as PDF
    $pdfBytes = [System.Convert]::FromBase64String($Base64Data)
    [System.IO.File]::WriteAllBytes($fullPath, $pdfBytes)

    return $fullPath
}

Complete Usage Example

# Download a single invoice
.\Download-InvoicePdf.ps1 -LegalEntity "USMF" -InvoiceId "FTI-000001"

# Batch download multiple invoices
$invoices = @("FTI-000001", "FTI-000002", "FTI-000003")
foreach ($invoice in $invoices) {
    .\Download-InvoicePdf.ps1 -LegalEntity "USMF" -InvoiceId $invoice -SkipAuthentication
}

Building Your Own Solution

Prerequisites

  1. Azure CLI installed and configured
  2. Authentication with your D365 F&O environment
  3. Appropriate permissions to access the documents you want to download

Authentication Setup

# Authenticate with your D365 F&O environment
az login --scope https://your-d365fo-env.dynamics.com/.default --allow-no-subscriptions

Basic Implementation Steps

  1. Identify Document Type: Determine which controller to use
  2. Build Parameters: Construct the controller arguments JSON
  3. Make OData Call: Execute the RunCopilotReport action
  4. Process Response: Extract and convert the base64 PDF data
  5. Save File: Write the PDF to disk

Error Handling Best Practices

try {
    $response = Invoke-D365FOAction -Environment $env -LegalEntity $le -InvoiceId $id

    if ($response.value) {
        Write-Host "Success: PDF data received" -ForegroundColor Green
        # Convert and save PDF
    } else {
        Write-Host "Warning: No PDF data in response" -ForegroundColor Yellow
        # Log response for debugging
    }
}
catch {
    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
    # Handle specific error cases
}

Error Handling and Troubleshooting

Common Issues and Solutions

1. Authentication Errors

# Solution: Re-authenticate with proper scope
az login --scope https://your-d365fo-env/.default --allow-no-subscriptions

2. Document Not Found

  • Verify the document ID exists in the specified legal entity
  • For sales confirmations, try both ConfirmId and SalesId
  • Check that the document has been posted/confirmed

3. Permission Denied

  • Ensure your user has access to the specific controller
  • Verify access to the underlying data tables
  • Check D365 F&O security roles and duties

4. Empty Response

  • Review the response JSON file for debugging information
  • Verify the controller arguments are correctly formatted
  • Check that the legal entity code is valid

Debug Mode

Always save the raw JSON response for troubleshooting:

$Response | ConvertTo-Json -Depth 10 | Out-File -FilePath "$InvoiceId-debug.json"

Advanced Use Cases

Bulk Document Processing

# Process all invoices for a date range
$invoices = Get-D365FOInvoices -LegalEntity "USMF" -FromDate "2025-01-01" -ToDate "2025-09-18"
foreach ($invoice in $invoices) {
    try {
        .\Download-InvoicePdf.ps1 -LegalEntity "USMF" -InvoiceId $invoice.InvoiceId -SkipAuthentication
        Write-Host "Downloaded: $($invoice.InvoiceId)" -ForegroundColor Green
    }
    catch {
        Write-Host "Failed: $($invoice.InvoiceId) - $($_.Exception.Message)" -ForegroundColor Red
    }
}

Integration with Power Automate

You can expose this functionality through Power Automate by creating a custom connector that calls your PowerShell script or by implementing the logic directly in Power Automate using HTTP actions.

Custom Document Templates

The _reportParameterJson parameter allows you to pass custom parameters to modify report generation:

{
	"_reportParameterJson": "{\"PrintManagementSettings\":{\"UseCustomTemplate\":true,\"TemplateId\":\"CustomInvoice\"}}"
}

Conclusion

Discovering the RunCopilotReport action opened up a world of automation possibilities for D365 Finance & Operations. What seemed like an insurmountable manual task – downloading hundreds of documents – became a simple scripting exercise.

The key insights from this discovery:

  1. Hidden Power: D365 F&O contains powerful APIs that aren't always well-documented
  2. Consistent Interface: The RunCopilotReport action provides a uniform way to access different document types
  3. Flexibility: Support for multiple field types (like ConfirmId vs SalesId) makes the solution robust
  4. Automation Ready: Perfect for bulk operations and integration scenarios

Whether you're dealing with month-end invoice distributions, audit document collection, or customer service requests, this approach can save hours of manual work and reduce human error.

This discovery is part of a broader exploration into D365 Finance & Operations automation and AI integration. If you found this useful, you might also be interested in:

Demo Repository & Resources

🔗 D365 F&O Client Demo Repository - Complete PowerShell scripts, authentication guides, and working examples for:

  • Invoice PDF downloads (featured in this post)
  • Sales confirmation automation
  • Purchase order processing
  • Authentication setup with Azure CLI
  • Error handling and troubleshooting guides

The repository includes everything you need to get started, from basic authentication setup to advanced bulk processing scenarios.

Conclusion

The beauty of this discovery is that it demonstrates how exploration and curiosity can uncover transformative capabilities hiding in plain sight. Sometimes the most powerful features are the ones waiting to be discovered.

Whether you're dealing with month-end invoice distributions, audit document collection, or customer service requests, this approach can save hours of manual work and reduce human error.

Have you found any hidden gems in your D365 F&O environment? I'd love to hear about your discoveries and how you're using automation to solve real business problems.

Share this article