Stop Copilot Auto-Approvals with a Human-in-the-Loop Flow

Published:28 July 2026 - 8 min. read

Audit Active Directory for stale users, weak passwords, and other security risks with Specops Password Auditor.

Go ahead and let your Copilot workflow approve a refund on its own. Wait for the first request it was never taught to recognize, and watch a five-second automation issue money nobody authorized.

Now build the version that stops and asks. Wire Power Automate, Copilot Studio’s Request for Information action, and a Microsoft Teams approval into one flow that reads a messy business request and lets deterministic rules clear the cheap decisions. Send the judgment calls to a human, log every outcome, and escalate when nobody answers. The same five steps carry over to access requests and purchasing approvals.

Prerequisites

This tutorial includes hands-on steps in the Microsoft Power Platform. To follow along, make sure you have the following:

  • A Microsoft 365 tenant with access to Power Automate and Copilot Studio.

  • Permission to create cloud flows and agent flows in your environment.

  • The Microsoft Teams and Approvals apps available to your account.

  • A logging destination such as a SharePoint list or a Microsoft Dataverse table.

How the Human-in-the-Loop Workflow Fits Together

Before you build anything, look at how the two layers divide the work. A durable HITL design runs an AI layer and a deterministic layer with very different failure profiles.

The AI layer handles ambiguity. It reads messy, conversational input and figures out intent. The deterministic layer handles rules: exact matching, if/then branching, and transactional updates that must never improvise. Copilot Studio supplies the first layer, and Power Automate supplies the second.

Assign each decision to the layer you can hold to account:

Decision style Handled by Example
Rules-based, predictable Power Automate “Any request under $500 with a valid cost code is auto-approved.”
Ambiguous, context-driven Copilot / AI Builder “Read this email and figure out what the person is actually asking for.”
High-stakes judgment A human in Teams “This refund is $12,000 and the account is 90 days overdue. Should we do it?”

The workflow you are about to build walks through those three lanes in order, as you can see below.

HITL workflow lanes

Step 1 starts at the left edge of that diagram, where the request is still unstructured text.


Warning: Do not let a model confidence score bypass the human stage for money, access, legal, or personnel actions. The human branch exists to catch the request that looks routine until someone reads it.


Step 1: Interpret the Business Request with Copilot

Real business requests rarely arrive as clean, structured data. They show up as a rambling email or a Teams message. Trying to parse that with split() and substring() breaks the instant someone phrases things differently.

Force a Structured Output From the AI

Instead, let the AI do the reading. In Power Automate, add the Run a prompt action from AI Builder and force it to return structured JSON you can trust downstream. Microsoft renamed that action in May 2025, so older walkthroughs and screenshots still call it Create text with GPT using a prompt.

Parse this business request into JSON with these exact fields:
- RequestType (must be 'Refund', 'Access', or 'Purchase')
- Amount (number, 0 if not applicable)
- Justification (string)
- Urgency (must be 'Low', 'Medium', or 'High')
Return only raw JSON with no markdown formatting.

Input: {varRequestBody}

Because you demanded strict JSON, you can feed the response into a Parse JSON action with a schema that matches those four fields:

{
  "type": "object",
  "properties": {
    "RequestType": { "type": "string" },
    "Amount": { "type": ["number", "string"] },
    "Justification": { "type": "string" },
    "Urgency": { "type": "string" }
  }
}

That schema turns the model’s probabilistic output into typed variables like Amount, RequestType, and Urgency that the deterministic half of your flow can read. Amount accepts two types on purpose: models quote numbers often enough that a strict number schema rejects the payload outright, and catching a quoted value in the next step beats losing the whole run here.

One failure shows up more than any other. Models routinely wrap their answer in a markdown code fence, and Parse JSON then kills the run with InvalidJSON because the backticks are not valid JSON. The last line of the prompt above is the cheap fix. If a model still fences the output, strip the backticks with a Compose action between the prompt and the parse step.

Your configured prompt action should look like the one below.

Run a prompt action

Keep Prompts Narrow Enough to Debug

Keep your prompts small and single-purpose. If you need to extract, normalize, and classify, use three chained prompts rather than one giant prompt trying to do all three. When the AI goes sideways, a narrow prompt is much easier to inspect and repair than a giant one with five responsibilities, and it is also the unit you re-run when the routing in the next step misfires.

Step 2: Apply Deterministic Conditions

Now that the request is structured, most of it should never touch a human at all. Power Automate resolves those cases in milliseconds with a single Condition action.

Add a Condition action that encodes your business rules. For example:

  • If RequestType is Refund and Amount is less than or equal to 500, mark it auto-approved, update your system of record, and end the flow.

  • If Amount is greater than 500, or Urgency equals High, route it to a human in Step 3.

Switch the condition to advanced mode and both rules collapse into one expression:

@and(equals(body('Parse_JSON')?['RequestType'], 'Refund'), lessOrEquals(int(body('Parse_JSON')?['Amount']), 500))

The int() cast is not decoration. When the model quotes the number, Amount arrives as the string "450", and lessOrEquals refuses to compare a string against an integer. The action fails with InvalidTemplate and the message “The template language function ‘lessOrEquals’ expects two parameter of matching types.” Neither branch runs, so the refund stops dead in a failed run rather than reaching a reviewer or your system of record. The expression reference for conditions lists the rest of the operators and their type rules.

