Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Automate

Building and Deploying Power Automate Flows That Call Azure OpenAI and AI Builder Models

Learn how to build production-grade Power Automate flows that integrate Azure OpenAI and AI Builder for real business automation — from secure authentication and structured prompt engineering to error handling, ALM promotion, and cost management. This is the complete, no-shortcuts guide to deploying AI-powered flows that actually survive production.

🔥 Expert30 min readAug 29, 2026Updated Sep 27, 2026
Building and Deploying Power Automate Flows That Call Azure OpenAI and AI Builder Models
On this page
  • Introduction
  • Prerequisites
  • Understanding the Architecture Before You Build
  • Why HTTP Action Instead of the Azure OpenAI Connector?
  • Setting Up Secure Authentication: Environment Variables and Key Vault
  • Creating an Environment Variable for Your API Key
  • Referencing Environment Variables in Flows
  • Building the Azure OpenAI HTTP Call: The Engine of the Flow
  • Engineering the Prompt for Structured Output
  • Using a Compose Action as a Staging Area
  • Parsing the Azure OpenAI Response
  • The Parse JSON Pattern
  • Integrating AI Builder Models as Native Actions
  • Using the Prebuilt Sentiment Analysis Model
  • Using AI Builder Entity Extraction
  • Building and Calling a Custom AI Builder Prediction Model
  • Conditional Routing: Translating AI Outputs Into Business Actions
  • Building the Teams Adaptive Card Notification
  • Writing Results to Dataverse: Building the Audit Trail
  • Designing for Failure: Error Handling and Retry Logic
  • Configuring HTTP Action Retry Policies
  • Running Actions on Failure: The Parallel Branch Pattern
  • Handling Parse JSON Failures Specifically
  • Managing the Flow for Production: ALM and Promotion Patterns
  • Solution-First Development
  • Environment-Specific Configuration
  • Monitoring with Application Insights
  • Performance, Scaling, and Cost Considerations
  • Token Budget Management
  • Concurrency and Throughput
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Building and Deploying Power Automate Flows That Call Azure OpenAI and AI Builder Models: Integrating Generative AI and Prediction Actions into Production Business Workflows

    Introduction

    Your company's customer support team is drowning. They receive 800+ emails a day, and the triage process — reading each one, figuring out urgency, routing to the right department, and drafting an initial response — eats up nearly two hours per agent every morning before any real work gets done. You've been handed the mandate to automate it. The natural instinct is to write a bunch of conditional logic: if the email contains the word "refund," route it to billing; if it contains "broken," route it to support. You know, even before you finish the sentence, that this will be a brittle disaster. What you actually need is a system that understands language the way a human does — one that can read an email, infer intent and sentiment, generate a contextually appropriate draft response, and log a structured record for your CRM. That's exactly what you can build by combining Power Automate with Azure OpenAI and AI Builder.

    By the end of this lesson, you won't just know how to drop a few connector actions into a flow. You'll understand the architectural choices that determine whether an AI-powered automation survives production or falls apart under load, how to authenticate securely without hardcoding credentials, how to parse and transform LLM outputs reliably, and how to handle the failure modes that will absolutely occur in a live environment. We'll build a complete, production-grade email triage and response flow from scratch, and then layer in an AI Builder prediction model to prioritize tickets based on historical resolution data.

    What you'll learn:

    • How to architect Power Automate flows that call Azure OpenAI via HTTP actions, including authentication, prompt engineering for automation contexts, and structured output parsing
    • How to use AI Builder's prebuilt and custom models — sentiment analysis, entity extraction, and custom prediction — as native actions within production flows
    • How to design for failure: retry logic, graceful degradation, error branch handling, and monitoring patterns that give you visibility when AI calls go wrong
    • How to manage secrets, environment variables, and connection references so your flows can be safely promoted from development to test to production without credential exposure
    • Advanced output shaping techniques — using JSON schema validation, regular expression parsing, and intermediate Compose actions — to convert unpredictable LLM text into stable, downstream-usable data structures

    Prerequisites

    Before you work through this lesson, you should have:

    • An active Power Platform environment with a Dataverse database provisioned
    • An Azure subscription with Azure OpenAI Service enabled and a gpt-4o or gpt-4 deployment created (the resource endpoint and API key accessible)
    • AI Builder credits allocated to your Power Platform environment (or a trial enabled)
    • Familiarity with Power Automate's basic canvas — you know how to create a cloud flow, add actions, and configure triggers
    • Comfort reading JSON — you don't need to be a developer, but you need to understand what a key-value pair is and how to access a nested property
    • A Dataverse table or SharePoint list where you want to write results (we'll use Dataverse)

    If you haven't yet created an Azure OpenAI deployment, do that first. Navigate to the Azure portal, create an Azure OpenAI resource, then go to Azure OpenAI Studio and deploy a model. Note your endpoint URL (it looks like https://your-resource-name.openai.azure.com/) and your API key from the resource's "Keys and Endpoint" blade.


    Understanding the Architecture Before You Build

    The biggest mistake people make when building AI-powered flows is treating the AI call as "just another action" and building linearly without thinking about the flow's data shape at each stage. Before touching the designer, you need a clear mental model of what data enters the flow, what the AI returns, how that return value is transformed, and what gets written downstream.

    Here's the architecture we're building:

    Email arrives (Outlook trigger)
        → Extract metadata (subject, sender, body, timestamp)
        → Call Azure OpenAI (classify intent, sentiment, urgency, draft reply)
        → Parse LLM JSON response
        → Call AI Builder Sentiment model (validate/augment)
        → Conditional routing based on classification
        → Write record to Dataverse
        → Send draft reply to agent's queue (Teams adaptive card)
        → Log to Application Insights (optional but recommended)
    

    The critical insight here is that Azure OpenAI and AI Builder serve different purposes in this stack, and you shouldn't try to make one do the other's job. Azure OpenAI is your generalist: it's reading unstructured text and producing complex, nuanced outputs — classification, reasoning, draft generation. AI Builder's prebuilt Sentiment model is your specialist: it's a purpose-built, deterministic-feeling model that gives you a consistent, structured sentiment score you can trust for routing logic. Using Azure OpenAI for sentiment directly is fine, but having AI Builder's dedicated model as a secondary signal gives you redundancy and auditability — regulators and managers love being able to point to a recognized Microsoft model as the sentiment arbiter.

    Why HTTP Action Instead of the Azure OpenAI Connector?

    At the time of writing, the native Azure OpenAI connector in Power Automate is still maturing, and production teams regularly run into rate limit handling issues and limited control over request headers. The premium HTTP action gives you full control over the request body, headers, timeout settings, and retry policies. Once you understand how to call Azure OpenAI via HTTP, the principles transfer directly to any REST API — which is a career-spanning skill worth developing deliberately.


    Setting Up Secure Authentication: Environment Variables and Key Vault

    Never — and I mean never — hardcode an API key in a flow action. If your flow is exported, shared, or accidentally shown in a screen recording, you've exposed a credential that can rack up thousands of dollars in Azure charges or exfiltrate your data.

    The right pattern has two layers. For environments where your organization has Azure Key Vault integrated, you store the secret there and reference it via a Power Automate connection to Key Vault, fetching it at runtime. For environments without Key Vault access, you use Power Platform Environment Variables of type Secret (available in managed environments), which encrypt the value at rest and expose it only within flows.

    Creating an Environment Variable for Your API Key

    In the Power Apps maker portal (make.powerapps.com), navigate to your target environment, then go to Solutions. Open or create a solution — and yes, all production flows should live inside a solution for ALM (Application Lifecycle Management) purposes. Inside the solution, select New → Environment Variable. Give it the name AzureOpenAI_APIKey and set the type to Secret. Enter your API key as the current value.

    For your endpoint URL, create a second environment variable with type String and value https://your-resource-name.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-01. This URL structure is the Azure OpenAI REST API format — the API version matters and should match your deployment.

    Architecture note

    Separating the endpoint into its own environment variable means promoting from dev to prod only requires updating the variable value, not opening and editing the flow. This is the difference between a maintainable system and a support nightmare.

    Referencing Environment Variables in Flows

    Inside a flow, when you configure a field that accepts dynamic content, you can reference environment variables using the expression parameters('AzureOpenAI_APIKey'). This only works for environment variables defined within the same solution as your flow — another reason to always work inside solutions.


    Building the Azure OpenAI HTTP Call: The Engine of the Flow

    With authentication handled, let's build the core AI call. In your flow, after your trigger and any initial data extraction steps, add a new action. Search for "HTTP" and select the HTTP action (the one labeled simply "HTTP" — it's a premium connector).

    Configure the action as follows:

    Method: POST

    URI: @{parameters('AzureOpenAI_APIEndpoint')}

    Headers:

    • Content-Type: application/json
    • api-key: @{parameters('AzureOpenAI_APIKey')}

    The body is where the real work happens.

    Engineering the Prompt for Structured Output

    For automation contexts, you should always instruct the model to return JSON. Unstructured prose is beautiful and human, but it's poison in an automation pipeline because you can't reliably parse it. Use the system message to establish the output contract, and use the user message to inject the actual email content.

    Here's the request body:

    {
      "messages": [
        {
          "role": "system",
          "content": "You are an expert customer support triage assistant. Analyze the customer email provided and return ONLY a valid JSON object — no markdown, no explanation, no code fences — with exactly these fields:\n- intent: one of [billing_inquiry, technical_support, refund_request, general_feedback, escalation_request, account_access]\n- urgency: one of [critical, high, medium, low]\n- sentiment: one of [positive, neutral, negative, hostile]\n- sentiment_score: a float between -1.0 (most negative) and 1.0 (most positive)\n- key_entities: an array of strings representing product names, order numbers, or account identifiers mentioned\n- suggested_routing_team: one of [billing, tier1_support, tier2_support, account_management, executive_escalation]\n- draft_reply: a professionally worded, empathetic reply of 3-5 sentences that acknowledges the issue and sets expectations for next steps. Do not resolve the issue — just acknowledge and set expectations.\n- confidence: a float between 0.0 and 1.0 representing your confidence in the intent classification"
        },
        {
          "role": "user",
          "content": "Sender: @{triggerOutputs()?['body/from']}\nSubject: @{triggerOutputs()?['body/subject']}\nReceived: @{triggerOutputs()?['body/receivedDateTime']}\n\nEmail Body:\n@{triggerOutputs()?['body/body/content']}"
        }
      ],
      "temperature": 0.2,
      "max_tokens": 800,
      "response_format": {"type": "json_object"}
    }
    

    Several things in this prompt deserve explanation.

    First, notice "response_format": {"type": "json_object"} — this is the JSON mode feature available in GPT-4o and GPT-4 Turbo. It forces the model to return valid JSON, but it does not enforce a specific schema. The system prompt does that work. The combination of JSON mode plus explicit schema instructions in the system message is the most reliable way to get parseable output.

    Second, temperature: 0.2 is intentional. Lower temperature means more deterministic, consistent output — you're not asking for creative writing, you're asking for reliable classification. For draft replies where a touch of natural variation is acceptable, you might bump this to 0.4, but stay below 0.5 for anything that feeds into routing logic.

    Third, the system prompt uses \n within the JSON string to represent literal newlines. In the Power Automate expression editor, the body field accepts this as a text expression — wrap the entire JSON in an expression using json(...) or enter it directly as the raw body. If you're pasting multi-line JSON into the body field, use the "Switch to input entire array" option if available, or use a Compose action beforehand.

    Using a Compose Action as a Staging Area

    A practice that will save you enormous debugging time is staging complex request bodies in a Compose action before the HTTP call:

    Action: Compose
    Name: Compose_OpenAI_RequestBody
    Inputs:
    {
      "messages": [
        {
          "role": "system",
          "content": "..."
        },
        {
          "role": "user", 
          "content": concat('Sender: ', triggerOutputs()?['body/from'], '\nSubject: ', triggerOutputs()?['body/subject'], '\n\nEmail Body:\n', triggerOutputs()?['body/body/content'])
        }
      ],
      "temperature": 0.2,
      "max_tokens": 800,
      "response_format": {"type": "json_object"}
    }
    

    Then in the HTTP action's body field, reference outputs('Compose_OpenAI_RequestBody'). Now when something goes wrong, you can inspect the run history, click the Compose action, and see exactly what was sent to the API — an invaluable debugging shortcut.


    Parsing the Azure OpenAI Response

    The HTTP action returns the full API response as a JSON object. The model's actual text output lives at outputs('HTTP_CallAzureOpenAI')?['body']?['choices'][0]?['message']?['content']. This is a string containing JSON — nested JSON inside JSON. You need to parse it before you can access individual fields.

    The Parse JSON Pattern

    Add a Compose action to extract the content string:

    Action: Compose  
    Name: Compose_LLMResponseString
    Inputs: @{outputs('HTTP_CallAzureOpenAI')?['body']?['choices'][0]?['message']?['content']}
    

    Then add a Parse JSON action:

    Action: Parse JSON
    Name: ParseJSON_LLMOutput
    Content: @{outputs('Compose_LLMResponseString')}
    Schema: (paste the schema below)
    

    Use this schema, which matches the output contract you defined in the system prompt:

    {
      "type": "object",
      "properties": {
        "intent": {"type": "string"},
        "urgency": {"type": "string"},
        "sentiment": {"type": "string"},
        "sentiment_score": {"type": "number"},
        "key_entities": {
          "type": "array",
          "items": {"type": "string"}
        },
        "suggested_routing_team": {"type": "string"},
        "draft_reply": {"type": "string"},
        "confidence": {"type": "number"}
      }
    }
    

    After this action succeeds, you can reference body('ParseJSON_LLMOutput')?['intent'], body('ParseJSON_LLMOutput')?['urgency'], and so on as clean, typed values throughout the rest of the flow.

    Warning

    Parse JSON will fail if the content string doesn't match the schema — for example, if the model hallucinates an extra field or returns a slightly malformed structure. This is a real failure mode. We'll address it in the error handling section.


    Integrating AI Builder Models as Native Actions

    Now let's add AI Builder to the picture. AI Builder actions appear as first-class citizens in the Power Automate designer — no HTTP calls, no manual parsing. They're fully managed, connected to your Power Platform environment, and draw from your AI Builder credit allocation.

    Using the Prebuilt Sentiment Analysis Model

    After your Parse JSON action, add a new step and search for "Sentiment Analysis." Select the AI Builder action Analyze positive or negative sentiment in text. For the Language field, select the appropriate option or use "Auto detect." For the Text field, reference your original email body: triggerOutputs()?['body/body/content'].

    The action returns:

    • Overall text sentiment: one of Positive, Neutral, or Negative
    • Positive score, Neutral score, Negative score: floats between 0 and 1

    Now you have two sentiment signals: the one from Azure OpenAI (nuanced, contextual, part of a larger classification) and the one from AI Builder (purpose-built, auditable, consistent). You can use them in concert. For example, your routing logic might say: if Azure OpenAI classifies urgency as "high" AND AI Builder sentiment is "Negative" with a negative score above 0.7, escalate to executive support regardless of other factors.

    Using AI Builder Entity Extraction

    The prebuilt Extract information from text (entity extraction) action can identify names, organizations, dates, phone numbers, and product codes from the email body without any training. Add it after the sentiment analysis step, feeding in the email body. The outputs are structured arrays of detected entities, each with a type and a value.

    This is useful as a validation layer — if your Azure OpenAI response includes key entities but the AI Builder extraction finds none of them in the text, that's a signal the LLM may have hallucinated entity values. You can build a confidence-adjustment step based on this cross-check.

    Building and Calling a Custom AI Builder Prediction Model

    The prebuilt models are powerful, but the real differentiation comes from custom models trained on your organization's own data. Let's walk through the pattern for calling a custom prediction model — specifically, one trained to predict ticket resolution time based on historical support data.

    Assume you've already built and published a custom Prediction model in AI Builder called "SupportTicketPriorityPredictor." This model was trained on a Dataverse table of historical support tickets, with features including intent_category, sentiment_score, customer_tier, and product_line, and the prediction target is resolution_time_category (one of: under_1_hour, under_4_hours, under_24_hours, over_24_hours).

    In your flow, after you've extracted these values from the Azure OpenAI output, add the action Make a prediction (under AI Builder). Select your model from the dropdown. Map the input columns:

    • intent_category → body('ParseJSON_LLMOutput')?['intent']
    • sentiment_score → body('ParseJSON_LLMOutput')?['sentiment_score']
    • customer_tier → (however you retrieve this — from a prior Dataverse lookup by sender email)
    • product_line → (from the key_entities array or a product lookup)

    The action returns Prediction result (the predicted class label) and a Confidence score (a probability-like float between 0 and 1). Pair this with the urgency from Azure OpenAI for a composite priority signal that's both AI-generated and grounded in your historical data.

    Important

    Custom AI Builder models must be in a published state to be called from flows. If you retrain the model, you need to republish before flows pick up the new version. There is no concept of model versioning aliases in AI Builder — publication is atomic, and all flows immediately use the new model. Test thoroughly before publishing a retrained model.


    Conditional Routing: Translating AI Outputs Into Business Actions

    With your AI-derived signals in hand — intent, urgency, sentiment from two sources, predicted resolution time, and routing team — you need to translate these into concrete actions. This is where many flows become a tangled mess of nested conditions. Use the Switch control instead of nested Condition blocks wherever you're branching on a single categorical variable.

    Add a Switch control. The "On" expression references body('ParseJSON_LLMOutput')?['suggested_routing_team']. Create cases for each routing team value: billing, tier1_support, tier2_support, account_management, executive_escalation.

    Within each case, the actions will differ. For tier1_support, you might:

    1. Create a Dataverse record in your Support Tickets table
    2. Send a Teams adaptive card to the Tier 1 channel with the draft reply and classification metadata
    3. Assign via round-robin to the on-duty agent (which you can implement with a Dataverse query and a counter)

    For executive_escalation, you'd skip Teams and instead send an email to a specific escalation distribution list and trigger a separate notification flow.

    Building the Teams Adaptive Card Notification

    Teams adaptive cards are JSON templates that render as rich interactive messages. Power Automate's Teams connector has a "Post adaptive card and wait for a response" action that lets you collect agent feedback (approve/edit the draft reply, change the routing) directly from the card. This closes the human-in-the-loop loop without requiring agents to leave Teams.

    The adaptive card body for an agent review card would look like this (condensed for clarity):

    {
      "type": "AdaptiveCard",
      "version": "1.4",
      "body": [
        {
          "type": "TextBlock",
          "text": "New Support Ticket: @{body('ParseJSON_LLMOutput')?['intent']}",
          "weight": "bolder",
          "size": "medium"
        },
        {
          "type": "FactSet",
          "facts": [
            {"title": "Urgency", "value": "@{body('ParseJSON_LLMOutput')?['urgency']}"},
            {"title": "Sentiment", "value": "@{body('ParseJSON_LLMOutput')?['sentiment']}"},
            {"title": "Predicted Resolution", "value": "@{outputs('MakeAPrediction')?['body/responsev2/predictionOutput/result']?['resolution_time_category']}"},
            {"title": "Sender", "value": "@{triggerOutputs()?['body/from']}"}
          ]
        },
        {
          "type": "TextBlock",
          "text": "Draft Reply:",
          "weight": "bolder"
        },
        {
          "type": "TextBlock",
          "text": "@{body('ParseJSON_LLMOutput')?['draft_reply']}",
          "wrap": true
        }
      ],
      "actions": [
        {"type": "Action.Submit", "title": "Approve & Send", "data": {"action": "approve"}},
        {"type": "Action.Submit", "title": "Edit Before Sending", "data": {"action": "edit"}}
      ]
    }
    

    Tip

    The "Post adaptive card and wait for a response" action has a configurable timeout. Set it to something reasonable — 4 hours for standard tickets, 30 minutes for critical urgency. After timeout, configure the action to treat the response as auto-approved, or escalate further. The default is to just fail the flow run, which is not what you want in production.


    Writing Results to Dataverse: Building the Audit Trail

    Every AI decision in your production flow should produce a written record. This isn't just good practice — it's how you build the dataset that lets you retrain your AI Builder model in six months and evaluate whether the Azure OpenAI classifications were accurate.

    Create a Dataverse table called AI_EmailTriageLog with these columns:

    Column Name Type Notes
    TriageLogId Autonumber Primary key
    EmailMessageId Text From the trigger
    SenderEmail Text
    ReceivedDateTime DateTime
    LLMIntent Text From Azure OpenAI
    LLMUrgency Text
    LLMSentiment Text
    LLMSentimentScore Decimal
    LLMConfidence Decimal
    AIBuilderSentiment Text
    AIBuilderNegativeScore Decimal
    PredictedResolutionCategory Text
    PredictionConfidence Decimal
    RoutingTeam Text
    DraftReply Multiline Text
    AgentAction Text approve/edit/escalate
    RawLLMResponse Multiline Text Full JSON string
    FlowRunId Text workflow()?['run']?['name']
    ProcessedAt DateTime utcNow()

    The RawLLMResponse column is particularly important. Store the full, unparsed JSON string from the LLM every time. This is your forensic record — if you ever need to investigate why a ticket was routed incorrectly, you have the exact output the model returned, not just the parsed fields you chose to store.

    Add a Create a new record (Dataverse) action after your routing switch completes, mapping all these fields.


    Designing for Failure: Error Handling and Retry Logic

    This is where production flows diverge from demos. A demo flow has a happy path. A production flow has a happy path and a dozen error paths that keep the business running when AI services misbehave.

    Configuring HTTP Action Retry Policies

    The HTTP action has a configurable retry policy. Click the three dots on the HTTP action, select "Settings," and find the Retry Policy section. For Azure OpenAI calls:

    • Policy Type: Exponential interval
    • Count: 4 (up to 4 retries)
    • Interval: PT5S (5 seconds base)
    • Minimum Interval: PT5S
    • Maximum Interval: PT1M

    Exponential backoff is essential here because the most common transient failure mode is rate limiting (HTTP 429). Azure OpenAI has tokens-per-minute and requests-per-minute limits per deployment. When you hit them, the API returns a 429 with a Retry-After header. Power Automate's exponential retry policy respects this without any additional configuration — the exponential backoff naturally spaces requests further apart.

    Warning

    The default retry policy in Power Automate is "None." Every HTTP action you add to a production flow should have an explicit retry policy configured. The default means a transient network error kills your flow immediately.

    Running Actions on Failure: The Parallel Branch Pattern

    For critical failure handling, use the "Configure run after" setting on a subsequent action. When you add an action after your HTTP call, click the three dots and choose "Configure run after." You can specify that this action should run when the previous action has failed, timed out, or been skipped — not just on success.

    Create a Scope action called "Scope_AzureOpenAI_Call" that contains your HTTP call and Parse JSON. Then create a parallel action outside the scope that runs "is failed" on that scope. Inside this failure branch:

    1. Log the failure to your AI_EmailTriageLog table with status "AI_FAILURE" and null AI fields
    2. Route the email directly to your human triage queue with a note that automated classification failed
    3. Send a notification to your flow monitoring team via Teams
    4. Optionally: call a fallback logic path using simpler rule-based classification (keyword matching via condition actions) so the email isn't just sitting in limbo

    This pattern ensures that even when Azure OpenAI is down or rate-limited beyond retry tolerance, the business process continues — just without the AI assistance.

    Handling Parse JSON Failures Specifically

    Parse JSON failure is particularly insidious because it means the LLM returned something, but it wasn't the format you expected. This is more dangerous than a total HTTP failure because your flow might partially succeed.

    Add a Condition action after Parse JSON that checks whether it succeeded:

    Expression: equals(outputs('ParseJSON_LLMOutput')?['statusCode'], 400)
    

    Actually, a more robust check: use the result() function in a Scope, or check the action's status using if(equals(actions('ParseJSON_LLMOutput')?['status'], 'Succeeded'), true, false).

    When Parse JSON fails, fall back to regex extraction. Add a Compose action that uses Power Automate's uriComponentToString and regular expression–style operations with split and indexOf to extract at minimum the intent and urgency from the raw string. These are lossy but better than nothing:

    Expression for emergency intent extraction:
    if(
      contains(toLower(outputs('Compose_LLMResponseString')), 'billing'),
      'billing_inquiry',
      if(
        contains(toLower(outputs('Compose_LLMResponseString')), 'refund'),
        'refund_request',
        'general_feedback'
      )
    )
    

    It's not pretty, but it's a safety net that keeps your downstream Dataverse write from failing on null fields.


    Managing the Flow for Production: ALM and Promotion Patterns

    Building the flow is 40% of the job. Making it maintainable in production is the other 60%.

    Solution-First Development

    Your flow, all its connections, and all its environment variables must live in a single unmanaged solution during development. When you're ready to promote to test or production, you export the solution as a Managed solution and import it. Managed solutions prevent direct editing in the target environment — which is exactly what you want, because it means production changes must go through your development process.

    The key ALM consideration for AI-powered flows is connection references. Every connector your flow uses — the HTTP connector, Dataverse, Teams, AI Builder — appears in the solution as a Connection Reference. When you import into a new environment, you map each connection reference to an actual connection in that environment. Document these mappings before you have to do an emergency deployment at 11 PM.

    Environment-Specific Configuration

    The endpoint URL environment variable you created earlier is the mechanism for pointing at different Azure OpenAI deployments in different environments. Your dev environment might point at a smaller, cheaper GPT-3.5-Turbo deployment (for testing without burning credits). Your production environment points at GPT-4o. The flow itself is identical — only the environment variable values change.

    For AI Builder models, the model is tied to the environment. If you have a custom prediction model, you'll need to re-create or migrate it in each environment. AI Builder does support exporting and importing models as part of solutions — make sure your model is added to the solution before export.

    Monitoring with Application Insights

    If your organization has an Application Insights workspace connected to your Power Platform environment (available in Dataverse-backed environments with the Application Insights integration enabled), your flow runs automatically emit telemetry. But you can add custom telemetry by calling the Application Insights REST API from your flows using an HTTP action after key milestones.

    Send a custom event after every successful AI classification:

    {
      "name": "EmailTriageClassified",
      "properties": {
        "intent": "@{body('ParseJSON_LLMOutput')?['intent']}",
        "urgency": "@{body('ParseJSON_LLMOutput')?['urgency']}",
        "confidence": "@{body('ParseJSON_LLMOutput')?['confidence']}",
        "routingTeam": "@{body('ParseJSON_LLMOutput')?['suggested_routing_team']}",
        "flowRunId": "@{workflow()?['run']?['name']}"
      }
    }
    

    This gives you a searchable, queryable log in Azure that lets you build dashboards showing classification distribution over time, confidence score trends, and routing team volume — exactly the data you need to evaluate whether the system is working and justify its continued operation to leadership.


    Performance, Scaling, and Cost Considerations

    Running hundreds of LLM calls per day has real cost and performance implications that you need to design around.

    Token Budget Management

    Every Azure OpenAI call costs tokens — both input (prompt) tokens and output (completion) tokens. Your prompt template is fixed-length plus the email body. Long emails can push your input token count high. Set max_tokens: 800 for the completion (as we did) to cap the output cost, but consider also truncating the email body input to a maximum of 2000 characters for classification purposes — the first 2000 characters of most emails contain the key information, and beyond that you're paying for signal you likely don't need for classification purposes.

    Build a Compose action that truncates the email body:

    Expression: 
    if(
      greater(length(triggerOutputs()?['body/body/content']), 2000),
      concat(substring(triggerOutputs()?['body/body/content'], 0, 2000), '... [truncated]'),
      triggerOutputs()?['body/body/content']
    )
    

    At current pricing (approximate), GPT-4o costs around $0.005 per 1,000 input tokens and $0.015 per 1,000 output tokens. An average call with our prompt might use ~600 input tokens and ~300 output tokens, costing roughly $0.0075 per email. At 800 emails/day, that's about $6/day or ~$180/month. Not enormous, but worth tracking — and a model that consumes more tokens than expected could spike this significantly.

    Concurrency and Throughput

    By default, Power Automate flows triggered by Outlook process one email at a time. For 800 emails in a two-hour morning window, that's roughly 6.7 emails per minute, or one every 9 seconds. If your Azure OpenAI call takes an average of 3-5 seconds (typical for GPT-4o), you should be fine — the flow will queue naturally.

    However, if you hit rate limits during peak load, the exponential retry policy will start adding latency. Monitor your Azure OpenAI deployment's TPM (tokens per minute) and RPM (requests per minute) limits and provision appropriately. For high-volume production scenarios, consider provisioned throughput (PTU) deployments in Azure OpenAI, which give you dedicated capacity without rate limiting.

    If your flow trigger is something other than per-email (e.g., a scheduled flow processing a batch), you can use the Apply to Each action with its concurrency control set to a value like 5 — processing 5 emails simultaneously. This dramatically improves throughput but requires your Azure OpenAI deployment to handle the concurrent load.

    Warning

    AI Builder actions also consume credits. AI Builder Sentiment Analysis costs 1 credit per 1,000 characters. A 500-character email costs 1 credit. At 800 emails/day, that's 800 credits/day or ~24,000 credits/month just for sentiment. Check your credit allocation and budget accordingly. AI Builder credits are purchased in packs or come with Power Apps Premium licenses.


    Hands-On Exercise

    Build a complete working version of the email triage flow described in this lesson. Here's your scaffolded challenge:

    Phase 1 – Foundation (30 minutes) Create a new cloud flow in a solution. Use the "When a new email arrives" trigger with the folder set to a specific test folder (create a folder called "AI_Test_Inbox" in your Outlook). Add a Compose action that constructs the Azure OpenAI request body as described, using hardcoded test values (not environment variables yet). Run the flow manually by sending a test email to yourself and moving it to the test folder.

    Phase 2 – AI Integration (45 minutes) Replace the hardcoded request body with environment variable references. Add the HTTP action and call your Azure OpenAI deployment. Add the Compose action to extract the response content, then the Parse JSON action to structure it. Add the AI Builder Sentiment Analysis action. Run the flow end-to-end and inspect the run history to confirm all outputs are populated correctly.

    Phase 3 – Routing and Storage (30 minutes) Add the Switch control for routing team, with at least three cases: billing, tier1_support, and an "otherwise" default. Inside the tier1_support case, add a Dataverse "Create a new record" action writing the key AI outputs to the AI_EmailTriageLog table you create. In the default case, add a Teams notification using the Post a message action (not the adaptive card — keep it simple for now).

    Phase 4 – Error Handling (30 minutes) Wrap your HTTP call and Parse JSON in a Scope. Add a parallel branch action after the scope, configured to run on failure. In the failure branch, write a record to AI_EmailTriageLog with status "AI_FAILURE" and all AI fields set to null. Send a Teams notification to yourself with the error details using result('Scope_AzureOpenAI_Call') to capture the failure information.

    Stretch goal: Implement the adaptive card Teams notification with approve/edit response handling, and use the agent's response to update the Dataverse record with the AgentAction field.


    Common Mistakes & Troubleshooting

    "Parse JSON fails intermittently, even with JSON mode enabled" JSON mode reduces but does not eliminate malformed outputs. The most common cause is the email body containing characters that break JSON encoding — particularly unescaped double quotes, backslashes, or control characters. Pre-process the email body through a replace() expression to escape known problematic characters before injecting it into the prompt. Also check that your prompt body isn't being truncated by Power Automate's expression length limits — very long prompts can be silently cut off.

    "HTTP action returns 401 Unauthorized" Almost always an API key issue. Verify that the environment variable is in the same solution as the flow, that the api-key header name is exact (it's not Authorization: Bearer, it's api-key for Azure OpenAI), and that the key hasn't been rotated in Azure without updating the environment variable.

    "HTTP action returns 404 Not Found" Check your endpoint URL carefully. The URL format must be: https://{resource-name}.openai.azure.com/openai/deployments/{deployment-name}/chat/completions?api-version=2024-02-01. The deployment-name in the URL must match the deployment name in Azure OpenAI Studio exactly — not the model name (gpt-4o) but the deployment name you assigned when you deployed it (often the same, but not always).

    "AI Builder action returns no output / empty results" First confirm the model is published. For the prebuilt Sentiment action, check that AI Builder is enabled in your environment admin center. Go to the Power Platform admin center, select your environment, and verify that AI Builder is listed as enabled. Also check that the user account or service principal running the flow has AI Builder permissions in that environment.

    "Flow runs fine in dev but fails in production after solution import" Almost always a connection reference mapping issue. After importing the managed solution, go to the solution's connection references and verify each one is mapped to a valid connection owned by an account that has appropriate permissions in production. Also check that environment variables have their current value set in the production environment — environment variable current values are not exported with solutions (by design), so you must set them manually after import.

    "The LLM returns valid JSON but with additional fields I didn't ask for" This is normal behavior. JSON mode ensures valid JSON but not exact schema conformance. Your Parse JSON schema only extracts the fields you defined — additional fields are silently ignored. This is safe. The risk direction is the opposite: the LLM occasionally omits a field. Make all schema properties optional (remove them from a required array) and use null-safe operators (?) when accessing parsed values.

    "Confidence score from Azure OpenAI seems unreliable" It is. LLM-generated confidence scores are not calibrated probabilities — they're the model's subjective self-assessment. Don't use them as hard thresholds. Use them as directional signals in combination with other factors. If the confidence is below 0.5, consider routing to human triage rather than making any automated decision. The AI Builder prediction confidence, by contrast, is a properly calibrated posterior probability from a trained classifier — you can use those values more assertively.


    Summary & Next Steps

    You've built something substantial here. Let's take stock of what you now understand and can implement.

    You know how to call Azure OpenAI via the HTTP action with proper authentication via environment variables, constructing prompts that produce reliable, parseable JSON outputs through the combination of JSON mode and explicit schema instructions in the system message. You know how to parse nested JSON responses, stage intermediate values in Compose actions for debuggability, and use AI Builder's prebuilt and custom models as complementary specialist components alongside the generalist LLM. You've designed routing logic using Switch controls, built an audit trail in Dataverse that supports future model evaluation and retraining, and implemented failure paths that keep the business running when AI services are unavailable. You understand the cost and performance math that turns an impressive demo into a sustainable production system.

    The architecture pattern you've built — LLM for nuanced understanding, specialist models for structured signals, conditional routing, human-in-the-loop via adaptive cards, and a comprehensive audit log — is not specific to email triage. It applies directly to document processing, invoice classification, contract review routing, support ticket deflection, and any other business process that involves making decisions based on unstructured text at scale.

    Where to go next:

    • Prompt engineering depth: Explore few-shot prompting in your system messages — providing 2-3 examples of ideal input/output pairs dramatically improves classification accuracy for domain-specific categories your general-purpose model might not handle well
    • AI Builder custom model lifecycle: Learn to build a training dataset from your Dataverse audit log records, retrain your prediction model on real production data, and implement a canary deployment pattern where you test the new model against a subset of traffic before full promotion
    • Power Automate Desktop + cloud hybrid flows: For documents that arrive as PDFs or scanned images rather than email text, learn how PAD's OCR capabilities can extract text that then feeds your cloud flow AI pipeline
    • Dataverse + Copilot Studio integration: The Dataverse records you're creating can become the knowledge base that powers a Copilot Studio agent, creating a feedback loop where your triage flow feeds your conversational AI assistant
    • Azure API Management as a gateway: In high-volume production, route your Azure OpenAI calls through APIM rather than directly, gaining centralized rate limit management, request logging, cost allocation by department, and the ability to swap model deployments without touching flows

    The skills you've developed in this lesson sit at the intersection of enterprise automation and applied AI — and that intersection is where most of the valuable, non-trivial work in modern data and automation engineering happens.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Flow Automation Basics

    Previous

    Building Power Automate Flows for Microsoft 365 Group and SharePoint Permission Management

    Next

    Understanding Power Automate Licensing: Choosing the Right Plan for Your Automation Needs

    Related Insights

    Power AutomatePractitioner

    Automating SharePoint-Based Leave Requests with Power Automate: Building an End-to-End Submission, Approval, and Calendar Event Flow Using Microsoft 365 Connectors

    21 min
    Power AutomateFoundation

    Implementing Power Automate Solution Upgrade Strategies: Applying Managed Solution Patches, Superseding Updates, and Rolling Back Breaking Changes Across Environments Without Flow Downtime

    18 min
    Power AutomateExpert

    Automating Multi-Step Microsoft 365 Approval Workflows with Power Automate: Collecting Responses, Logging to SharePoint, and Notifying Teams

    24 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding the Architecture Before You Build
    • Why HTTP Action Instead of the Azure OpenAI Connector?
    • Setting Up Secure Authentication: Environment Variables and Key Vault
    • Creating an Environment Variable for Your API Key
    • Referencing Environment Variables in Flows
    • Building the Azure OpenAI HTTP Call: The Engine of the Flow
    • Engineering the Prompt for Structured Output
    • Using a Compose Action as a Staging Area
    • Parsing the Azure OpenAI Response
    • The Parse JSON Pattern
    • Integrating AI Builder Models as Native Actions
    • Using the Prebuilt Sentiment Analysis Model
    • Using AI Builder Entity Extraction
    • Building and Calling a Custom AI Builder Prediction Model
    • Conditional Routing: Translating AI Outputs Into Business Actions
    • Building the Teams Adaptive Card Notification
    • Writing Results to Dataverse: Building the Audit Trail
    • Designing for Failure: Error Handling and Retry Logic
    • Configuring HTTP Action Retry Policies
    • Running Actions on Failure: The Parallel Branch Pattern
    • Handling Parse JSON Failures Specifically
    • Managing the Flow for Production: ALM and Promotion Patterns
    • Solution-First Development
    • Environment-Specific Configuration
    • Monitoring with Application Insights
    • Performance, Scaling, and Cost Considerations
    • Token Budget Management
    • Concurrency and Throughput
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Summary & Next Steps