Long-Running Bulk Job Alert Monitor
Runs every 2 hours, checks for any Salesforce bulk jobs that have been in 'InProgress' state for more than 4 hours, and sends an alert to IT admins via Teams and email with job ID, object, operation type, and duration.
Provided as-is, without warranty of any kind. Review and test each pattern in a non-production environment before deploying it to live automations. See our Terms.
Overview
This scheduled cloud flow polls Salesforce for bulk data load jobs that have been stuck in a running state for longer than a configurable threshold. When stale jobs are found, it posts a formatted HTML table to a Microsoft Teams channel and sends a parallel email alert to the IT distribution list so someone can investigate and cancel the job before it burns API hours.
Built as part of the FlowLibs demo library to showcase: (1) periodic polling with time-window filtering, (2) Data Operations Filter Array + length() based conditional branching, (3) fan-out alerting to Teams and Outlook in parallel, and (4) fully env-var driven configuration with zero hardcoded identifiers.
Use Case
Salesforce bulk API v2 ingest jobs can silently hang when upstream data sources are slow or malformed. An analyst kicking off a nightly 2-million-record upsert expects it to finish in ~40 minutes; if it is still InProgress after 4 hours, something is wrong. Rather than having ops staff manually tail the Bulk Data Load Jobs screen, this flow watches the queue on a 2-hour cadence and raises an alert the moment a job crosses its defined age threshold. IT receives both a Teams post (for visibility during working hours) and an email (for after-hours paging) containing the job Id, operation type, object, age, and a direct link to the job record.
Flow Architecture
Recurrence
RecurrenceRuns on a schedule every 2 hours to poll Salesforce for long-running bulk jobs.
Initialize varThresholdHours
Initialize variableInteger variable pulled from env var flowlibs_SalesforceJobRunningThresholdHours. Default age (in hours) a job must exceed before being flagged.
Initialize varMonitoredState
Initialize variableString variable from env var flowlibs_SalesforceMonitoredJobState. The Salesforce state to watch (e.g. InProgress, UploadComplete).
Initialize varTeamsGroupId
Initialize variableString variable from env var flowlibs_AdminTeamsGroupId. Target Teams team GUID.
Initialize varTeamsChannelId
Initialize variableString variable from env var flowlibs_AdminTeamsChannelId. Target Teams channel Id.
Initialize varAlertRecipient
Initialize variableString variable from env var flowlibs_ITTeamEmail. Distribution list for email alerts.
Initialize varCutoffTimestamp
Initialize variableString variable computed via addHours(utcNow(), mul(variables('varThresholdHours'), -1)). Acts as the upper bound on systemModstamp for the filter (anything older than cutoff is stale).
List_All_Bulk_Jobs
Salesforce - GetAllJobsPulls every bulk job in the queue. No server-side filter so we can inspect state transitions across the full set.
Filter_Long_Running_Jobs
Filter arrayData Operations Filter Array. From: @body('List_All_Bulk_Jobs')?['records']. Query: @and(equals(item()?['state'], variables('varMonitoredState')), less(item()?['systemModstamp'], variables('varCutoffTimestamp'))).
Environment Variables
| Schema name | Type | Default | Description |
|---|---|---|---|
| flowlibs_SalesforceJobRunningThresholdHours | String | 4 | Default age in hours a job must exceed before being flagged as stale. Parsed as an integer and multiplied by -1 inside the cutoff expression — positive whole numbers only. |
| flowlibs_SalesforceMonitoredJobState | String | InProgress | The Salesforce bulk job state to watch. Valid values include Open, UploadComplete, InProgress, Aborted, JobComplete, Failed. |
| flowlibs_AdminTeamsGroupId | String | <configure> | Target Microsoft Teams team GUID where alerts should be posted. |
| flowlibs_AdminTeamsChannelId | String | <configure> | Target Teams channel Id (format like 19:...@thread.tacv2) where the alert message will be posted. |
| flowlibs_ITTeamEmail | String | alerts@yourcompany.com | Distribution list address that receives the parallel email alert (e.g. it-alerts@yourcompany.com). |
Connectors & Connections
| Connector | API name | Actions used |
|---|---|---|
| Salesforce | shared_salesforce | GetAllJobs (list bulk data load jobs) |
| Microsoft Teams | shared_teams | PostMessageToConversation |
| Office 365 Outlook | shared_office365 | SendEmailV2 |
Note — All connections are referenced as solution connection references; the flow is portable between environments as long as a connection is mapped at import time.
Customization Guide
Almost every realistic variant of this flow can be implemented by changing environment variable values. A few cases require small edits inside the flow definition — those are called out explicitly below.
- Change the polling cadence
- Edit the Recurrence trigger interval/frequency. For sub-hour polling be aware of Salesforce daily API limits.
- Watch a different Salesforce state
- Change the default of flowlibs_SalesforceMonitoredJobState in your environment. Valid values include Open, UploadComplete, InProgress, Aborted, JobComplete, Failed.
- Change the threshold
- Edit env var flowlibs_SalesforceJobRunningThresholdHours. The value is parsed as an integer and multiplied by -1 inside the cutoff expression, so positive whole numbers only.
- Add a cancellation step
- In the If-Yes branch, insert an Apply_to_each over body('Filter_Long_Running_Jobs') and call the Salesforce PatchJobState action with state: Aborted. This turns the flow from passive alert into active remediation — consider adding an approval before the cancel for safety.
- Route alerts elsewhere
- Replace the Teams post with a Slack PostMessage, or swap Outlook for Gmail. The HTML body will render in any modern mail/chat client.
- Suppress duplicate alerts
- Add a Dataverse table flowlibs_AlertedJob keyed on job Id and gate the alert branch on get row / add row to ensure each job only pages once per escalation window.
Key Expressions
The flow is intentionally light on Power Fx / WDL gymnastics — the heaviest expressions are the branch-name concatenation and the approval outcome check. They are listed below in the order they appear in the flow.
EXPR.01Cutoff timestamp (varCutoffTimestamp)
Computes the upper-bound timestamp for the filter: anything with a systemModstamp older than this is considered stale.
EXPR.02Filter Array query
Keeps only jobs that are in the monitored state AND have not had a systemModstamp update since the cutoff.
EXPR.03If condition
Branches the flow into the alert path only when the filter returns at least one stale job.
EXPR.04Select row template (value side)
Each stale job is projected into a single HTML table row inside the Select action's value field.
EXPR.05Compose HTML body (triple-replace unwrap)
The triple-replace pattern strips the JSON array wrapper from the Select output so the <tr> rows render as raw HTML inside the table.
Comments
Sign in to join the conversation.
Sign inNo comments yet. Be the first to share your experience with this flow.