This gate protects the reviewer’s attention. The deterministic path returns the same answer for the same input, so cheap decisions resolve instantly and only genuine judgment calls survive into Step 3. Submit one $450 refund and confirm the Condition succeeds on the auto-approve branch without creating an approval, before you build the human branch on top of it. A failed Condition there means the cast is missing.

Step 3: Route Judgment Calls to Teams

When a request fails the auto-approve condition, your reviewer should be able to decide without opening another application.

Use Approval Cards for Binary Decisions

Power Automate’s Start and wait for an approval action is the fastest path for a clean yes/no decision. Set Approval type to Approve/Reject – First to respond, and assign it to the reviewer or to a manager looked up dynamically from the requester. Put the amount, the justification, the AI’s interpretation, and a link back to the original request into the Details field so the reviewer decides from one screen.

That configuration sends an actionable card to Outlook, raises a notification in Teams, and puts the request in the Teams Approvals app. The reviewer reads the summary, sees the stakes, and selects Approve or Reject without opening a separate portal. This action does not output an Adaptive Card for Teams. To post a real Adaptive Card into a Teams chat, swap it for Create an approval, post that card with the Teams flow bot, then resume with Wait for an approval.

The request as it appears in the Teams Approvals app looks like the one below.

Teams Approvals app entry

If the reviewer never responds, this action becomes the weak point of the whole flow, which is what Step 5 fixes.

Use RFI When the Reviewer Must Add Data

Standard approvals are perfect for a binary answer. They are much weaker when the reviewer needs to supply information mid-flow: a corrected amount, a revised delivery date, or a backup approver’s email.

The Request for Information (RFI) action in Copilot Studio covers that case. RFI pauses the agent flow, asks a designated person for specific typed inputs such as Text, Number, Date, Yes/No, or Email, and resumes automatically once they submit.

The suspended flow captures those values as output parameters and injects them into the rest of the automation, which is the part that makes RFI worth the extra configuration and one of the business-process automation problems Adam works on most often. Verify it end to end before you trust it: submit a corrected amount and confirm the downstream action used the reviewer’s value, not the AI’s.

Step 4: Log Every Decision

A human-in-the-loop workflow is only as defensible as the record it leaves behind. The moment a decision is made, automated or human, write it down.

Add an action to append a row to your logging store capturing:

  • The original request and the AI’s interpretation.

  • Which path handled it (auto-approved vs. human-reviewed).

  • Who approved or rejected it, and when.

  • Any values the reviewer supplied through RFI.

A single row should answer every audit question at once, the way the log below does.

Decision log row

That row satisfies compliance and segregation-of-duties requirements by proving the requester was not also the approver. It also becomes the data source for a Power BI dashboard showing bottlenecks, approval times, and how often the AI’s first read matched the human’s final call.


Pro Tip: Log the AI interpretation and the final human decision side by side. When the workflow starts drifting, that pairing tells you whether the real problem is the prompt, the routing rule, or the review criteria.


Step 5: Escalate on Timeout

Silence costs you more than a rejection, and it never shows up as an error.

Set a Timeout Before the Platform Sets One for You

Power Automate approvals and Copilot pauses inherit the platform’s 30-day run duration limit. Let an approval sit past that limit and every pending step times out, leaving you with a stale card and a dead workflow.

So set your own, shorter timeout well before you get there. In the approval action’s settings, define a timeout using ISO 8601 duration format, such as P3D for three days:

PT30M  = 30 minutes
PT6H   = 6 hours
P1D    = 1 day
P3D    = 3 days

Catch the Timeout Branch and Escalate It

After the approval action, use Configure run after so the next branch executes when the approval has timed out instead of when it succeeds. Microsoft’s error-handling guidance applies the same technique to failed and skipped actions, so one pattern covers all three states.

Check the has timed out box and clear is successful, as shown below.

Run after timeout settings

From that timeout branch, pick an escalation strategy that fits the risk:

  • Auto-resolve low-risk items when that outcome is acceptable and documented.

  • Escalate to the reviewer’s manager or a backup approver with a fresh, high-priority approval.

  • Split the wait across two flows for decisions that may legitimately exceed 30 days. The first flow calls Create an approval, which returns an approval ID without blocking, and stores that ID in Dataverse. A second flow picks the ID back up with Wait for an approval, so the original run can expire without losing the decision. Microsoft documents the difference between the two actions and which outputs each one gives you.

Whichever branch you pick, write it to the same log as Step 4. A timeout that never reaches the log is indistinguishable from a decision nobody made.

Test the Workflow End to End

With all five steps in place, run the whole thing:

  1. Send a small request under $500. Confirm it auto-approves and lands in your log with no human involved.

  2. Send a large or high-urgency request. Confirm the request arrives in the Teams Approvals app and your Approve/Reject decision is captured.

  3. Send another large request and ignore it past your timeout. Confirm the escalation branch fires and records the timeout path.

Read each result in the flow’s 28-day run history instead of trusting the notification email. The run detail shows which branch executed and exactly what the Parse JSON step produced. When the int() cast is missing, you get no branch at all: the Condition itself is marked failed, and its error names the mismatched String and Integer types that a quoted Amount created.

When all three behave, ship it, then read the log again after two weeks. If nearly everything landed in the human branch, your $500 threshold is wrong for this process. If the AI’s RequestType disagreed with the reviewer’s final call more than a few times, fix the prompt before you widen the auto-approve gate. Nobody will report either problem, because both of them look like a workflow that is running fine.

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!