---
title: "Power Automate: Build a Reliable AI Document Intake Pipeline"
description: "Build a Power Automate intake pipeline that deduplicates Outlook attachments, extracts fields with AI Builder, and routes low-confidence results to Teams."
canonical: "https://adamtheautomator.com/power-automate-document-intake-pipeline/"
---

# Power Automate: Build a Reliable AI Document Intake Pipeline

> Build a Power Automate intake pipeline that deduplicates Outlook attachments, extracts fields with AI Builder, and routes low-confidence results to Teams.

Source: https://adamtheautomator.com/power-automate-document-intake-pipeline/

---

ATA Learning

Tap to hide

[

ATA Learning

](/)

*   [Home](/)
*   [Tutorials](/tutorials/)
*   [Instructors](/author/)
*   [Advertising](/advertising/)
*   [Recommended Resources](/resources/)
*   [About Adam](/about-adam/)

Search for:  

*   [](https://twitter.com/adbertram)
*   [](https://github.com/Adam-the-Automator)
*   [](https://www.linkedin.com/company/adam-the-automator-llc)
*   [](/feed/)

![Power Automate: Build a Reliable AI Document Intake Pipeline](https://adamtheautomator.com/wp-content/uploads/2026/07/featured_image-3.webp)

# Power Automate: Build a Reliable AI Document Intake Pipeline

[![](https://secure.gravatar.com/avatar/d0b9d42e21e5622713f8b693aa5c0f9244d5f7dd200ed29b8398f52dee5de337?s=192&d=mm&r=g)Adam Bertram](https://adamtheautomator.com/author/adam-bertram/)28 July 202615 min. read

Categories: [Cloud](/category/cloud/)

Tags:[Artificial Intelligence](/tag/artificial-intelligence/)[Automation](/tag/automation/)[Power Automate](/tag/power-automate/)[SharePoint](/tag/sharepoint/)

Table of Contents

*   [Prerequisites and Pipeline Map](#prerequisites-and-pipeline-map)
*   [Keep Storage Separate From Reasoning](#keep-storage-separate-from-reasoning)
*   [Step 1: Capture and Normalize Outlook Attachments](#step-1-capture-and-normalize-outlook-attachments)
*   [Reject Noise Before It Reaches AI](#reject-noise-before-it-reaches-ai)
*   [Step 2: Create a Replay-Safe SharePoint Intake Record](#step-2-create-a-replay-safe-sharepoint-intake-record)
*   [Build a Bounded Intake Key](#build-a-bounded-intake-key)
*   [Reserve Before Creating the File](#reserve-before-creating-the-file)
*   [Step 3: Extract a Versioned Field Contract With AI Builder](#step-3-extract-a-versioned-field-contract-with-ai-builder)
*   [Step 4: Calibrate the Confidence Gate](#step-4-calibrate-the-confidence-gate)
*   [Compare Pass and Review Errors](#compare-pass-and-review-errors)
*   [Step 5: Use Copilot Studio for Ambiguity, Not Control](#step-5-use-copilot-studio-for-ambiguity-not-control)
*   [Step 6: Dispatch Human Review Through Teams](#step-6-dispatch-human-review-through-teams)
*   [Keep Review Runs Independent](#keep-review-runs-independent)
*   [Step 7: Add Retries, Exceptions, and Teams Alerts](#step-7-add-retries-exceptions-and-teams-alerts)
*   [Log Before You Alert](#log-before-you-alert)
*   [Step 8: Test Every Branch Before Production](#step-8-test-every-branch-before-production)
*   [Run the Pipeline as a Business Process](#run-the-pipeline-as-a-business-process)

Open the mailbox where invoices, applications, claims, or service requests arrive. Pick one attachment and account for every decision between that email and a trusted business record. If you cannot point to the duplicate check, the extraction rule, and the person who owns an exception, you do not have an intake pipeline yet. You have an unattended inbox with extra steps.

Build the pipeline with [Microsoft Power Automate](https://learn.microsoft.com/en-us/power-automate/getting-started), the [Office 365 Outlook connector](https://learn.microsoft.com/en-us/connectors/office365/), [SharePoint](https://learn.microsoft.com/en-us/connectors/sharepointonline/), [AI Builder](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-in-flow), [Microsoft Copilot Studio](https://learn.microsoft.com/en-us/connectors/microsoftcopilotstudio/), and [Microsoft Teams](https://learn.microsoft.com/en-us/connectors/teams/). Capture and deduplicate each file before extracting data. Then calibrate confidence gates, separate automated and human decisions, and prove that every retry reuses the original intake record.

## Prerequisites and Pipeline Map

If you want to follow along, you will need:

*   A [Power Platform environment](https://learn.microsoft.com/en-us/power-platform/admin/environments-overview) where you can create a solution-aware cloud flow, because the solution will hold connection references and environment-specific settings.
    
*   An Outlook mailbox for document intake; use a shared mailbox when the process belongs to a department rather than one employee.
    
*   A SharePoint document library where you can add metadata columns, a separate intake-registry list, and a separate exception list.
    
*   An internal [HTTP-triggered Azure Function](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger) that the solution can call to build a bounded SHA-256 intake key.
    
*   A [trained and published AI Builder document processing model](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-overview) with the fields your process requires.
    
*   A published Copilot Studio agent only if your documents contain classification or completeness questions that deterministic rules cannot answer.
    
*   A Teams chat or channel whose reviewers own low-confidence and failed submissions.
    
*   A labeled document set with approved field values, because your confidence threshold must come from your documents rather than somebody else’s favorite number.
    

### Keep Storage Separate From Reasoning

Build the cloud flow inside a [Power Platform solution](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/understand-benefits-solution-aware-flows). Solution-aware flows use connection references, which prevents the design from being tied to one maker’s Outlook or SharePoint connection when you move it into test or production.

The finished control path looks like the diagram below. SharePoint holds both the original evidence and the current processing state; AI components never become the system of record.

![Outlook, Power Automate, SharePoint, AI Builder, Copilot Studio, and Teams connected in a document-intake control path](https://adamtheautomator.com/wp-content/uploads/2026/07/document-intake-architecture-5.svg)

_Tracing the intake path_

The pipeline will only be as reliable as the business rules around it. Stop automation at any decision you cannot validate deterministically, and assign an accountable reviewer to own that exception.

## Step 1: Capture and Normalize Outlook Attachments

Create an automated flow with [**When a new email arrives (V3)**](https://learn.microsoft.com/en-us/connectors/office365/) for a process mailbox or **When a new email arrives in a shared mailbox (V2)** for a department. The [shared-mailbox connection account needs mailbox access](https://learn.microsoft.com/en-us/connectors/office365/). Set **Only with Attachments** to **Yes**; add folder or subject filters only for existing business rules.

In the panel below, change **Only with Attachments** to **Yes** and **Include Attachments** to **No** before adding per-file retrieval.

![Power Automate designer with the When a new email arrives V3 trigger selected; its Parameters panel shows Include Attachments, Importance, Only with Attachments, and Folder](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-outlook-trigger-4.jpg)

_Trigger fields to change_

Microsoft warns that trigger downloads can time out during attachment-heavy bursts. Keep **Include Attachments** set to **No**, loop through metadata, and call [**Get Attachment (V2)**](https://learn.microsoft.com/en-us/connectors/office365/) for each accepted file.

### Reject Noise Before It Reaches AI

Email signatures often arrive as inline attachments. Inside [**Apply to each**](https://learn.microsoft.com/en-us/power-automate/apply-to-each), add a [**Condition**](https://learn.microsoft.com/en-us/power-automate/add-condition) requiring **Attachments Is Inline** to equal `false`, as Microsoft’s [AI Builder guidance](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-in-flow#filter-email-signature-from-a-flow-so-that-its-not-processed-by-the-document-processing-model-microsoft-365-outlook) documents.

Apply the rest of the intake controls before saving the file:

*   Accept only the file types your published model supports. AI Builder document processing accepts PDF, JPEG, and PNG inputs.
    
*   Reject files over 20 MB before prediction, matching the [document processing model requirements](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-requirements).
    
*   Route password-protected PDFs to the exception list because AI Builder requires the password lock to be removed before processing.
    
*   Require a non-empty attachment array. Microsoft notes that [Defender for Office 365 Dynamic Delivery](https://learn.microsoft.com/en-us/connectors/office365/#known-issues-and-limitations-with-triggers) can cause the Outlook trigger to run twice, with the first run containing no attachments.
    

* * *

**_Warning: A filename extension is a routing hint, not proof of content. Keep your existing email-security and malware controls in front of this flow, and send a rejected file to review without opening it in an AI action._**

* * *

The output is a validated byte stream plus the connector Message Id, audit-only Internet Message Id, Attachment Id, source mailbox address, and approved extension. Nothing has been committed, so an unsafe file can still stop before storage.

## Step 2: Create a Replay-Safe SharePoint Intake Record

A filename cannot tell you whether Power Automate already saw an attachment. Vendors reuse `invoice.pdf`, and a resend can rename the same document.

Store the bounded key and its raw source identifiers with different jobs:

| Column | Purpose | Example Source |
| --- | --- | --- |
| `IntakeKey` | Identifies one attachment submission | 64-character lowercase SHA-256 digest |
| `OutlookMessageID` | Addresses the source message during worker retrieval | Outlook trigger **Message Id** |
| `InternetMessageID` | Preserves the RFC message identifier for audit only; never use it as `OutlookMessageID` | Outlook **Internet Message Id** |
| `AttachmentID` | Addresses the source attachment during worker retrieval | Outlook attachment **Id** |
| `OriginalMailboxAddress` | Points retrieval at the shared mailbox; blank only for the connection-owned mailbox | Configured intake mailbox |
| `ValidatedExtension` | Reuses the approved storage suffix without parsing the name again | `pdf`, `jpg`, or `png` |
| `OriginalFileName` | Preserves the sender’s attachment name | Outlook attachment name |
| `ConversationID` | Groups replies into one business case | Outlook Conversation ID |
| `SchemaVersion` | Records which extraction contract ran | Solution environment variable |
| `ProcessingStatus` | Shows the registry control state | `Reserved`, `FileCreateFailed`, or `FileCreated` |
| `FileIdentifier` | Binds the reservation to the created SharePoint file | SharePoint **Create file** output |
| `RecoveryRequested` | Authorizes a worker retry on the same reservation | SharePoint Yes/No column, default `No` |
| `ProcessingDispatched` | Prevents a second child-flow call | SharePoint Yes/No column, default `No` |
| `ReviewDispatched` | Prevents a second review card | SharePoint Yes/No column, default `No` |
| `ReviewField` | Names the field that needs a decision | Normalized field contract |
| `ReviewValue` | Preserves the extracted value | Normalized field contract |
| `ReviewConfidence` | Preserves the field score | AI Builder output |
| `FailedRule` | Explains why automation stopped | Validation result |

### Build a Bounded Intake Key

Create a solution-aware `BuildIntakeKey` child flow. Reject null, empty, and whitespace-only inputs, then UTF-8 encode accepted originals without trimming or normalization. Prefix each [byte-array length](https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.getbytes) with a [four-byte big-endian integer](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.binary.binaryprimitives.writeint32bigendian), ordering message before attachment.

```
using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;

static string BuildIntakeKey(string internetMessageId, string attachmentId)
{
    if (string.IsNullOrWhiteSpace(internetMessageId))
        throw new ArgumentException("InternetMessageID is required.");
    if (string.IsNullOrWhiteSpace(attachmentId))
        throw new ArgumentException("AttachmentID is required.");

    byte[] messageBytes = Encoding.UTF8.GetBytes(internetMessageId);
    byte[] attachmentBytes = Encoding.UTF8.GetBytes(attachmentId);
    byte[] payload = new byte[4 + messageBytes.Length + 4 + attachmentBytes.Length];
    int offset = 0;

    BinaryPrimitives.WriteInt32BigEndian(payload.AsSpan(offset, 4), messageBytes.Length);
    offset += 4;
    messageBytes.CopyTo(payload, offset);
    offset += messageBytes.Length;
    BinaryPrimitives.WriteInt32BigEndian(payload.AsSpan(offset, 4), attachmentBytes.Length);
    offset += 4;
    attachmentBytes.CopyTo(payload, offset);

    byte[] hashBytes = SHA256.HashData(payload);
    string intakeKey = Convert.ToHexString(hashBytes).ToLowerInvariant();

    if (hashBytes.Length != 32 || intakeKey.Length != 64)
        throw new InvalidOperationException("The intake key must be 64 lowercase hex characters.");

    foreach (char character in intakeKey)
    {
        bool isLowerHex = (character >= '0' && character <= '9') ||
                          (character >= 'a' && character <= 'f');
        if (!isLowerHex)
            throw new InvalidOperationException("The intake key must be 64 lowercase hex characters.");
    }

    return intakeKey;
}
```

[`SHA256.HashData`](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.sha256.hashdata) returns 32 bytes; [`Convert.ToHexString`](https://learn.microsoft.com/en-us/dotnet/api/system.convert.tohexstring) emits uppercase. Call [`ToLowerInvariant`](https://learn.microsoft.com/en-us/dotnet/api/system.string.tolowerinvariant) and return only 64 lowercase hex characters.

Call it as `Run_BuildIntakeKey` inside `For_each_attachment` and put `intakeKey` in [**Compose**](https://learn.microsoft.com/en-us/power-automate/data-operations#use-the-compose-action) `Compose_IntakeKey`. Send any non-64-character result to **Catch**. The digest fits SharePoint’s [255-character text limit](https://support.microsoft.com/en-US/SharePoint/lists/data-and-lists/list-and-library-column-types-and-options#single-line-of-text); store raw IDs separately.

### Reserve Before Creating the File

Create `DocumentIntakeRegistry` with a unique text key; SharePoint [rejects and indexes duplicates](https://support.microsoft.com/en-us/sharepoint/lists/data-and-lists/create-list-relationships-by-using-lookup-columns#unique-columns). After extension validation, `Reserve_intake` [**creates the item**](https://learn.microsoft.com/en-us/connectors/sharepointonline/#create-item) with `OutlookMessageID`, audit-only `InternetMessageID`, `AttachmentID`, `OriginalMailboxAddress`, `ValidatedExtension`, remaining audit fields, a blank `FileIdentifier`, `RecoveryRequested=No`, and `ProcessingStatus=Reserved`. A later duplicate becomes `IgnoredDuplicate`; exhaustion changes the same row to `FileCreateFailed`.

Give `RegistryFileWorker` trigger concurrency `1`. Every run starts with **Get item** by registry item ID. Continue only for live `Reserved` plus blank `FileIdentifier`, or `FileCreateFailed` plus blank identifier and `RecoveryRequested=Yes`. Otherwise stop before connector writes.

Call [**Get Attachment (V2)**](https://learn.microsoft.com/en-us/connectors/office365/) with `OutlookMessageID` as **Message Id**, `AttachmentID` as **Attachment Id**, and persisted `OriginalMailboxAddress`. Leave the mailbox value blank only when the connection account owns the source. Pass **Content Bytes** directly to SharePoint [**Create file**](https://learn.microsoft.com/en-us/connectors/sharepointonline/#create-file) as **File Content**.

Build `Compose_StorageFileName` from the persisted approved extension:

```
concat(outputs('Compose_IntakeKey'), '.', outputs('Compose_ValidatedExtension'))
```

Set **Overwrite: No**. Bind the returned ID and mark `FileCreated`. After an uncertain response, look up the digest filename and bind only when its name and stored `IntakeKey` match this row; otherwise retain `FileCreateFailed`. Never choose another name.

This path lasts only while Outlook can address the source. Microsoft documents not-found results after message movement or deletion; retention expiry creates the same gap. Failed retrieval leaves the row incomplete, writes a visible exception, and skips **Create file**. If recovery must survive those events, store bytes in approved staging before intake ends and persist its identifier. Do not stage for a shorter window already covered by mailbox policy.

Put Steps 3 through 5 in a solution-aware [child flow](https://learn.microsoft.com/en-us/power-automate/create-child-flows) named `ProcessDocument`. Give its manual trigger three text inputs: `IntakeKey`, `FileIdentifier`, and `SchemaVersion`. Create a processor-controller flow with SharePoint [**When a file is created or modified (properties only)**](https://learn.microsoft.com/en-us/connectors/sharepointonline/) and this [trigger condition](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/optimize-power-automate-triggers#set-trigger-conditions):

```
@and(equals(triggerOutputs()?['body/ProcessingStatus/Value'], 'Received'), equals(triggerOutputs()?['body/ProcessingDispatched'], false))
```

Set `ProcessingDispatched` to `Yes` before calling `ProcessDocument`. Each accepted attachment gets its own controller run, and the replay flow can call the same child flow without creating another file.

## Step 3: Extract a Versioned Field Contract With AI Builder

Add the [AI Builder](https://www.anrdoezrs.net/links/7627660/type/dlg/sid/ata-ai-builder-document-intake/https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fgetting-started-microsoft-power-platform-ai-builder) [**Process documents** action](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-in-flow) and select your published model, document type, and attachment content. The action returns each extracted field’s value and confidence score between 0 and 1. It can also return table cells, page numbers, coordinates, and bounding boxes for advanced review experiences.

Use **Pages** only when the document layout makes the range deterministic. Microsoft recommends the [page-range parameter](https://learn.microsoft.com/en-us/ai-builder/form-processing-model-in-flow#page-range) when one known form occupies specific pages, because processing fewer pages can reduce prediction cost and improve performance. Do not hard-code `1` for a packet whose required form moves between page 1 and page 4.

Normalize the dynamic outputs in a **Compose** action named `Compose_NormalizedFields` before applying validation rules. A sample result looks like this:

```json
[
  {
    "field": "InvoiceNumber",
    "value": "INV-1042",
    "confidence": 0.93,
    "sourcePage": 1,
    "rulePassed": true
  },
  {
    "field": "InvoiceTotal",
    "value": "1840.50",
    "confidence": 0.81,
    "sourcePage": 1,
    "rulePassed": true
  }
]
```

This array is your field contract, not a copy of AI Builder’s internal response. Give it a `SchemaVersion` and keep field names stable across flows, Adaptive Cards, reports, and downstream integrations.

| Field Rule | Why It Exists | Failure Route |
| --- | --- | --- |
| `InvoiceNumber` is required | Prevents an untraceable payable | Human review |
| `InvoiceTotal` parses as decimal | Stops a text value from reaching finance | Human review |
| `InvoiceDate` is a valid date | Prevents a malformed posting date | Human review |
| `VendorID` exists in the vendor list | Blocks an unknown payee from automatic processing | Business exception |

AI confidence does not replace these validation rules. A model can confidently extract `1804.50` when the approved invoice total is `1840.50`. Validate the extracted value independently of the confidence score.

## Step 4: Calibrate the Confidence Gate

Create a decimal environment variable named `DocumentIntake_MinConfidence` and reference it from the flow. [Power Automate environment variables](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/use-environment-variables) separate configuration from flow logic, so test and production can use validated values without editing every condition.

Values such as `0.80` and `0.85` are useful examples while you wire the branch. They are not production defaults. Choose the threshold by running your labeled document set through the published model and comparing extracted values with approved values field by field.

Add [**Initialize variable**](https://learn.microsoft.com/en-us/power-automate/create-variable-store-values), name it `MinConfidence`, choose **Float**, and select the `DocumentIntake_MinConfidence` environment-variable value. Then add [**Filter array**](https://learn.microsoft.com/en-us/power-automate/data-operations), set **From** to `outputs('Compose_NormalizedFields')`, and paste this advanced-mode condition:

```
@or(empty(coalesce(item()?['value'], '')), equals(item()?['rulePassed'], false), less(float(item()?['confidence']), variables('MinConfidence')))
```

At `0.85`, the sample array returns only `InvoiceTotal` because its confidence is `0.81`. Send `@equals(length(body('Filter_array')), 0)` to the automatic path; any returned object goes to review.

The routing logic should resemble the following diagram. Notice that a rule failure bypasses the automatic path even when every confidence score is high.

![Business rules and a calibrated confidence gate routing documents to automatic processing, Copilot Studio, or human review](https://adamtheautomator.com/wp-content/uploads/2026/07/confidence-routing-5.svg)

_Routing by confidence_

### Compare Pass and Review Errors

Track four outcomes during calibration:

| Outcome | What It Tells You |
| --- | --- |
| Correct automatic pass | The threshold allowed useful straight-through processing |
| Incorrect automatic pass | The threshold or business rules are too permissive |
| Correct review route | The gate protected the downstream record |
| Unnecessary review route | The threshold is creating avoidable manual work |

* * *

**_Reality Check: Raising the threshold reduces risky automatic passes and increases the review queue. Lowering it does the reverse. Your labeled documents decide where that tradeoff belongs._**

* * *

Repeat the evaluation for each required field and document layout. A single document-level average can hide one dangerously weak field, so route on the minimum required-field confidence or on field-specific thresholds when the business impact differs.

## Step 5: Use Copilot Studio for Ambiguity, Not Control

AI Builder should remain the first extraction path for a stable form. Invoke Copilot Studio when the file requires semantic judgment: classify an unfamiliar document, decide which required information is missing from a free-form letter, or explain why the content does not fit a known category.

Publish the agent, add the Microsoft Copilot Studio connector, and choose [**Execute Agent and wait**](https://learn.microsoft.com/en-us/connectors/microsoftcopilotstudio/). The action accepts a message and attachment parameters under its advanced inputs. Pass the normalized field array, the expected schema version, and either the supported document content or carefully normalized text.

Give the agent a narrow response contract. For example:

```json
{
  "documentType": "invoice",
  "requiresReview": true,
  "missingFields": ["PurchaseOrderNumber"],
  "reason": "No purchase order reference was found in the document."
}
```

Rename the agent action `Execute_Agent_and_wait`. Add [**Parse JSON**](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/use-data-operations) after it and select the action’s **lastResponse** dynamic value for **Content**. The connector documents `lastResponse` as the final text activity and `responses` as the array of returned text activities. Use `responses` only when your contract intentionally aggregates several activities; this design expects one JSON object, so parsing the array would change the contract. Paste this schema:

The selected action below shows where **Content** and **Schema** appear in the Power Automate designer. The live flow uses a smaller response contract; replace that example schema with the stricter document-intake contract that follows.

![Power Automate designer with Parse Agent Response selected; the Parameters panel highlights the Content token and JSON Schema editor](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-parse-agent-response-4.jpg)

_Parse JSON input and schema_

```json
{
  "type": "object",
  "properties": {
    "documentType": { "type": "string" },
    "requiresReview": { "type": "boolean" },
    "missingFields": { "type": "array", "items": { "type": "string" } },
    "reason": { "type": "string" }
  },
  "required": ["documentType", "requiresReview", "missingFields", "reason"],
  "additionalProperties": false
}
```

Initialize an Array variable named `SupportedDocumentTypes` with the categories your process accepts. Route this condition to `ReviewRequired`:

```
@or(not(contains(variables('SupportedDocumentTypes'), body('Parse_JSON')?['documentType'])), equals(body('Parse_JSON')?['requiresReview'], true), greater(length(body('Parse_JSON')?['missingFields']), 0))
```

Configure the same metadata update to run after `Parse_JSON` **has failed** or **has timed out**. A malformed response or missing required property now reaches review without giving the agent control over storage, deduplication, or an irreversible transaction.

That separation makes the failure path understandable. When a record lands in review, you can tell whether a rule failed, a score missed its threshold, or the agent returned an invalid classification.

## Step 6: Dispatch Human Review Through Teams

Do not make the intake flow sit on [**Post adaptive card and wait for a response**](https://learn.microsoft.com/en-us/connectors/teams/#post-adaptive-card-and-wait-for-a-response). That action pauses its flow run until somebody responds. Instead, let the intake flow save `ReviewRequired` metadata and finish. Create a second review-dispatcher flow that triggers on the SharePoint status change, waits for the reviewer, and writes the correction back.

### Keep Review Runs Independent

Use SharePoint [**When a file is created or modified (properties only)**](https://learn.microsoft.com/en-us/connectors/sharepointonline/) for the dispatcher. Add this [trigger condition under **Settings**](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/optimize-power-automate-triggers#set-trigger-conditions):

```
@and(equals(triggerOutputs()?['body/ProcessingStatus/Value'], 'ReviewRequired'), equals(triggerOutputs()?['body/ReviewDispatched'], false))
```

Make [**Update file properties**](https://learn.microsoft.com/en-us/connectors/sharepointonline/) set `ReviewDispatched` to `Yes` before posting the card. Then paste this [Adaptive Card JSON payload](https://adaptivecards.io/explorer/AdaptiveCard.html) into the Teams action and replace the displayed values only by changing the corresponding SharePoint column names:

```json
{
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.4",
  "body": [
    { "type": "TextBlock", "weight": "Bolder", "text": "Document review" },
    { "type": "TextBlock", "wrap": true, "text": "Field: @{triggerOutputs()?['body/ReviewField']}\nValue: @{triggerOutputs()?['body/ReviewValue']}\nConfidence: @{triggerOutputs()?['body/ReviewConfidence']}\nRule: @{triggerOutputs()?['body/FailedRule']}" },
    { "type": "Input.Text", "id": "correctedValue", "label": "Correction" }
  ],
  "actions": [
    { "type": "Action.OpenUrl", "title": "Open document", "url": "@{triggerOutputs()?['body/{Link}']}" },
    { "type": "Action.Submit", "title": "Approve", "data": { "decision": "Approve", "intakeKey": "@{triggerOutputs()?['body/IntakeKey']}" } },
    { "type": "Action.Submit", "title": "Correct", "data": { "decision": "Correct", "intakeKey": "@{triggerOutputs()?['body/IntakeKey']}" } },
    { "type": "Action.Submit", "title": "Reject", "data": { "decision": "Reject", "intakeKey": "@{triggerOutputs()?['body/IntakeKey']}" } }
  ]
}
```

The submitted `data` object must contain the original `IntakeKey` and one decision. Giving each review item its own dispatcher run prevents one unanswered card from holding unrelated documents behind it.

If you process multiple attachments inside one email run, **Apply to each** executes sequentially by default. Enable a modest concurrency value only after the bounded-key service and atomic registry reservation pass their race tests. Microsoft’s [parallel execution guidance](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/implement-parallel-execution) supports 1 to 50 parallel iterations, but a higher value adds connector pressure and does not guarantee a faster flow.

When the reviewer submits the card, update the normalized field values, record reviewer identity and review time, and set the status to `Corrected` or `Rejected`. Keep the original AI output intact in audit metadata so you can measure where the model is failing instead of training on a corrected value with no history.

## Step 7: Add Retries, Exceptions, and Teams Alerts

Put state-changing actions in a [**Try** scope](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/error-handling). Run **Catch** after failure, timeout, or skip, never success.

Use a connector-free harness to verify **Try** and **Catch**.

![Annotated Power Automate test harness scopes](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-demo-harness-annotated-4.png)

_Test harness scopes_

Expand **Try** to confirm **Duplicate Check** precedes **Confidence Gate**.

![Expanded Power Automate routing branches](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-demo-harness-expanded-4.png)

_Expanded test branches_

Confirm the example **AND** gate uses `0.85` overall and `0.90` for invoices.

![Power Automate confidence gate parameters](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-confidence-gate-4.png)

_Confidence gate conditions_

Verify the three selected **Catch** states and cleared success state.

![Power Automate Catch run-after settings](https://adamtheautomator.com/wp-content/uploads/2026/07/power-automate-catch-run-after-4.png)

_Catch run-after settings_

Inside **Catch**, filter [`result('Try')`](https://learn.microsoft.com/en-us/azure/logic-apps/expression-functions-reference#result) twice. For failed or timed-out actions, use:

```
@or(equals(item()?['status'], 'Failed'), equals(item()?['status'], 'TimedOut'))
```

For skipped actions, use:

```
@equals(item()?['status'], 'Skipped')
```

Iterate both arrays and log every result instead of `first(...)` with:

```
item()?['name']
item()?['status']
item()?['code']
item()?['outputs']
```

Keep complete outputs and never read root `error`. Map `outputs.body` only after observing the connector schema. Log skips separately. A named nested scope such as `Try_AI_Builder` needs its own `result()` call.

Use exponential retry only for transient connector failures. Password locks, unsupported types, invalid agent responses, and rule rejections need review, not another identical request.

### Log Before You Alert

Write one exception row per failed or timed-out result, and separate rows for skipped actions:

*   `IntakeKey`, flow name, run ID, action name, action status, action code, and the complete outputs object.
    
*   The SharePoint file identifier plus links to the file and Power Automate run.
    
*   Retry count, final status, and the person or queue responsible for resolution.
    
*   A `ReplayRequested` flag and `ResolutionStatus` choice that an operator can set after fixing the cause.
    

Post Teams only after committing the exception row. SharePoint remains the record if Teams is unavailable.

Create a replay-controller flow with SharePoint [**When an item is created or modified**](https://learn.microsoft.com/en-us/connectors/sharepointonline/) and this trigger condition:

```
@and(equals(triggerOutputs()?['body/ReplayRequested'], true), equals(triggerOutputs()?['body/ResolutionStatus/Value'], 'Open'))
```

Set `ResolutionStatus=ReplayQueued` and `ReplayRequested=No`, then inspect the registry. For `Reserved` or `FileCreateFailed` without an ID, set `RecoveryRequested=Yes` on that row and stop. The worker receives only its item ID, re-reads live state, retrieves with persisted connector fields, and sends **Content Bytes** to the digest filename. Only `FileCreated` with an ID can call `ProcessDocument`. An unavailable Outlook source retains the failed row and updates its exception.

* * *

**_Quick Win: Reuse the same_** `IntakeKey` **_during replay. A repaired run should update the failed record, never create a fresh file that hides the first attempt._**

* * *

After logging, [**Terminate**](https://learn.microsoft.com/en-us/power-automate/guidance/coding-guidelines/error-handling) with an explicit failure status so dashboards cannot report an unprocessed document as successful.

## Step 8: Test Every Branch Before Production

Define expected SharePoint state before tuning. The [cloud flow run view](https://learn.microsoft.com/en-us/power-automate/see-a-flow-run) proves each control point. A harness covers routing; bound nonproduction tests cover connectors and nested scopes.

| Test Document | Expected Result |
| --- | --- |
| Supported file with correct high-confidence fields | One file and one completed record |
| Correct fields below the example threshold | One review card and no automatic commit |
| High-confidence field that violates a business rule | Business exception despite the score |
| Same attachment delivered twice | One registry/file pair; the losing run logs `IgnoredDuplicate` |
| `Create file` fails after reservation and the email run ends | Recovery starts the worker with only the registry item ID; persisted Outlook fields produce one registry row and one digest-named file |
| Two distinct attachments both named `invoice.pdf` | Two digest-named files with `OriginalFileName` preserved and no overwrite |
| Password-locked or oversized file | Intake exception before AI processing |
| Malformed Copilot Studio `lastResponse` | `ReviewRequired` with the response preserved |

Run the labeled set at `0.80`, `0.85`, and the measured threshold; compare incorrect passes with unnecessary reviews.

Repeat an ID pair for the same lowercase 64-character key; change either ID for a different key. Test non-ASCII, surrounding whitespace, and concurrent duplicates. The last case must leave one registry/file pair.

Force `Create file` to fail after reservation and let the email run finish. Start recovery with only the registry item ID. Verify **Get item** supplies `OutlookMessageID`, `AttachmentID`, `OriginalMailboxAddress`, and `ValidatedExtension`; **Get Attachment (V2)** supplies the **Content Bytes** consumed by **Create file**. Before `ProcessDocument`, assert exactly one row for `IntakeKey` and one digest-named file. A moved, deleted, or expired source must create a visible exception unless approved staging is bound.

## Run the Pipeline as a Business Process

Move the solution through each environment with variables for SharePoint, review destination, schema version, and thresholds. [Export it](https://learn.microsoft.com/en-us/power-automate/export-flow-solution) with connection references, each mapped to a service-owned account.

Watch the pipeline with operational measures you can act on:

*   Automatic-pass, review, correction, rejection, and duplicate rates.
    
*   Field-level accuracy against reviewed documents.
    
*   Time from Outlook receipt to completed or rejected status.
    
*   Exceptions by action, document type, and sender.
    
*   AI Builder pages processed and Copilot Studio calls per accepted document.
    

A production run leaves one registry item, one digest-named file with the original name, a versioned extraction record, and a visible exception owner. The serialized worker recovers from persisted state while Outlook exposes the message, or from approved staging when policy requires longer. Raise volume only after labeled and replay tests preserve those counts.

Share this article

[Share on X](https://twitter.com/intent/tweet?url=https%3A%2F%2Fadamtheautomator.com%2Fpower-automate-document-intake-pipeline%2F&text=Power%20Automate%3A%20Build%20a%20Reliable%20AI%20Document%20Intake%20Pipeline)[Share on Facebook](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fadamtheautomator.com%2Fpower-automate-document-intake-pipeline%2F)[Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fadamtheautomator.com%2Fpower-automate-document-intake-pipeline%2F)

## Related Posts

![](https://adamtheautomator.com/wp-content/uploads/2026/04/featured_image-16.webp)

### [Microsoft 365 E7: Is the $99/User Price Tag Worth It?](/microsoft-365-e7-99user-price-tag-worth-it/)

Evaluate whether Microsoft 365 E7’s $99 price justifies Agent 365 governance using PowerShell scripts to assess Copilot usage and automation risk exposure.

![](https://adamtheautomator.com/wp-content/uploads/2021/10/How-to-Install-Terraform-on-Linux-and-Windows.jpg)

### [How to Install Terraform on Linux and Windows](/install-terraform/)

Terraform is the most popular automation tool to build, change and manage your cloud infrastructure in an effective and quick way. Jump in this article to learn how to install Terraform on your Linux and Windows devices!

![](https://adamtheautomator.com/wp-content/uploads/2026/06/featured_image_v2.png)

### [When AI Automates the Joy Out of Work](/ai-automation-work/)

A personal look at how AI agents and automation can boost productivity while draining the satisfaction that makes hands-on technical work feel meaningful.

## Categories

*   [IT Ops](/category/it-ops/)
*   [Cloud](/category/cloud/)
*   [DevOps](/category/devops/)
*   [Home Ops](/category/home-ops/)
*   [Information Security](/category/infosec/)
*   [Software Development](/category/software-development/)

## Site

*   [Home](/)
*   [Tutorials](/tutorials/)
*   [Instructors](/author/)
*   [Advertising](/advertising/)
*   [Recommended Resources](/resources/)
*   [About Adam](/about-adam/)

Copyright 2026© ATA Learning | [Privacy Policy](/privacy/)
