Custom orchestration status in Durable Functions (Azure Functions)
Article
Custom orchestration status lets you set a custom status value for your orchestrator function. This status is provided via the HTTP GetStatus API or the equivalent SDK API on the orchestration client object.
Sample use cases
Visualize progress
Clients can poll the status end point and display a progress UI that visualizes the current execution stage. The following sample demonstrates progress sharing:
These C# examples are written for Durable Functions 2.x and are not compatible with Durable Functions 1.x. For more information about the differences between versions, see the Durable Functions versions article.
[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
[DurableClient] IDurableOrchestrationClient starter,
string functionName,
ILogger log)
{
// Function input comes from the request content.
dynamic eventData = await req.Content.ReadAsAsync<object>();
string instanceId = await starter.StartNewAsync(functionName, (string)eventData);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
DurableOrchestrationStatus durableOrchestrationStatus = await starter.GetStatusAsync(instanceId);
while (durableOrchestrationStatus.CustomStatus.ToString() != "London")
{
await Task.Delay(200);
durableOrchestrationStatus = await starter.GetStatusAsync(instanceId);
}
HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(durableOrchestrationStatus))
};
return httpResponseMessage;
}
}
const df = require("durable-functions");
module.exports = async function(context, req) {
const client = df.getClient(context);
// Function input comes from the request content.
const eventData = req.body;
const instanceId = await client.startNew(req.params.functionName, undefined, eventData);
context.log(`Started orchestration with ID = '${instanceId}'.`);
let durableOrchestrationStatus = await client.getStatus(instanceId);
while (durableOrchestrationStatus.customStatus.toString() !== "London") {
await new Promise((resolve) => setTimeout(resolve, 200));
durableOrchestrationStatus = await client.getStatus(instanceId);
}
const httpResponseMessage = {
status: 200,
body: JSON.stringify(durableOrchestrationStatus),
};
return httpResponseMessage;
};
Note
In JavaScript, the customStatus field will be set when the next yield or return action is scheduled.
import json
import logging
import azure.functions as func
import azure.durable_functions as df
from time import sleep
async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse:
client = df.DurableOrchestrationClient(starter)
instance_id = await client.start_new(req.params.functionName, None, None)
logging.info(f"Started orchestration with ID = '{instance_id}'.")
durable_orchestration_status = await client.get_status(instance_id)
while durable_orchestration_status.custom_status != 'London':
sleep(0.2)
durable_orchestration_status = await client.get_status(instance_id)
return func.HttpResponse(body='Success', status_code=200, mimetype='application/json')
Note
In Python, the custom_status field will be set when the next yield or return action is scheduled.
The feature is not currently implemented in PowerShell
@FunctionName("StartHelloCities")
public HttpResponseMessage startHelloCities(
@HttpTrigger(name = "req") HttpRequestMessage<Void> req,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext,
final ExecutionContext context) throws InterruptedException {
DurableTaskClient client = durableContext.getClient();
String instanceId = client.scheduleNewOrchestrationInstance("HelloCities");
context.getLogger().info("Created new Java orchestration with instance ID = " + instanceId);
OrchestrationMetadata metadata = client.waitForInstanceStart(instanceId, Duration.ofMinutes(5), true);
while (metadata.readCustomStatusAs(String.class) != "London") {
Thread.sleep(200);
metadata = client.getInstanceMetadata(instanceId, true);
}
return req.createResponseBuilder(HttpStatus.OK).build();
}
Output customization
Another interesting scenario is segmenting users by returning customized output based on unique characteristics or interactions. With the help of custom orchestration status, the client-side code will stay generic. All main modifications will happen on the server side as shown in the following sample:
[FunctionName("CityRecommender")]
public static void Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
int userChoice = context.GetInput<int>();
switch (userChoice)
{
case 1:
context.SetCustomStatus(new
{
recommendedCities = new[] {"Tokyo", "Seattle"},
recommendedSeasons = new[] {"Spring", "Summer"}
});
break;
case 2:
context.SetCustomStatus(new
{
recommendedCities = new[] {"Seattle, London"},
recommendedSeasons = new[] {"Summer"}
});
break;
case 3:
context.SetCustomStatus(new
{
recommendedCities = new[] {"Tokyo, London"},
recommendedSeasons = new[] {"Spring", "Summer"}
});
break;
}
// Wait for user selection and refine the recommendation
}
CityRecommender orchestrator
const df = require("durable-functions");
module.exports = df.orchestrator(function*(context) {
const userChoice = context.df.getInput();
switch (userChoice) {
case 1:
context.df.setCustomStatus({
recommendedCities: [ "Tokyo", "Seattle" ],
recommendedSeasons: [ "Spring", "Summer" ],
});
break;
case 2:
context.df.setCustomStatus({
recommendedCities: [ "Seattle", "London" ],
recommendedSeasons: [ "Summer" ],
});
break;
case 3:
context.df.setCustomStatus({
recommendedCity: [ "Tokyo", "London" ],
recommendedSeasons: [ "Spring", "Summer" ],
});
break;
}
// Wait for user selection and refine the recommendation
});
CityRecommender orchestrator
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
userChoice = int(context.get_input())
if userChoice == 1:
context.set_custom_status({
'recommendedCities' : ['Tokyo', 'Seattle'],
'recommendedSeasons' : ['Spring', 'Summer']
}))
if userChoice == 2:
context.set_custom_status({
'recommendedCities' : ['Seattle', 'London']
'recommendedSeasons' : ['Summer']
}))
if userChoice == 3:
context.set_custom_status({
'recommendedCities' : ['Tokyo', 'London'],
'recommendedSeasons' : ['Spring', 'Summer']
}))
# Wait for user selection and refine the recommendation
main = df.Orchestrator.create(orchestrator_function)
CityRecommender orchestrator
param($Context)
$userChoice = $Context.Input -as [int]
if ($userChoice -eq 1) {
Set-DurableCustomStatus -CustomStatus @{ recommendedCities = @('Tokyo', 'Seattle');
recommendedSeasons = @('Spring', 'Summer')
}
}
if ($userChoice -eq 2) {
Set-DurableCustomStatus -CustomStatus @{ recommendedCities = @('Seattle', 'London');
recommendedSeasons = @('Summer')
}
}
if ($userChoice -eq 3) {
Set-DurableCustomStatus -CustomStatus @{ recommendedCities = @('Tokyo', 'London');
recommendedSeasons = @('Spring', 'Summer')
}
}
# Wait for user selection and refine the recommendation
@FunctionName("CityRecommender")
public String cityRecommender(
@DurableOrchestrationTrigger(name = "runtimeState") String runtimeState) {
return OrchestrationRunner.loadAndRun(runtimeState, ctx -> {
int userChoice = ctx.getInput(int.class);
switch (userChoice) {
case 1:
ctx.setCustomStatus(new Recommendation(
new String[]{ "Tokyo", "Seattle" },
new String[]{ "Spring", "Summer" }));
break;
case 2:
ctx.setCustomStatus(new Recommendation(
new String[]{ "Seattle", "London" },
new String[]{ "Summer" }));
break;
case 3:
ctx.setCustomStatus(new Recommendation(
new String[]{ "Tokyo", "London" },
new String[]{ "Spring", "Summer" }));
break;
}
// Wait for user selection with an external event handler
});
}
class Recommendation {
public Recommendation() { }
public Recommendation(String[] cities, String[] seasons) {
this.recommendedCities = cities;
this.recommendedSeasons = seasons;
}
public String[] recommendedCities;
public String[] recommendedSeasons;
}
Instruction specification
The orchestrator can provide unique instructions to the clients via the custom state. The custom status instructions will be mapped to the steps in the orchestration code:
public static async Task SetStatusTest([OrchestrationTrigger] IDurableOrchestrationContext context)
{
// ...do work...
// update the status of the orchestration with some arbitrary data
var customStatus = new { nextActions = new [] {"A", "B", "C"}, foo = 2, };
context.SetCustomStatus(customStatus);
// ...do more work...
}
const df = require("durable-functions");
module.exports = df.orchestrator(function*(context) {
// ...do work...
// update the status of the orchestration with some arbitrary data
const customStatus = { nextActions: [ "A", "B", "C" ], foo: 2, };
context.df.setCustomStatus(customStatus);
// ...do more work...
});
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
# ...do work...
custom_status = {'nextActions': ['A','B','C'], 'foo':2}
context.set_custom_status(custom_status)
# ...do more work...
main = df.Orchestrator.create(orchestrator_function)
@FunctionName("MyCustomStatusOrchestrator")
public String myCustomStatusOrchestrator(
@DurableOrchestrationTrigger(name = "runtimeState") String runtimeState) {
return OrchestrationRunner.loadAndRun(runtimeState, ctx -> {
// ... do work ...
// update the status of the orchestration with some arbitrary data
CustomStatusPayload payload = new CustomStatusPayload();
payload.nextActions = new String[] { "A", "B", "C" };
payload.foo = 2;
ctx.setCustomStatus(payload);
// ... do more work ...
});
}
class CustomStatusPayload {
public String[] nextActions;
public int foo;
}
While the orchestration is running, external clients can fetch this custom status:
GET /runtime/webhooks/durabletask/instances/instance123