Content Moderation Governance Monitor - Flow Setup Guide¶
Overview¶
Step-by-step guide for deploying the CMM daily content moderation validation flow in Power Automate.
This flow provides automated orchestration and alerting for the Content Moderation Governance Monitor solution. It runs daily at 6:00 AM UTC, executes the Azure Automation runbook, detects drift from baseline moderation configurations, and routes alerts to Microsoft Teams and email based on severity.
What this flow does:
- Triggers daily at 6:00 AM UTC (configurable)
- Executes
Start-ModerationValidationRunbook.ps1in Azure Automation - Parses validation results including per-agent moderation drift detection
- Triggers the runbook that writes validation results to the Dataverse immutable audit trail (all scans, not just failures) — the flow itself does not call Dataverse to persist scan history
- Posts adaptive card to Teams for Critical/Failed/Error severity
- Sends email to distribution list for all drift alerts
- Handles errors with CRITICAL email notification
Prerequisites¶
Before creating the flow, ensure you have:
- Azure Automation Account with:
Start-ModerationValidationRunbook.ps1imported as a PowerShell 7.2 runbook- Certificate uploaded (Certificates blade)
- Modules installed: MSAL.PS
- Application permissions granted as required by Power Platform admin APIs
- Dataverse environment with CMM schema deployed (Phase 2):
- 3 tables:
fsi_moderationvalidationhistory,fsi_moderationbaselines,fsi_moderationviolations - 7 environment variables configured
- 4 connection references created
- Microsoft Teams channel for alert notifications
- Email distribution list for compliance alerts
- Power Automate Premium per user license (required for the premium Azure Automation connector). See Microsoft licensing for current SKU details.
- Connection references bound in Power Automate:
fsi_cr_dataverse_moderationmonitor(Dataverse)fsi_cr_teams_moderationmonitor(Microsoft Teams)fsi_cr_office365_moderationmonitor(Office 365 Outlook)- Azure Automation connection to your subscription
Step 1: Create the Scheduled Flow¶
No flow JSON ships with this solution (per the repository content policy). Build the flow manually using these steps.
- Navigate to make.powerautomate.com
- Select your target environment (same environment where CMM schema is deployed)
- Click Create > Scheduled cloud flow
- Name:
CMM - Content Moderation Validation (Daily) - Set schedule:
- Start: Today
- Repeat every: 1 Day
- At: 6:00 AM
- Time zone: UTC
- Click Create
- Build the flow actions following the steps in this guide
Step 2: Configure Variables¶
Update these variables in the flow designer (Initialize Variable actions):
Note: These values are configured as
Initialize Variableactions in the flow you build by hand. The solution also deploysfsi_CMM_TeamsGroupIdandfsi_CMM_TeamsChannelIdas Dataverse environment variables. For multi-environment portability, consider replacing the matching Initialize Variable actions with Get Environment Variable Value lookups (Dataverse connector) so the flow does not require manual editing per environment. Pick one approach and apply it consistently — running both leads to silent value precedence.
| Variable | Type | Default Value | Description |
|---|---|---|---|
DataverseUrl |
String | your-dataverse-url-here |
Your Dataverse environment URL (where CMM schema is deployed). Must be changed per environment. |
TenantId |
String | example.onmicrosoft.com |
Microsoft Entra ID tenant identifier |
ClientId |
String | your-client-id-here |
App registration client ID (same one used for certificate auth) |
CertificateThumbprint |
String | your-thumbprint |
Certificate thumbprint uploaded to Azure Automation |
SubscriptionId |
String | your-subscription-id-here |
Azure subscription containing Automation Account |
ResourceGroup |
String | rg-content-moderation-monitor |
Resource group with Automation Account |
AutomationAccount |
String | aa-content-moderation-monitor |
Azure Automation Account name |
TeamsGroupId |
String | your-group-id-here |
Teams group (team) ID for moderation alerts |
TeamsChannelId |
String | your-channel-id-here |
Teams channel ID for moderation alerts (get from channel link) |
ComplianceDistributionList |
String | alerts@your-org.com |
Email distribution list for all alerts |
How to get Teams Channel ID:
- In Microsoft Teams, right-click the channel > Get link to channel
- The URL format is:
...teams.microsoft.com/l/channel/[CHANNEL_ID]/... - Copy the CHANNEL_ID portion (starts with
19:)
How to get Teams Group ID:
- In Microsoft Teams, right-click the team > Get link to team
- The URL contains the group ID in the
groupIdparameter
How to get Dataverse URL:
- Navigate to make.powerapps.com
- Select your environment
- Click Settings (gear icon) > Session details
- Copy the Instance url (e.g.,
https://org.crm.dynamics.com)
Step 3: Bind Connection References¶
The flow uses four connection references deployed during Phase 2:
| Connection Reference | Service | Purpose |
|---|---|---|
fsi_cr_dataverse_moderationmonitor |
Dataverse | Write validation history records |
fsi_cr_teams_moderationmonitor |
Microsoft Teams | Post adaptive card alerts |
fsi_cr_office365_moderationmonitor |
Office 365 Outlook | Send email alerts |
fsi_cr_azureautomation_moderationmonitor |
Azure Automation | Execute validation runbook |
To bind connection references:
- In Power Automate, open the flow
- Click Edit
- For each action that shows a connection warning:
- Click the action
- Select the appropriate connection from the dropdown
- If no connection exists, click Add new connection and authenticate
- Save the flow
Step 4: Validation History Persistence¶
Important: Validation history persistence to Dataverse is handled by the PowerShell runbook (
Start-ModerationValidationRunbook.ps1) using the-PersistResultsswitch andWrite-ModerationValidationHistoryinCMMClient.psm1— not by the Power Automate flow itself. The flow's role is orchestration and alerting; the flow goes directly fromParse_ResultstoCheck_Alert_Required.
Dataverse table: fsi_ModerationValidationHistory (OrganizationOwned, immutable)
How persistence works:
- The flow triggers
Start-ModerationValidationRunbook.ps1via Azure Automation - The runbook calls
Write-ModerationValidationHistory(inCMMClient.psm1) to persist scan results to Dataverse - The flow receives the runbook output JSON, parses it (
Parse_Results), and evaluates alert conditions (Check_Alert_Required)
Troubleshooting validation history writes:
| Error Code | Cause | Resolution |
|---|---|---|
| 403 Forbidden | Runbook identity lacks Create permission on fsi_ModerationValidationHistory |
Assign security role with Organization-level Create on the table |
| 404 Not Found | Table not deployed to environment | Deploy Dataverse schema from Phase 2 (scripts/deploy.py) |
| 400 Bad Request | Schema mismatch (column names don't match) | Verify column names match the schema deployed in Phase 2 |
Step 5: Test the Flow¶
5.1 Manual Test Run¶
- Click Test in the flow editor
- Select Manually
- Click Test to start
- Monitor the flow run in real-time
5.2 Verify Each Step¶
Watch for these key actions to complete successfully:
- Create_Automation_Job: Runbook job created (returns jobId)
- Wait_For_Job: Job status polling (max 2 hours, 30-second intervals)
- Get_Job_Output: JSON output retrieved from completed runbook (includes Dataverse persistence by the runbook)
- Parse_Results: JSON schema validation passes
- Check_Alert_Required: Condition evaluates based on AlertRequired flag
- Post_Teams_Card (if Critical/Failed/Error): Adaptive card posted to Teams
- Send_Alert_Email (if alert required): Email sent to distribution list
5.3 Expected Outcomes¶
If validation passes (no violations, no drift):
- Flow completes successfully
- No Teams card posted
- No email sent
- Validation history record written to Dataverse by the runbook
- Check Azure Automation job output for
"OverallStatus": "Passed"
If validation fails or drift detected:
- Flow completes successfully
- Critical/Failed/Error: Teams card posted + email sent (High importance)
- High/Warning: Email sent only (Normal importance)
- Validation history record written to Dataverse by the runbook
- Check Teams channel for adaptive card with per-agent violation and drift details
- Check email for HTML table with zone summary, agent violations, and drift detection
Step 6: Enable Daily Schedule¶
- After successful test, the Recurrence trigger activates automatically
- Flow runs daily at 6:00 AM UTC
- Monitor first 3 days of automated runs for consistency
- Check run history: My flows > CMM - Content Moderation Validation (Daily) > Run history
Step 7: Capture Initial Baseline¶
After the flow is running, capture the initial baseline for drift detection:
# Run from PowerShell with MSAL.PS installed
.\Invoke-ModerationBaselineCapture.ps1 `
-TenantId "your-tenant.onmicrosoft.com" `
-ClientId "your-client-id" `
-DataverseUrl "https://your-org.crm.dynamics.com" `
-Interactive
Why this matters:
- The first validation run will show
IsFirstRun: truefor all agents - Drift detection requires a baseline to compare against
- Without a baseline, the runbook skips drift detection per agent
- Capture baseline after confirming your content moderation settings are correctly configured
Re-capture baselines when:
- You make approved changes to agent content moderation levels
- You update zone-specific moderation policies
- You add new agents to a governance zone
- After any intentional change to Zone 1/2/3 moderation configuration
Zone-specific capture:
# Capture only Zone 3 agents (highest risk)
.\Invoke-ModerationBaselineCapture.ps1 `
-TenantId "your-tenant.onmicrosoft.com" `
-ClientId "your-client-id" `
-DataverseUrl "https://your-org.crm.dynamics.com" `
-Zone 3 `
-Interactive
Alert Routing Summary¶
The flow routes alerts based on the AlertRequired flag set by the runbook. Only Critical/High violations or weakened drift set AlertRequired = true. Medium-severity violations produce a Warning overall status but do not trigger alerts — these are recorded in Dataverse validation history only.
| Severity | Teams Card | Email Importance | Notes | |
|---|---|---|---|---|
| Critical | Yes | Yes | High | |
| Failed | Yes | Yes | High | |
| Error | Yes | Yes | High | |
| High | No | Yes | Normal | |
| Medium | No | No | — | Recorded in Dataverse only; no active alert |
| Warning | No | No | — | Recorded in Dataverse only; no active alert |
| Passed/Info | No | No | — |
Known Limitations¶
Retry Policies¶
The seven API connection actions (Create_Automation_Job, Get_Job_Status, Get_Job_Output, Post_Teams_Card, Send_Alert_Email, Send_Job_Failure_Email, Send_Critical_Error_Email) have explicit retry policies configured (fixed interval, 3 retries, 30-second delay). These were added to handle transient connector failures (e.g., Teams throttling, temporary Automation API errors) in production governance workflows.
Environment Variables vs Flow Variables¶
Flow configuration is currently stored as InitializeVariable actions rather than reading from Dataverse environment variables at runtime. The solution deploys environment variables (e.g., fsi_CMM_TeamsGroupId, fsi_CMM_TeamsChannelId) but the flow does not yet consume them. Migrating to Get Environment Variable Value actions would require adding a Dataverse connector reference and rewiring downstream variable references. See Step 2: Configure Variables for the current approach.
Troubleshooting¶
Dataverse Write Failures¶
See the Validation History Persistence section above for error code resolution (403, 404, 400).
Additional checks:
- Confirm the runbook identity has Create permission on fsi_ModerationValidationHistory
- Check that column names in CMMClient.psm1 match the deployed schema exactly
Azure Automation Job Failures¶
- Job stuck in "Running": Check Azure Automation job logs; may be waiting for module install
- Job completes but output is empty: Verify runbook parameters, check Write-Verbose output
- Authentication errors: Verify certificate thumbprint, check certificate expiration
- Module not found: Install MSAL.PS in Automation Account (Modules blade)
- Timeout (2-hour limit): The Wait_For_Job loop times out after 2 hours; increase if scanning many agents
Teams Channel Not Found¶
- Verify
TeamsGroupIdandTeamsChannelIdvariables match your target channel - Confirm the Teams connection has permissions to post to the channel
- Check that
fsi_cr_teams_moderationmonitorconnection reference is properly bound - Test channel posting manually in Power Automate to isolate permissions issues
Parse JSON Schema Mismatch¶
- The Parse_Results schema must match the runbook output structure exactly
- If the runbook output changes (e.g., new fields added), update the schema in the flow
- Check Get_Job_Output action output for the raw JSON to debug
- Key CMM schema differences from other monitors:
TotalAgentsfield, agent-level violations withExpectedModerationLevel/ActualModerationLevel,Driftas an object (not array) withDetailsarray
No Violations Detected but Alert Sent¶
- Check the
AlertRequiredlogic in the runbook — drift alone can trigger alerts - Review
AlertSeverityin the runbook output to understand the classification - Verify zone assignments are current via ELM zone classification or naming convention
Drift Detection Shows All Agents as First Run¶
- Baselines have not been captured yet
- Run
Invoke-ModerationBaselineCapture.ps1to establish initial baselines - After capture, the next scan will compare against the baseline and detect actual drift
Adaptive Card Template Updates¶
Note: The adaptive card standalone template at
templates/adaptive-card-moderation-alert.jsonincludes full features (Violations section, Drift Detection section,${DocumentationUrl}placeholder) for reference and testing in the Adaptive Card Designer. When building the flow'sPost_Teams_Cardaction, use a simplified inline version adapted for Power Automate'sreplace()expression constraints (FactSet-based zone summary, hardcoded documentation URL). The inline version intentionally omits the Violations and Drift Detection sections because per-agent detail binding requires$datatemplating not supported by Power Automate's expression language.
Flow Errors (Scope_Catch)¶
- If you receive a "[CRITICAL] CMM Flow Execution Failed" email, the flow itself encountered an error
- Check the flow run history for the specific action that failed
- Common causes: expired connections, permission changes, Azure Automation account unavailable