Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The monitor pattern is a recurring process in a workflow that polls an external system until a condition is met. For example, it checks job status until it completes, or watches weather data until skies are clear. Unlike a fixed-schedule timer trigger, a monitor waits between iterations (avoiding overlap), supports dynamic intervals, and can terminate itself once the condition is satisfied or a timeout expires.
This article explains how to implement the monitor pattern by using durable orchestrations.
Tip
This article shows the complete implementation. For a conceptual overview of durable orchestration use cases, see What is Durable Task?
The Durable Functions examples include a weather monitoring scenario (C#/JavaScript) and a GitHub issue monitoring scenario (Python).
Note
Version 4 of the Node.js programming model for Azure Functions is generally available. The v4 model is designed to provide a more flexible and intuitive experience for JavaScript and TypeScript developers. For more information about the differences between v3 and v4, see the migration guide.
In the following code snippets, JavaScript (PM4) denotes programming model v4, the new experience.
The Durable Task SDKs example demonstrates job status monitoring with configurable polling intervals by using .NET, JavaScript, Python, and Java.
Prerequisites
- .NET 8.0 SDK or later
- Access to Azure Durable Task Scheduler or the local emulator
Monitor scenario overview
This sample monitors a location's current weather conditions and alerts a user by SMS when the skies are clear. You could use a regular timer-triggered function to check the weather and send alerts. However, one problem with this approach is lifetime management. If only one alert should be sent, the monitor needs to disable itself after clear weather is detected.
The monitoring pattern can end its own execution, among other benefits:
- Monitors run on intervals, not schedules: a timer trigger runs every hour; a monitor waits one hour between actions. A monitor's actions don't overlap unless you specify otherwise, which can be important for long-running tasks.
- Monitors can have dynamic intervals: the wait time can change based on some condition.
- Monitors can terminate when some condition is met or be terminated by another process.
- Monitors can take parameters. The sample shows how the same monitoring process can be applied to any requested location, phone number, or repository.
- Monitors are scalable. Because each monitor is an orchestration instance, you can create multiple monitors without having to create new functions or define more code.
- Monitors integrate easily into larger workflows. A monitor can be one section of a more complex orchestration function, or a sub-orchestration.
This sample monitors the status of a long-running job and returns the final result when the job completes or times out. You could use a regular polling loop to check job status, but this approach has limitations around lifetime management and reliability.
The monitoring pattern provides these benefits:
- Durable polling: The orchestration survives process restarts and can continue monitoring even after failures.
- Configurable intervals: You can adjust the wait time between status checks dynamically.
- Timeout support: The monitor can terminate when a condition is met or a timeout expires.
- Status visibility: Clients can query the orchestration's custom status to see current monitoring progress.
- Scalability: Multiple monitors can run concurrently, each tracking different jobs.
Configuration
Configuring a weather API
The C#/JavaScript samples call a weather API to check current conditions. You need to provide your own weather API key and update the sample code accordingly. The sample code references a WeatherUndergroundApiKey app setting - replace this key with your chosen weather provider's key.
| App setting name | Value description |
|---|---|
| WeatherUndergroundApiKey | Your weather API key (replace with your provider's key name as needed). |
Orchestrator
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
namespace VSSample;
public static partial class Monitor
{
[Function("E3_Monitor")]
public static async Task Run(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
MonitorRequest input = context.GetInput<MonitorRequest>()
?? throw new ArgumentNullException(nameof(context), "An input object is required.");
VerifyRequest(input);
ILogger logger = context.CreateReplaySafeLogger("E3_Monitor");
DateTime endTime = context.CurrentUtcDateTime.AddHours(6);
logger.LogInformation(
"Instantiating monitor for {Location}. Expires: {EndTime}.",
input.Location,
endTime);
while (context.CurrentUtcDateTime < endTime)
{
logger.LogInformation(
"Checking current weather conditions for {Location} at {CurrentTime}.",
input.Location,
context.CurrentUtcDateTime);
bool isClear = await context.CallActivityAsync<bool>(
"E3_GetIsClear",
input.Location);
if (isClear)
{
await context.CallActivityAsync(
"E3_SendGoodWeatherAlert",
input.Phone);
break;
}
DateTime nextCheckpoint = context.CurrentUtcDateTime.AddMinutes(30);
await context.CreateTimer(nextCheckpoint, CancellationToken.None);
}
logger.LogInformation("Monitor expiring.");
}
private static void VerifyRequest(MonitorRequest request)
{
ArgumentNullException.ThrowIfNull(request.Location);
ArgumentException.ThrowIfNullOrEmpty(request.Phone);
}
}
public sealed class MonitorRequest
{
public required Location Location { get; init; }
public required string Phone { get; init; }
}
public sealed class Location
{
public required string State { get; init; }
public required string City { get; init; }
public override string ToString() => $"{City}, {State}";
}
[FunctionName("E3_Monitor")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext monitorContext, ILogger log)
{
MonitorRequest input = monitorContext.GetInput<MonitorRequest>();
if (!monitorContext.IsReplaying) { log.LogInformation($"Received monitor request. Location: {input?.Location}. Phone: {input?.Phone}."); }
VerifyRequest(input);
DateTime endTime = monitorContext.CurrentUtcDateTime.AddHours(6);
if (!monitorContext.IsReplaying) { log.LogInformation($"Instantiating monitor for {input.Location}. Expires: {endTime}."); }
while (monitorContext.CurrentUtcDateTime < endTime)
{
// Check the weather
if (!monitorContext.IsReplaying) { log.LogInformation($"Checking current weather conditions for {input.Location} at {monitorContext.CurrentUtcDateTime}."); }
bool isClear = await monitorContext.CallActivityAsync<bool>("E3_GetIsClear", input.Location);
if (isClear)
{
// It's not raining! Or snowing. Or misting. Tell our user to take advantage of it.
if (!monitorContext.IsReplaying) { log.LogInformation($"Detected clear weather for {input.Location}. Notifying {input.Phone}."); }
await monitorContext.CallActivityAsync("E3_SendGoodWeatherAlert", input.Phone);
break;
}
else
{
// Wait for the next checkpoint
var nextCheckpoint = monitorContext.CurrentUtcDateTime.AddMinutes(30);
if (!monitorContext.IsReplaying) { log.LogInformation($"Next check for {input.Location} at {nextCheckpoint}."); }
await monitorContext.CreateTimer(nextCheckpoint, CancellationToken.None);
}
}
log.LogInformation($"Monitor expiring.");
}
[Deterministic]
private static void VerifyRequest(MonitorRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request), "An input object is required.");
}
if (request.Location == null)
{
throw new ArgumentNullException(nameof(request.Location), "A location input is required.");
}
if (string.IsNullOrEmpty(request.Phone))
{
throw new ArgumentNullException(nameof(request.Phone), "A phone number input is required.");
}
}
The orchestrator function requires a location to monitor and a phone number to send a message to when the weather becomes clear at the location. You pass this data to the orchestrator function as a strongly typed MonitorRequest object.
This orchestrator function performs the following actions:
- Gets the MonitorRequest consisting of the location to monitor and the phone number to which it sends an SMS notification (or repo for the Python example).
- Determines the expiration time of the monitor. The sample uses a hard-coded value for brevity.
- Calls the status-checking activity to determine whether the condition is met.
- If the condition is met, calls the alert activity to send a notification.
- Creates a durable timer to resume the orchestration at the next polling interval. The sample uses a hard-coded value for brevity.
- Continues running until the current UTC time passes the monitor's expiration time, or an alert is sent.
You can run multiple orchestrator function instances simultaneously by calling the orchestrator function multiple times. You can specify the location to monitor and the phone number to send an alert to. The orchestrator function isn't running while waiting for the timer, so you aren't charged for it.
The orchestrator periodically checks the status of a job and returns when the job completes or times out.
using Microsoft.DurableTask;
using System;
using System.Threading.Tasks;
[DurableTask(nameof(MonitoringJobOrchestration))]
public class MonitoringJobOrchestration : TaskOrchestrator<JobMonitorInput, JobMonitorResult>
{
public override async Task<JobMonitorResult> RunAsync(
TaskOrchestrationContext context, JobMonitorInput input)
{
var jobId = input.JobId;
var pollingInterval = TimeSpan.FromSeconds(input.PollingIntervalSeconds);
var expirationTime = context.CurrentUtcDateTime.AddSeconds(input.TimeoutSeconds);
// Initialize monitoring state
int checkCount = 0;
while (context.CurrentUtcDateTime < expirationTime)
{
// Check current job status
var jobStatus = await context.CallActivityAsync<JobStatus>(
nameof(CheckJobStatusActivity),
new CheckJobInput { JobId = jobId, CheckCount = checkCount });
checkCount = jobStatus.CheckCount;
// Make job status available via custom status
context.SetCustomStatus(jobStatus);
if (jobStatus.Status == "Completed")
{
return new JobMonitorResult
{
JobId = jobId,
FinalStatus = "Completed",
ChecksPerformed = checkCount
};
}
// Calculate next check time
var nextCheck = context.CurrentUtcDateTime.Add(pollingInterval);
if (nextCheck > expirationTime)
{
nextCheck = expirationTime;
}
// Wait until next polling interval
await context.CreateTimer(nextCheck, default);
}
// Timeout reached
return new JobMonitorResult
{
JobId = jobId,
FinalStatus = "Timeout",
ChecksPerformed = checkCount
};
}
}
This orchestrator performs the following actions:
- Takes the job ID, polling interval, and timeout as input parameters.
- Records the start time and calculates the expiration time.
- Enters a polling loop that checks the job status.
- Updates the custom status so clients can monitor progress.
- If the job completes, returns the final result.
- If the timeout is reached, returns a timeout status.
- Uses
CreateTimerto wait between polling attempts without consuming resources.
Activities
As with other samples, the helper activity functions are regular functions that use the activityTrigger trigger binding.
Status checking activity
The E3_GetIsClear function gets the current weather conditions by using the Weather Underground API and determines whether the sky is clear.
[FunctionName("E3_GetIsClear")]
public static async Task<bool> GetIsClear([ActivityTrigger] Location location)
{
var currentConditions = await WeatherUnderground.GetCurrentConditionsAsync(location);
return currentConditions.Equals(WeatherCondition.Clear);
}
Run the monitor sample
By using the HTTP-triggered functions included in the sample, you can start the orchestration by sending the following HTTP POST request:
POST https://{host}/orchestrators/E3_Monitor
Content-Length: 77
Content-Type: application/json
{ "location": { "city": "Redmond", "state": "WA" }, "phone": "+1425XXXXXXX" }
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
Location: https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635?taskHub=SampleHubVS&connection=Storage&code={SystemKey}
RetryAfter: 10
{"id": "f6893f25acf64df2ab53a35c09d52635", "statusQueryGetUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635?taskHub=SampleHubVS&connection=Storage&code={systemKey}", "sendEventPostUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/raiseEvent/{eventName}?taskHub=SampleHubVS&connection=Storage&code={systemKey}", "terminatePostUri": "https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/terminate?reason={text}&taskHub=SampleHubVS&connection=Storage&code={systemKey}"}
The E3_Monitor instance starts and queries the current conditions. If the condition is met, it calls an activity function to send an alert; otherwise, it sets a timer. When the timer expires, the orchestration resumes.
You can see the orchestration's activity by looking at the function logs in the Azure Functions portal.
The orchestration completes once its timeout is reached or the condition is detected. You can also use the terminate API inside another function or invoke the terminatePostUri HTTP POST webhook referenced in the preceding 202 response. To use the webhook, replace {text} with the reason for the early termination. The HTTP POST URL looks roughly as follows:
POST https://{host}/runtime/webhooks/durabletask/instances/f6893f25acf64df2ab53a35c09d52635/terminate?reason=Because&taskHub=SampleHubVS&connection=Storage&code={systemKey}
To run the sample, you need:
Start the Durable Task Scheduler emulator (for local development):
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latestStart the worker to register the orchestrator and activities.
Run the client to schedule a monitoring orchestration.
using System;
using System.Threading.Tasks;
var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build();
// Schedule the monitoring orchestration
var input = new JobMonitorInput
{
JobId = "job-" + Guid.NewGuid().ToString(),
PollingIntervalSeconds = 5,
TimeoutSeconds = 30
};
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(MonitoringJobOrchestration), input);
Console.WriteLine($"Started monitoring orchestration: {instanceId}");
// Wait for completion while checking status
while (true)
{
var state = await client.GetInstanceMetadataAsync(instanceId, getInputsAndOutputs: true);
if (state.RuntimeStatus == OrchestrationRuntimeStatus.Completed ||
state.RuntimeStatus == OrchestrationRuntimeStatus.Failed)
{
Console.WriteLine($"Monitoring completed: {state.ReadOutputAs<JobMonitorResult>().FinalStatus}");
break;
}
Console.WriteLine($"Current status: {state.ReadCustomStatusAs<JobStatus>()?.Status}");
await Task.Delay(2000);
}
Next steps
This sample demonstrates how to use Durable Functions to monitor an external source's status by using durable timers and conditional logic. The next sample shows how to use external events and durable timers to handle human interaction.
This sample demonstrated how to use the Durable Task SDKs to implement the monitoring pattern with durable timers and status tracking. To learn more about other patterns and features, see: