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
PowerShell samples for Durable Functions aren't yet available for this scenario.
Java samples for Durable Functions aren't yet available for this scenario. See the Durable Task SDKs pivot.
- .NET 8.0 SDK or later
- Access to Azure Durable Task Scheduler or the local emulator
- Node.js 22 or later
- Access to Azure Durable Task Scheduler or the local emulator
- Python 3.9 or later
- Access to Azure Durable Task Scheduler or the local emulator
This sample is shown for .NET, JavaScript, Java, and Python.
- Java 11 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.
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.
This sample monitors the count of issues in a GitHub repo and alerts the user if there are more than 3 open issues. You could use a regular timer-triggered function to request the opened issue counts at regular intervals. However, one problem with this approach is lifetime management. If only one alert should be sent, the monitor needs to disable itself after 3 or more issues are detected.
PowerShell samples for Durable Functions aren't yet available for this scenario.
Java samples for Durable Functions aren't yet available for this scenario. See the Durable Task SDKs pivot.
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 Twilio integration
This sample involves using the Twilio service to send SMS messages to a mobile phone. Azure Functions already has support for Twilio via the Twilio binding, and the sample uses that feature.
The first thing you need is a Twilio account. You can create one free at https://www.twilio.com/try-twilio. Once you have an account, add the following three app settings to your function app.
| App setting name |
Value description |
| TwilioAccountSid |
The SID for your Twilio account |
| TwilioAuthToken |
The Auth token for your Twilio account |
| TwilioPhoneNumber |
The phone number associated with your Twilio account. This is used to send SMS messages. |
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). |
Configuring Twilio integration
This sample involves using the Twilio service to send SMS messages to a mobile phone. Azure Functions already has support for Twilio via the Twilio binding, and the sample uses that feature.
The first thing you need is a Twilio account. You can create one free at https://www.twilio.com/try-twilio. Once you have an account, add the following three app settings to your function app.
| App setting name |
Value description |
| TwilioAccountSid |
The SID for your Twilio account |
| TwilioAuthToken |
The Auth token for your Twilio account |
| TwilioPhoneNumber |
The phone number associated with your Twilio account. This is used to send SMS messages. |
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). |
Configuring Twilio integration
This sample involves using the Twilio service to send SMS messages to a mobile phone. Azure Functions already has support for Twilio via the Twilio binding, and the sample uses that feature.
The first thing you need is a Twilio account. You can create one free at https://www.twilio.com/try-twilio. Once you have an account, add the following three app settings to your function app.
| App setting name |
Value description |
| TwilioAccountSid |
The SID for your Twilio account |
| TwilioAuthToken |
The Auth token for your Twilio account |
| TwilioPhoneNumber |
The phone number associated with your Twilio account. This is used to send SMS messages. |
PowerShell samples for Durable Functions aren't yet available for this scenario.
Java samples for Durable Functions aren't yet available for this scenario. See the Durable Task SDKs pivot.
Orchestrator
[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.
The E3_Monitor function uses the standard function.json for orchestrator functions.
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}
Here's the code that implements the function:
const df = require("durable-functions");
const moment = require("moment");
module.exports = df.orchestrator(function* (context) {
const input = context.df.getInput();
context.log(
"Received monitor request. location: " +
(input ? input.location : undefined) +
". phone: " +
(input ? input.phone : undefined) +
"."
);
verifyRequest(input);
const endTime = moment.utc(context.df.currentUtcDateTime).add(6, "h");
context.log(
"Instantiating monitor for " +
input.location.city +
", " +
input.location.state +
". Expires: " +
endTime +
"."
);
while (moment.utc(context.df.currentUtcDateTime).isBefore(endTime)) {
// Check the weather
context.log(
"Checking current weather conditions for " +
input.location.city +
", " +
input.location.state +
" at " +
context.df.currentUtcDateTime +
"."
);
const isClear = yield context.df.callActivity("E3_GetIsClear", input.location);
if (isClear) {
// It's not raining! Or snowing. Or misting. Tell our user to take advantage of it.
context.log(
"Detected clear weather for " +
input.location.city +
", " +
input.location.state +
". Notifying " +
input.phone +
"."
);
yield context.df.callActivity("E3_SendGoodWeatherAlert", input.phone);
break;
} else {
// Wait for the next checkpoint
const nextCheckpoint = moment.utc(context.df.currentUtcDateTime).add(30, "s");
context.log(
"Next check for " +
input.location.city +
", " +
input.location.state +
" at " +
nextCheckpoint.toString()
);
yield context.df.createTimer(nextCheckpoint.toDate()); // accomodate cancellation tokens
}
}
context.log("Monitor expiring.");
});
function verifyRequest(request) {
if (!request) {
throw new Error("An input object is required.");
}
if (!request.location) {
throw new Error("A location input is required.");
}
if (!request.phone) {
throw new Error("A phone number input is required.");
}
}
The E3_Monitor function uses the standard function.json for orchestrator functions.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
Here's the code that implements the function:
import azure.durable_functions as df
from datetime import timedelta
from typing import Dict
def orchestrator_function(context: df.DurableOrchestrationContext):
monitoring_request: Dict[str, str] = context.get_input()
repo_url: str = monitoring_request["repo"]
phone: str = monitoring_request["phone"]
# Expiration of the repo monitoring
expiry_time = context.current_utc_datetime + timedelta(minutes=5)
while context.current_utc_datetime < expiry_time:
# Count the number of issues in the repo (the GitHub API caps at 30 issues per page)
too_many_issues = yield context.call_activity("E3_TooManyOpenIssues", repo_url)
# If we detect too many issues, we text the provided phone number
if too_many_issues:
# Extract URLs of GitHub issues, and return them
yield context.call_activity("E3_SendAlert", phone)
break
else:
# Reporting the number of statuses found
status = f"The repository does not have too many issues, for now ..."
context.set_custom_status(status)
# Schedule a new "wake up" signal
next_check = context.current_utc_datetime + timedelta(minutes=1)
yield context.create_timer(next_check)
return "Monitor completed!"
main = df.Orchestrator.create(orchestrator_function)
The orchestrator function periodically checks a condition and sends an alert if it's met. It uses a durable timer for polling intervals and continues until the monitor expires.
param($Context)
$input = $Context.Input | ConvertFrom-Json
$expirationTime = (Get-Date).AddHours(6)
$pollingInterval = New-TimeSpan -Seconds $input.pollingIntervalSeconds
while ((Get-Date) -lt $expirationTime) {
# Check current conditions
$isClear = Invoke-DurableActivity -FunctionName 'E3_GetIsClear' -Input $input.location
if ($isClear) {
# Condition met - send alert and exit
Invoke-DurableActivity -FunctionName 'E3_SendGoodWeatherAlert' -Input $input.phone
break
}
# Wait for the next polling interval
Start-DurableTimer -Duration $pollingInterval
}
@FunctionName("E3_Monitor")
public void monitorOrchestrator(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
MonitorRequest input = ctx.getInput(MonitorRequest.class);
Instant expirationTime = ctx.getCurrentInstant().plus(Duration.ofHours(6));
int pollingInterval = input.getPollingIntervalSeconds();
while (ctx.getCurrentInstant().isBefore(expirationTime)) {
// Check current conditions
boolean isClear = ctx.callActivity(
"E3_GetIsClear", input.getLocation(), boolean.class).await();
if (isClear) {
// Condition met - send alert and exit
ctx.callActivity("E3_SendGoodWeatherAlert", input.getPhone()).await();
break;
}
// Wait for the next polling interval
Instant nextCheck = ctx.getCurrentInstant().plus(
Duration.ofSeconds(pollingInterval));
ctx.createTimer(nextCheck).await();
}
}
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
};
}
}
import {
OrchestrationContext,
TOrchestrator,
} from "@microsoft/durabletask-js";
const monitorOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext,
input: { jobId: string; pollingIntervalSeconds: number; timeoutSeconds: number }
): any {
const { jobId, pollingIntervalSeconds, timeoutSeconds } = input;
const expirationTime = new Date(
ctx.currentUtcDateTime.getTime() + timeoutSeconds * 1000
);
let checkCount = 0;
while (ctx.currentUtcDateTime < expirationTime) {
// Check current job status
const jobStatus: any = yield ctx.callActivity(checkJobStatus, {
jobId,
checkCount,
});
checkCount = jobStatus.checkCount;
// Make job status available via custom status
ctx.setCustomStatus(jobStatus);
if (jobStatus.status === "Completed") {
return {
jobId,
finalStatus: "Completed",
checksPerformed: checkCount,
};
}
// Wait for next polling interval
yield ctx.createTimer(pollingIntervalSeconds);
}
// Timeout reached
return {
jobId,
finalStatus: "Timeout",
checksPerformed: checkCount,
};
};
import datetime
from durabletask import task
def monitoring_job_orchestrator(ctx: task.OrchestrationContext, job_data: dict) -> dict:
"""
Orchestrator that demonstrates the monitoring pattern.
Periodically checks the status of a job until it completes or times out.
"""
job_id = job_data.get("job_id")
polling_interval = job_data.get("polling_interval_seconds", 5)
timeout = job_data.get("timeout_seconds", 30)
# Record the start time
start_time = ctx.current_utc_datetime
expiration_time = start_time + datetime.timedelta(seconds=timeout)
# Initialize monitoring state
job_status = {
"job_id": job_id,
"status": "Unknown",
"check_count": 0
}
# Loop until the job completes or times out
while True:
# Check current job status
check_input = {"job_id": job_id, "check_count": job_status.get("check_count", 0)}
job_status = yield ctx.call_activity("check_job_status", input=check_input)
# Make the job status available via custom status
ctx.set_custom_status(job_status)
if job_status["status"] == "Completed":
break
# Check if we've hit the timeout
current_time = ctx.current_utc_datetime
if current_time >= expiration_time:
job_status["status"] = "Timeout"
break
# Calculate next check time
next_check_time = current_time + datetime.timedelta(seconds=polling_interval)
if next_check_time > expiration_time:
next_check_time = expiration_time
# Wait until next polling interval
yield ctx.create_timer(next_check_time)
# Return the final status
return {
"job_id": job_id,
"final_status": job_status["status"],
"checks_performed": job_status["check_count"]
}
This sample is shown for .NET, JavaScript, Java, and Python.
import com.microsoft.durabletask.*;
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
import java.time.Duration;
DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString)
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() { return "MonitoringJobOrchestrator"; }
@Override
public TaskOrchestration create() {
return ctx -> {
JobData jobData = ctx.getInput(JobData.class);
int pollingCount = 0;
// Set initial status
ctx.setCustomStatus(new JobStatus("Starting monitoring..."));
while (true) {
// Update status
ctx.setCustomStatus(new JobStatus(
"Polling job status (attempt " + (++pollingCount) + ")"));
// Wait for polling interval
ctx.createTimer(Duration.ofSeconds(jobData.pollingIntervalSeconds)).await();
// Check if job is complete (simulated after 3 attempts)
if (pollingCount >= 3) {
ctx.setCustomStatus(new JobStatus("Job completed successfully"));
ctx.complete(new JobResult(
"COMPLETED",
"Job completed after " + pollingCount + " attempts"));
break;
}
}
};
}
})
.build();
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
CreateTimer to 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);
}
The function.json is defined as follows:
{
"bindings": [
{
"name": "location",
"type": "activityTrigger",
"direction": "in"
}
],
"disabled": false
}
Here's the implementation.
const request = require("request");
const clearWeatherConditions = [
"Overcast",
"Clear",
"Partly Cloudy",
"Mostly Cloudy",
"Scattered Clouds",
];
module.exports = function (context, location) {
getCurrentConditions(location)
.then(function (data) {
const isClear = clearWeatherConditions.includes(data.weather);
context.done(null, isClear);
})
.catch(function (err) {
context.log(`E3_GetIsClear encountered an error: ${err}`);
context.done(err);
});
};
function getCurrentConditions(location) {
return new Promise(function (resolve, reject) {
const options = {
url: `https://api.wunderground.com/api/${process.env["WeatherUndergroundApiKey"]}/conditions/q/${location.state}/${location.city}.json`,
method: "GET",
json: true,
};
request(options, function (err, res, body) {
if (err) {
reject(err);
}
if (body.error) {
reject(body.error);
}
if (body.response.error) {
reject(body.response.error);
}
resolve(body.current_observation);
});
});
}
The E3_TooManyOpenIssues function gets a list of currently open issues on the repo and determines if there are "too many" of them: more than 3 as per the sample.
The function.json is defined as follows:
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "repoID",
"type": "activityTrigger",
"direction": "in"
}
]
}
Here's the implementation.
import requests
import json
def main(repoID: str) -> str:
# We use the GitHub API to count the number of open issues in the repo provided
# Note that the GitHub API only displays at most 30 issues per response, so
# the maximum number this activity will return is 30. That's enough for demo'ing purposes.
[user, repo] = repoID.split("/")
url = f"https://api.github.com/repos/{user}/{repo}/issues?state=open"
res = requests.get(url)
if res.status_code != 200:
error_message = f"Could not find repo {user} under {repo}! API endpoint hit was: {url}"
raise Exception(error_message)
issues = json.loads(res.text)
too_many_issues: bool = len(issues) >= 3
return too_many_issues
The E3_GetIsClear function checks whether the condition is met. In this example, it checks weather conditions for a location.
param($location)
# Call weather API to check current conditions
# In a real app, call an external API here
$conditions = Get-WeatherConditions -Location $location
$conditions -eq 'Clear'
The E3_GetIsClear function checks the current weather conditions for a location by using a weather API.
@FunctionName("E3_GetIsClear")
public boolean getIsClear(
@DurableActivityTrigger(name = "location") Location location) {
// Call weather API to check current conditions
String conditions = getWeatherConditions(location);
return conditions.equals("Clear");
}
Alert activity
The E3_SendGoodWeatherAlert function uses the Twilio binding to send an SMS message that notifies the end user that it's a good time for a walk.
[FunctionName("E3_SendGoodWeatherAlert")]
public static void SendGoodWeatherAlert(
[ActivityTrigger] string phoneNumber,
ILogger log,
[TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "%TwilioPhoneNumber%")]
out CreateMessageOptions message)
{
message = new CreateMessageOptions(new PhoneNumber(phoneNumber));
message.Body = $"The weather's clear outside! Go take a walk!";
}
internal class WeatherUnderground
{
private static readonly HttpClient httpClient = new HttpClient();
private static IReadOnlyDictionary<string, WeatherCondition> weatherMapping = new Dictionary<string, WeatherCondition>()
{
{ "Clear", WeatherCondition.Clear },
{ "Overcast", WeatherCondition.Clear },
{ "Cloudy", WeatherCondition.Clear },
{ "Clouds", WeatherCondition.Clear },
{ "Drizzle", WeatherCondition.Precipitation },
{ "Hail", WeatherCondition.Precipitation },
{ "Ice", WeatherCondition.Precipitation },
{ "Mist", WeatherCondition.Precipitation },
{ "Precipitation", WeatherCondition.Precipitation },
{ "Rain", WeatherCondition.Precipitation },
{ "Showers", WeatherCondition.Precipitation },
{ "Snow", WeatherCondition.Precipitation },
{ "Spray", WeatherCondition.Precipitation },
{ "Squall", WeatherCondition.Precipitation },
{ "Thunderstorm", WeatherCondition.Precipitation },
};
internal static async Task<WeatherCondition> GetCurrentConditionsAsync(Location location)
{
var apiKey = Environment.GetEnvironmentVariable("WeatherUndergroundApiKey");
if (string.IsNullOrEmpty(apiKey))
{
throw new InvalidOperationException("The WeatherUndergroundApiKey environment variable was not set.");
}
var callString = string.Format("http://api.wunderground.com/api/{0}/conditions/q/{1}/{2}.json", apiKey, location.State, location.City);
var response = await httpClient.GetAsync(callString);
var conditions = await response.Content.ReadAsAsync<JObject>();
JToken currentObservation;
if (!conditions.TryGetValue("current_observation", out currentObservation))
{
JToken error = conditions.SelectToken("response.error");
if (error != null)
{
throw new InvalidOperationException($"API returned an error: {error}.");
}
else
{
throw new ArgumentException("Could not find weather for this location. Try being more specific.");
}
}
return MapToWeatherCondition((string)(currentObservation as JObject).GetValue("weather"));
}
private static WeatherCondition MapToWeatherCondition(string weather)
{
foreach (var pair in weatherMapping)
{
if (weather.Contains(pair.Key))
{
return pair.Value;
}
}
return WeatherCondition.Other;
}
}
Note
To run the sample code, install the Microsoft.Azure.WebJobs.Extensions.Twilio NuGet package.
Its function.json is simple:
{
"bindings": [
{
"name": "phoneNumber",
"type": "activityTrigger",
"direction": "in"
},
{
"type": "twilioSms",
"name": "message",
"from": "%TwilioPhoneNumber%",
"accountSidSetting": "TwilioAccountSid",
"authTokenSetting": "TwilioAuthToken",
"direction": "out"
}
],
"disabled": false
}
And here's the code that sends the SMS message:
module.exports = function (context, phoneNumber) {
context.bindings.message = {
body: `The weather's clear outside! Go take a walk!`,
to: phoneNumber,
};
context.done();
};
The E3_SendAlert function uses the Twilio binding to send an SMS message notifying the end user that there are at least 3 open issues awaiting a resolution.
Its function.json is simple:
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "repoID",
"type": "activityTrigger",
"direction": "in"
}
]
}
And here's the code that sends the SMS message:
import json
import random
random.seed(10)
def main(phoneNumber: str, message):
payload = {
"body": f"Hey! You may want to check on your repo, there are too many open issues",
"to": phoneNumber
}
message.set(json.dumps(payload))
return "Message sent!"
The E3_SendGoodWeatherAlert function sends an SMS notification to the user.
param($phoneNumber)
# Send an SMS alert using your preferred messaging service
Write-Host "Sending good weather alert to $phoneNumber."
# In a real app, use Twilio or another SMS provider here
The E3_SendGoodWeatherAlert function sends an SMS notification to the user.
@FunctionName("E3_SendGoodWeatherAlert")
public void sendGoodWeatherAlert(
@DurableActivityTrigger(name = "phoneNumber") String phoneNumber) {
// Send an SMS alert using your preferred messaging service
sendSms(phoneNumber, "The weather is clear outside! Enjoy your day.");
}
The activity checks the current status of the job. In a real application, this step calls an external API or service.
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
[DurableTask(nameof(CheckJobStatusActivity))]
public class CheckJobStatusActivity : TaskActivity<CheckJobInput, JobStatus>
{
private readonly ILogger<CheckJobStatusActivity> _logger;
public CheckJobStatusActivity(ILogger<CheckJobStatusActivity> logger)
{
_logger = logger;
}
public override Task<JobStatus> RunAsync(TaskActivityContext context, CheckJobInput input)
{
_logger.LogInformation("Checking status for job: {JobId} (check #{CheckCount})",
input.JobId, input.CheckCount + 1);
// Simulate job status - completes after 3 checks
var status = input.CheckCount >= 3 ? "Completed" : "Running";
return Task.FromResult(new JobStatus
{
JobId = input.JobId,
Status = status,
CheckCount = input.CheckCount + 1,
LastCheckTime = DateTime.UtcNow
});
}
}
// Data classes
public class JobMonitorInput
{
public string JobId { get; set; }
public int PollingIntervalSeconds { get; set; } = 5;
public int TimeoutSeconds { get; set; } = 30;
}
public class CheckJobInput
{
public string JobId { get; set; }
public int CheckCount { get; set; }
}
public class JobStatus
{
public string JobId { get; set; }
public string Status { get; set; }
public int CheckCount { get; set; }
public DateTime LastCheckTime { get; set; }
}
public class JobMonitorResult
{
public string JobId { get; set; }
public string FinalStatus { get; set; }
public int ChecksPerformed { get; set; }
}
import { ActivityContext } from "@microsoft/durabletask-js";
const checkJobStatus = async (
_ctx: ActivityContext,
input: { jobId: string; checkCount: number }
): Promise<any> => {
console.log(
`Checking status for job: ${input.jobId} (check #${input.checkCount + 1})`
);
// Simulate job status — completes after 3 checks
const status = input.checkCount >= 3 ? "Completed" : "Running";
return {
jobId: input.jobId,
status,
checkCount: input.checkCount + 1,
lastCheckTime: new Date().toISOString(),
};
};
import datetime
from durabletask import task
def check_job_status(ctx: task.ActivityContext, job_data: dict) -> dict:
"""
Activity that checks the status of a long-running job.
In a real application, this would call an external API or service.
"""
job_id = job_data.get("job_id", "unknown")
check_count = job_data.get("check_count", 0)
# Simulate job status - completes after 3 checks
if check_count >= 3:
status = "Completed"
else:
status = "Running"
return {
"job_id": job_id,
"status": status,
"check_count": check_count + 1,
"last_check_time": datetime.datetime.now().isoformat()
}
This sample is shown for .NET, JavaScript, Java, and Python.
In the Java sample, the orchestrator directly simulates the status check. In a real application, you would create a separate activity:
.addActivity(new TaskActivityFactory() {
@Override
public String getName() { return "CheckJobStatus"; }
@Override
public TaskActivity create() {
return ctx -> {
JobCheckInput input = ctx.getInput(JobCheckInput.class);
// Simulate checking job status
// In a real app, this would call an external API
String status = input.checkCount >= 3 ? "Completed" : "Running";
return new JobStatus(status, input.checkCount + 1);
};
}
})
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" }
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" }
You need a GitHub account. Create a temporary public repository that you can open issues to.
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
{ "repo": "<your GitHub handle>/<a new GitHub repo under your user>", "phone": "+1425XXXXXXX" }
For example, if your GitHub username is foo and your repository is bar, set the value for "repo" to "foo/bar".
By using the HTTP-triggered function included in the sample, you can start the orchestration by sending the following HTTP POST request:
POST https://{host}/api/orchestrators/E3_Monitor
Content-Type: application/json
{ "location": { "city": "Redmond", "state": "WA" }, "phone": "+1425XXXXXXX" }
By using the HTTP-triggered function included in the sample, you can start the orchestration by sending the following HTTP POST request:
POST https://{host}/api/StartMonitor
Content-Type: application/json
{ "location": { "city": "Redmond", "state": "WA" }, "phone": "+1425XXXXXXX" }
The HTTP trigger function schedules the orchestration:
@FunctionName("StartMonitor")
public HttpResponseMessage startMonitor(
@HttpTrigger(name = "req", methods = {HttpMethod.POST}) HttpRequestMessage<Optional<String>> req,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext,
final ExecutionContext context) {
DurableTaskClient client = durableContext.getClient();
String instanceId = client.scheduleNewOrchestrationInstance("E3_Monitor", req.getBody().get());
context.getLogger().info("Started monitor orchestration with ID = " + instanceId);
return durableContext.createCheckStatusResponse(req, instanceId);
}
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:latest
Start 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);
}
import {
DurableTaskAzureManagedClientBuilder,
DurableTaskAzureManagedWorkerBuilder,
} from "@microsoft/durabletask-js-azuremanaged";
const client = new DurableTaskAzureManagedClientBuilder()
.connectionString(connectionString)
.build();
const worker = new DurableTaskAzureManagedWorkerBuilder()
.connectionString(connectionString)
.addOrchestrator(monitorOrchestrator)
.addActivity(checkJobStatus)
.build();
await worker.start();
// Schedule the monitoring orchestration
const input = {
jobId: `job-${Date.now()}`,
pollingIntervalSeconds: 5,
timeoutSeconds: 30,
};
const instanceId = await client.scheduleNewOrchestration(
monitorOrchestrator,
input
);
console.log(`Started monitoring orchestration: ${instanceId}`);
// Wait for completion
const result = await client.waitForOrchestrationCompletion(
instanceId,
true,
60
);
console.log(`Final result: ${result?.serializedOutput}`);
await worker.stop();
await client.stop();
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
import time
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub,
token_credential=credential
)
# Schedule the monitoring orchestration
job_data = {
"job_id": "job-123",
"polling_interval_seconds": 5,
"timeout_seconds": 30
}
instance_id = client.schedule_new_orchestration(
monitoring_job_orchestrator,
input=job_data
)
print(f"Started monitoring orchestration: {instance_id}")
# Wait for completion
result = client.wait_for_orchestration_completion(instance_id, timeout=60)
print(f"Final result: {result.serialized_output}")
This sample is shown for .NET, JavaScript, Java, and Python.
import java.time.Duration;
import java.util.UUID;
DurableTaskClient client = DurableTaskSchedulerClientExtensions
.createClientBuilder(connectionString).build();
// Schedule the monitoring orchestration
JobData jobData = new JobData(
"job-" + UUID.randomUUID().toString(),
5, // polling interval seconds
30 // timeout seconds
);
String instanceId = client.scheduleNewOrchestrationInstance(
"MonitoringJobOrchestrator",
new NewOrchestrationInstanceOptions().setInput(jobData));
System.out.println("Started monitoring orchestration: " + instanceId);
// Wait for completion
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofSeconds(60), true);
System.out.println("Final result: " + result.readOutputAs(JobResult.class).status);
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: