Power Automate Flow Setup¶
Phase 3 — OPTIONAL. Complete Phase 1 (POC bar) and Phase 2 (Operationalize) of the POC Quickstart first. The PowerShell sync alone is enough for a working POC.
⛔ Mutually exclusive with the Phase 1 PowerShell webhook. Running both notification paths at the same time produces duplicate Teams alerts because
fsi_notifiedonprovides per-row idempotency within one path only — not cross-path deduplication. Before enabling this flow:
- Clear
$env:MCM_TEAMS_WEBHOOK_URLon every host that runsInvoke-MessageCenterSync.ps1:- Re-run
Test-McmPrerequisites.ps1— check 11 (mutual-exclusion) must report PASS before you continue building the flow.
This guide walks through creating the Message Center Monitor flow in Power Automate.
Overview¶
The flow: 1. Runs on a daily schedule 2. Calls Microsoft Graph API to get Message Center posts 3. Upserts posts to Dataverse 4. Sends Teams notifications for high-severity posts
Prerequisites¶
Before starting, ensure you have:
- Microsoft Entra ID app registration with
ServiceMessage.Read.Allpermission - Admin consent granted for the app (any administrator with permission to consent)
- Client ID and Tenant ID ready; client secret only when using the legacy cloud-flow fallback, preferably retrieved from Key Vault
- Dataverse table created (see README.md)
- Azure Key Vault with the legacy client secret stored, if using the cloud-flow fallback
Step 1: Create a New Flow¶
- Go to make.powerautomate.com
- Click Create > Scheduled cloud flow
- Name:
Message Center Monitor - Set schedule:
- Start: Today
- Repeat every: 1 Day
- At: 9:00 AM (or your preferred time)
- Click Create
Concurrency Control: To prevent duplicate processing when a manual test run overlaps with a scheduled run, configure the Recurrence trigger's concurrency setting. Click the trigger's ... menu > Settings > enable Concurrency Control and set Degree of Parallelism to
1. This means only one instance of the flow runs at a time — if a second run is triggered while one is in progress, it queues instead of running in parallel.
Step 2: Get Client Secret from Key Vault (Recommended)¶
Authentication note: Power Automate cloud flows do not natively support managed identity. For production deployments where managed-identity-first authentication is required by policy, run this workload in Logic Apps Standard or an Azure Function with a system-assigned managed identity instead. The remaining steps assume the client-secret + Key Vault path that is the supported fallback for cloud flows.
If using Azure Key Vault:
- Add action: Azure Key Vault - Get secret
- Configure:
- Vault name: Your Key Vault name
- Secret name: Your client secret name
- The output
valuecontains your client secret
If not using Key Vault, direct secret entry in the HTTP action is a legacy non-prod fallback and is not recommended for production.
Step 3: Call Microsoft Graph API¶
Add action: HTTP with Microsoft Entra ID (preauthorized). In older designers this may appear as HTTP with an OAuth authentication panel; bind the connection reference fsi_cr_http_messagecenter to the current HTTP with Microsoft Entra ID connector.
Configure:
- Method: GET
- URI: https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages?$select=id,title,category,severity,services,startDateTime,endDateTime,lastModifiedDateTime,isMajorChange,actionRequiredByDateTime,body,tags,hasAttachments
- Microsoft Entra ID resource / Audience: https://graph.microsoft.com
- Tenant: Your tenant ID (GUID)
- Client ID: Your app registration client ID
- Credential Type: Secret (legacy cloud-flow fallback)
- Secret: From Key Vault output; avoid direct entry except in disposable non-prod validation
- Retry policy: Change from default to Fixed interval — Count: 3, Interval: PT30S (30 seconds). This handles transient Graph API errors (429 throttling, 503 service unavailable) without manual re-runs.
Microsoft Graph endpoint note: The Message Center API is available in Microsoft Graph v1.0 at
/admin/serviceAnnouncement/messages; beta is not required for this solution. The separatehealthOverviewsandissuesendpoints requireServiceHealth.Read.Alland are intentionally out of scope unless you add service-health monitoring.
Authentication Screenshot Reference¶
┌─────────────────────────────────────────────┐
│ Authentication │
├─────────────────────────────────────────────┤
│ Connector: HTTP with Microsoft Entra ID │
│ │
│ Tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxx │
│ Audience: https://graph.microsoft.com │
│ Client ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxx │
│ Credential Type: Secret (legacy fallback) │
│ Secret: [Key Vault output] │
└─────────────────────────────────────────────┘
Step 4: Parse JSON Response¶
Add action: Parse JSON
- Content:
@{body('HTTP')} - Schema: Click "Generate from sample" and paste this sample:
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#admin/serviceAnnouncement/messages",
"@odata.nextLink": "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages?$skip=100",
"value": [
{
"id": "MC123456",
"title": "Sample Message Center Post",
"category": "planForChange",
"severity": "normal",
"services": ["Microsoft 365 suite"],
"startDateTime": "2026-01-01T00:00:00Z",
"endDateTime": "2026-02-01T00:00:00Z",
"lastModifiedDateTime": "2026-01-01T12:00:00Z",
"isMajorChange": false,
"actionRequiredByDateTime": null,
"body": {
"contentType": "html",
"content": "<p>Message content here</p>"
},
"tags": ["Feature update"],
"hasAttachments": false
}
]
}
Note: The
@odata.nextLinkfield appears when there are more results. Include it in your schema so Parse JSON doesn't fail when pagination is present.
Current Message Center categorization¶
Microsoft Graph serviceUpdateMessage returns category values planForChange, stayInformed, preventOrFixIssue, and unknownFutureValue; severity values are normal, high, critical, and unknownFutureValue. The Switch mappings below handle the documented values and default unknown future values to Admin/Normal for safe triage.
Message Center UI filtering is based on Service, Tag, and Message state. Tags currently include Admin impact, Data privacy, Feature update, Major update, New feature, Retirement, User impact, and Updated message. Graph returns services[] and tags[] as Microsoft-controlled strings, so route Copilot Studio, Power Platform, or other AI-related posts through configurable rules instead of hard-coding a closed service list.
Step 5: Handle Pagination (Important)¶
Microsoft Graph API returns paged results. Without handling pagination, you may only get the first page (~100 posts) and miss older messages.
Understanding @odata.nextLink¶
When more results exist than fit in one response, Graph API includes @odata.nextLink - a URL to fetch the next page. You must loop until this value is absent.
Page Size: The default page size is 100 items. You can request up to 1000 items per page by adding the
Prefer: odata.maxpagesize=1000header to your HTTP request. This reduces the number of API calls needed for tenants with many posts.
Pattern: Do Until Loop¶
- Initialize variable
nextLink(String) =https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages?$select=id,title,category,severity,services,startDateTime,endDateTime,lastModifiedDateTime,isMajorChange,actionRequiredByDateTime,body,tags,hasAttachments - Initialize variable
allMessages(Array) =[] - Do Until
@empty(variables('nextLink')): - HTTP GET to
@{variables('nextLink')}(with same authentication and retry policy: Fixed interval, Count: 3, Interval: PT30S — same as the Step 3 HTTP action, to handle transient 429/503 errors during pagination) - Set variable
allMessages=@{union(variables('allMessages'), body('HTTP_2')?['value'])}
Warning: Do NOT use "Append to array" here.
Append to arrayadds a single element, so it would create a nested array[[page1_msgs], [page2_msgs]]instead of a flat list. UseSet variablewithunion()to merge page results correctly.Action naming: If you already added an HTTP action in Step 3, Power Automate auto-renames this second HTTP action to
HTTP_2. The expressions above usebody('HTTP_2')to reflect this. If you delete the Step 3 HTTP action and use only the Do Until pattern, rename references back tobody('HTTP'). Always verify the action name in your flow matches the expressions.Loop Limits: Configure the Do Until's Limits settings (click the … menu > Settings on the Do Until action). Set Count to
60(maximum iterations) and Timeout toPT1H(1 hour). While these are Power Automate's defaults, setting them explicitly prevents runaway execution if the API returns malformed@odata.nextLinkvalues that never resolve to empty.
- Set variable
nextLink=@{coalesce(body('HTTP_2')?['@odata.nextLink'], '')} - Process
allMessagesin the Apply to each loop
Simplified Flow Diagram¶
┌─ Initialize nextLink = Graph URL
├─ Initialize allMessages = []
│
├─ Do Until (nextLink is empty)
│ ├─ HTTP GET nextLink
│ ├─ Set allMessages = union(allMessages, value)
│ └─ Set nextLink = @odata.nextLink (or empty)
│
└─ Apply to each (allMessages)
└─ Process message...
Note: For new deployments, this pattern is recommended. However, if you're processing daily and your tenant has fewer than 100 active posts, the basic single-request approach will work.
Parse JSON with Pagination¶
When using the pagination pattern above, the allMessages variable is a flat array of message objects (not wrapped in a { "value": [...] } envelope). If you want to validate the flat array with Parse JSON, use this schema instead:
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"category": { "type": "string" },
"severity": { "type": "string" },
"services": { "type": "array", "items": { "type": "string" } },
"startDateTime": { "type": "string" },
"endDateTime": { "type": ["string", "null"] },
"lastModifiedDateTime": { "type": "string" },
"isMajorChange": { "type": "boolean" },
"actionRequiredByDateTime": { "type": ["string", "null"] },
"body": {
"type": "object",
"properties": {
"contentType": { "type": "string" },
"content": { "type": "string" }
}
},
"tags": { "type": "array", "items": { "type": "string" } },
"hasAttachments": { "type": "boolean" }
}
}
}
Tip: Parse JSON is optional when using pagination—the
Apply to eachloop can directly iterate overallMessages. Use Parse JSON only if you want schema validation or IntelliSense for field names in subsequent actions.
Step 6: Loop Through Messages¶
Add action: Apply to each
⚠️ Keep execution sequential (default). Do not enable parallel execution ("Concurrency Control") on this Apply to each loop. The loop uses a non-atomic read-then-update pattern: it reads
notifiedOnfrom the upsert response, checks if null, then sends a Teams notification and updatesnotifiedOn. If parallel execution is enabled, two iterations could both readnotifiedOn == nullfor the same post before either writes the update, causing duplicate Teams notifications.
- Select an output from previous steps:
@{variables('allMessages')}(if using pagination) or@{body('Parse_JSON')?['value']}(basic)
Inside the loop, add these actions:
6a: Upsert to Dataverse¶
Recommended: Alternate Key Approach
The schema script scripts/create_mcm_dataverse_schema.py already provisions an alternate key (EntityKey on fsi_messagecenterid) when you run it with --output-docs. No manual key creation in the maker UI is required for tenants deployed via the script. If you deployed an earlier schema version manually, re-run the script to provision the key.
Use the Dataverse — Update or insert (upsert) a row action:
- Table: Message Center Logs (entity set
fsi_messagecenterlogs) - Alternate Key column:
fsi_messagecenterid - Alternate Key value:
@{items('Apply_to_each')?['id']}
The Web API resolves this to the canonical alternate-key URL:
This replaces the List + Condition logic.
Alternative: Manual Check Pattern
If you cannot use the upsert action (e.g., the alternate key has not propagated yet, or you are on a region that does not support the action), use this pattern:
- Add action: Dataverse - List rows
- Table: Message Center Logs
-
Filter:
fsi_messagecenterid eq '@{items('Apply_to_each')?['id']}' -
Add Condition: Check if row exists
- If yes: Update a row
- If no: Add a row
Duplicate prevention with List rows: When using this pattern instead of upsert, check the
notifiedOnfield from the List rows response in your notification condition:equals(first(outputs('List_rows')?['body/value'])?['fsi_notifiedon'], null). If no row exists yet (new post),first()returns null and the condition is satisfied, allowing the first notification.
Field mappings for Add/Update/Upsert:
Naming: Column shown is the logical name (lowercased, no underscores between words). The Power Automate column picker may display a different label (Display Name) — confirm the underlying logical name matches before saving.
| Dataverse Column | Expression |
|---|---|
fsi_messagecenterid |
@{items('Apply_to_each')?['id']} |
fsi_title |
@{items('Apply_to_each')?['title']} |
fsi_category |
Integer from Switch (see Choice Field Implementation with Switch below) |
fsi_severity |
Integer from Switch (see Choice Field Implementation with Switch below) |
fsi_services |
@{join(coalesce(items('Apply_to_each')?['services'], json('[]')), ', ')} |
fsi_startdatetime |
@{items('Apply_to_each')?['startDateTime']} |
fsi_actionrequiredbydatetime |
@{items('Apply_to_each')?['actionRequiredByDateTime']} |
fsi_lastmodifieddatetime |
@{items('Apply_to_each')?['lastModifiedDateTime']} |
fsi_enddatetime |
@{items('Apply_to_each')?['endDateTime']} |
fsi_ismajorchange |
@{items('Apply_to_each')?['isMajorChange']} |
fsi_body |
@{coalesce(items('Apply_to_each')?['body']?['content'], '')} |
fsi_tags |
@{join(coalesce(items('Apply_to_each')?['tags'], json('[]')), ', ')} |
fsi_hasattachments |
@{items('Apply_to_each')?['hasAttachments']} |
fsi_assessmentstatus |
100000000 (NotAssessed — set on insert only; do not overwrite on update) |
Severity and category mapping: These columns are Choice (Picklist) fields backed by global option sets. Dataverse rejects text labels — bind them to the integer values produced by the Switch blocks below.
6b: Choice Field Implementation with Switch¶
Category and severity columns are Dataverse Choice (Picklist) fields backed by global option sets — they reject text labels and require the integer values defined in create_mcm_dataverse_schema.py (and reflected in docs/dataverse-schema.md). Use a Switch action to map the Microsoft Graph API enum values to the option-set integers, then write the integer to the Choice column.
Category Switch (writes to fsi_category, option set fsi_MCM_messagecategory):
- Add action: Switch
- On:
@{items('Apply_to_each')?['category']} - Add cases:
- Case
planForChange: Set variablecategoryValue=100000000(Feature) - Case
stayInformed: Set variablecategoryValue=100000001(Admin) - Case
preventOrFixIssue: Set variablecategoryValue=100000002(Security) - Default: Set variable
categoryValue=100000001(Admin)
Severity Switch (writes to fsi_severity, option set fsi_MCM_messageseverity):
- Add action: Switch
- On:
@{items('Apply_to_each')?['severity']} - Add cases:
- Case
high: Set variableseverityValue=100000000(High) - Case
normal: Set variableseverityValue=100000001(Normal) - Case
critical: Set variableseverityValue=100000002(Critical) - Default: Set variable
severityValue=100000001(Normal)
The variables are typed as Integer so the Update/Upsert Row action can write them directly to the Choice column. See docs/dataverse-schema.md for the canonical option-set definitions, or create_mcm_dataverse_schema.py:OPTION_SETS for the source of truth.
Step 7: Teams Notification for High Severity¶
Inside the Apply to each loop, after the Dataverse action:
Add Condition to notify when action is truly needed:
Notification Condition (includes duplicate prevention):
Important: The flow runs daily and re-evaluates ALL posts. Without duplicate prevention, previously notified posts trigger alerts again on every run. The
fsi_notifiedoncolumn (defined indocs/dataverse-schema.md) tracks which posts have already been notified.Action naming: If you delete and re-add the upsert action, Power Automate may rename it to
Upsert_a_row_2. Verify the action name in any expression likeoutputs('Upsert_a_row')?['body/fsi_notifiedon']matches your flow's actual action name — otherwise the expression resolves tonulland duplicate prevention silently breaks.
Option A: Basic Check
@and(
equals(outputs('Upsert_a_row')?['body/fsi_notifiedon'], null),
contains(
split(toLower(<replace-with-fsi_MCM_NotifySeverities-env-var>), ','),
toLower(items('Apply_to_each')?['severity'])
)
)
The contains(split(<env-var>, ','), …) pattern reads the comma-separated severity list from the fsi_MCM_NotifySeverities environment variable (e.g., high,critical) instead of hard-coding literal 'high'/'critical' checks. Replace the <replace-with-fsi_MCM_NotifySeverities-env-var> token with whichever expression you use to retrieve the env var value (e.g., outputs('Get_Environment_Variable')?['body/value']).
Note: The
Upsert_a_rowresponse body returns the full Dataverse record, including the existingnotifiedOnvalue. Do not useitems('Apply_to_each')?['notifiedOn']— Graph API messages do not contain this field.
OR (visual editor equivalent):
In the condition editor, create the following structure:
- Row 1: @{outputs('Upsert_a_row')?['body/fsi_notifiedon']} is equal to null
- AND (click "Add group" → OR group):
- Row 2a: @{items('Apply_to_each')?['severity']} is equal to high
- OR
- Row 2b: @{items('Apply_to_each')?['severity']} is equal to critical
Grouping matters: You must nest the two severity rows inside an OR group, then AND that group with the null check. Without explicit grouping, the default evaluation would be
(notifiedOn == null AND severity == 'high') OR severity == 'critical', which bypasses duplicate prevention for critical-severity posts.
Option B: Refined Check (Recommended)
Use an expression to only notify when actionRequiredByDateTime is in the future:
@and(
equals(outputs('Upsert_a_row')?['body/fsi_notifiedon'], null),
or(
contains(
split(toLower(<replace-with-fsi_MCM_NotifySeverities-env-var>), ','),
toLower(items('Apply_to_each')?['severity'])
),
and(
not(equals(items('Apply_to_each')?['actionRequiredByDateTime'], null)),
greater(items('Apply_to_each')?['actionRequiredByDateTime'], utcNow())
)
)
)
This prevents notifications for posts with past deadlines and means each post triggers at most one notification.
After sending a Teams notification, add a Dataverse - Update a row action to set fsi_notifiedon to @{utcNow()}. This marks the post as notified so it won't trigger again on the next run. If you later want to re-notify (e.g., deadline approaching), reset fsi_notifiedon to null or add a separate reminder condition.
Optional Enhancement: Also check isMajorChange:
@and(
equals(outputs('Upsert_a_row')?['body/fsi_notifiedon'], null),
or(
contains(
split(toLower(<replace-with-fsi_MCM_NotifySeverities-env-var>), ','),
toLower(items('Apply_to_each')?['severity'])
),
equals(items('Apply_to_each')?['isMajorChange'], true),
and(
not(equals(items('Apply_to_each')?['actionRequiredByDateTime'], null)),
greater(items('Apply_to_each')?['actionRequiredByDateTime'], utcNow())
)
)
)
If yes:
Add action: Microsoft Teams - Post card in a chat or channel
Note: The action was previously called "Post adaptive card in a chat or channel" but has been renamed. If you see the old name in an existing flow, it should continue to work; new flows should use the current connector action name.
- Post as: Flow bot
- Post in: Channel
- Team: Your team
- Channel: Your alerts channel
- Adaptive Card: Use the template from teams-notification-card.json
Replace placeholders in the card with dynamic content:
- {title} → @{items('Apply_to_each')?['title']}
- {severity} → @{items('Apply_to_each')?['severity']}
- {category} → @{items('Apply_to_each')?['category']}
- {services} → @{join(coalesce(items('Apply_to_each')?['services'], json('[]')), ', ')}
- {startDateTime} → @{formatDateTime(items('Apply_to_each')?['startDateTime'], 'MMM dd, yyyy')}
- {actionRequiredByDateTime} → @{if(equals(items('Apply_to_each')?['actionRequiredByDateTime'], null), 'None', formatDateTime(items('Apply_to_each')?['actionRequiredByDateTime'], 'MMM dd, yyyy'))}
- {id} → @{items('Apply_to_each')?['id']}
- {recordId} → @{outputs('Upsert_a_row')?['body/fsi_messagecenterlogid']}
Step 8: Error Handling¶
Configure Run After¶
For the Apply to each action: 1. Click the three dots (...) > Configure run after 2. Ensure it runs after HTTP succeeds
Add Scope for Error Handling¶
Wrap the main processing logic in a Scope action for better error handling:
- Add Scope action (call it "Try")
- Move inside the Try scope:
- HTTP action (or Do Until loop for pagination)
- Parse JSON action
- Apply to each loop (including Dataverse upsert and Teams notification)
- Add another Scope after (call it "Catch")
- Configure "Catch" to run after "Try" has failed
- In "Catch", add a Teams notification for errors:
-
Post a message using this expression to include the error details and timestamp:
-
This enables faster operational triage without requiring manual navigation to flow run history.
Why include Apply to each in Try? If a single message fails to process (e.g., malformed data), the entire loop stops. Wrapping it in Try means you get notified of partial failures. For even more granular handling, you can add a nested Try/Catch inside the Apply to each loop to handle individual message failures without stopping the entire run.
Step 9: Save and Test¶
- Click Save
- Click Test > Manually > Test
- Wait for the flow to complete
- Check:
- Dataverse table has new rows
- Teams channel received notifications (if high-severity posts exist)
Complete Flow Structure¶
┌─ Recurrence (Daily at 9 AM)
│
├─ [Scope: Try]
│ ├─ Get secret (Azure Key Vault)
│ ├─ Initialize variables (nextLink, allMessages)
│ │
│ ├─ Do Until (nextLink is empty)
│ │ ├─ HTTP GET nextLink
│ │ ├─ Set allMessages = union(allMessages, value)
│ │ └─ Set nextLink = @odata.nextLink
│ │
│ ├─ Parse JSON (optional, for schema validation)
│ │
│ └─ Apply to each (allMessages)
│ ├─ Upsert row (alternate key)
│ │ OR
│ │ ├─ List rows (check if exists)
│ │ └─ Condition → Add or Update
│ │
│ └─ Condition (high severity OR future action required? AND not yet notified)
│ └─ Yes: Post adaptive card to Teams
│ Update notifiedOn = utcNow()
│
└─ [Scope: Catch] (runs on failure)
└─ Post error notification to Teams
Troubleshooting¶
HTTP action fails with 401¶
- Verify app registration has
ServiceMessage.Read.All - Confirm admin consent was granted
- Check tenant ID and client ID are correct
- Verify client secret hasn't expired
HTTP action fails with 403¶
ServiceMessage.Read.Allrequires Application permission, not Delegated- Admin consent must be granted by an administrator with permission to consent to enterprise applications
Parse JSON fails¶
- Check HTTP response for error messages
- Verify the schema matches the actual response
- Use "Generate from sample" with real API response
Dataverse actions fail¶
- Verify table and column names match exactly
- Check that choice values are mapped correctly
- Ensure the connection has sufficient permissions
Teams notifications not appearing¶
- Verify the Teams connector is properly authenticated
- Check that the team and channel exist
- Review the adaptive card JSON for syntax errors
Rate Limits¶
Microsoft Graph service communications API calls are throttled per tenant and application. Honor 429 responses and Retry-After headers; the retry policy above handles transient throttling for cloud flows.
Daily polling is well within typical operational needs. Even hourly polling is usually unnecessary because most Message Center posts are planning updates rather than urgent incident signals.
Environment Variables for ALM Portability¶
The steps above hardcode the polling schedule, severity thresholds, and Teams channel ID directly in the flow. For single-environment use this is fine, but when promoting the solution across environments (dev → test → prod), use Dataverse environment variables so values can be updated per-environment without editing the flow definition.
Recommended environment variables:
| Display Name | Schema Name | Type | Example Value |
|---|---|---|---|
| Polling Interval (days) | fsi_MCM_PollingIntervalDays |
Integer | 1 |
| Notification Severity Threshold | fsi_MCM_NotifySeverities |
Text | high,critical |
| Teams Channel ID | fsi_MCM_TeamsChannelId |
Text | 19:abc123@thread.tacv2 |
| Teams Team ID | fsi_MCM_TeamsTeamId |
Text | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| Dataverse URL | fsi_MCM_DataverseUrl |
Text | https://org.crm.dynamics.com |
| Key Vault Secret Name | fsi_MCM_KeyVaultSecretName |
Text | MessageCenterClientSecret |
Note: Create these in your solution via Solutions > your solution > Add > Environment variable. The env vars are also auto-provisioned by
scripts/create_mcm_environment_variables.py.
To reference an environment variable in the flow, use the Dataverse - List rows action to query the Environment Variable Values table, or use the @{outputs('Get_Environment_Variable')?['body/value']} pattern after adding a dedicated lookup action. This aligns with ALM best practices and simplifies managed solution deployments.
Optional Optimizations¶
Delta Tracking with lastModifiedDateTime¶
After your first sync, you can filter to only retrieve recently modified posts. This reduces processing time and API payload size.
Pattern:
- Store the timestamp of your last successful sync (options):
- Dataverse config table row
- Environment variable
-
Flow variable persisted to a file/SharePoint
-
On subsequent runs, filter the API call:
https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages?$filter=lastModifiedDateTime ge 2025-01-25T00:00:00Z
- Update the stored timestamp after successful processing
Expression for filter:
First Run vs. Subsequent Runs:
- First run: No filter, retrieve all posts
- Subsequent runs: Filter by
lastModifiedDateTime - Suggested: Also run a full sync weekly to catch any edge cases
Combining Optimizations¶
For busy tenants, combine pagination with delta tracking:
┌─ Get lastSyncTime from config
├─ Set API URL with $filter parameter
├─ Do Until (pagination loop)
│ └─ Process new/updated messages
└─ Update lastSyncTime in config
This approach minimizes both API calls and processing time while ensuring you don't miss any posts.