Function chaining refers to the pattern of executing a sequence of functions in a particular order. Often the output of one function needs to be applied to the input of another function. This article describes the chaining sequence that you create when you complete the Durable Functions quickstart (C#, JavaScript, TypeScript, Python, PowerShell, or Java). For more information about Durable Functions, see Durable Functions overview.
Note
Version 4 of the Node.js programming model for Azure Functions is in preview. The new v4 model is designed to have a more flexible and intuitive experience for JavaScript and TypeScript developers. Learn more about the differences between v3 and v4 in the upgrade guide.
In the following code snippets, JavaScript (PM4) denotes programming model V4, the new experience.
This article explains the following functions in the sample app:
E1_HelloSequence
: An orchestrator function that calls E1_SayHello
multiple times in a sequence. It stores the outputs from the E1_SayHello
calls and records the results.
E1_SayHello
: An activity function that prepends a string with "Hello".
HttpStart
: An HTTP triggered durable client function that starts an instance of the orchestrator.
E1_HelloSequence orchestrator function
[FunctionName("E1_HelloSequence")]
public static async Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return outputs;
}
All C# orchestration functions must have a parameter of type DurableOrchestrationContext
, which exists in the Microsoft.Azure.WebJobs.Extensions.DurableTask
assembly. This context object lets you call other activity functions and pass input parameters using its CallActivityAsync
method.
The code calls E1_SayHello
three times in sequence with different parameter values. The return value of each call is added to the outputs
list, which is returned at the end of the function.
If you use Visual Studio Code or the Azure portal for development, here's the content of the function.json file for the orchestrator function. Most orchestrator function.json files look almost exactly like this.
{
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
],
"disabled": false
}
The important thing is the orchestrationTrigger
binding type. All orchestrator functions must use this trigger type.
Warning
To abide by the "no I/O" rule of orchestrator functions, don't use any input or output bindings when using the orchestrationTrigger
trigger binding. If other input or output bindings are needed, they should instead be used in the context of activityTrigger
functions, which are called by the orchestrator. For more information, see the orchestrator function code constraints article.
Here is the orchestrator function:
const df = require("durable-functions");
module.exports = df.orchestrator(function* (context) {
context.log("Starting chain sample");
const output = [];
output.push(yield context.df.callActivity("E1_SayHello", "Tokyo"));
output.push(yield context.df.callActivity("E1_SayHello", "Seattle"));
output.push(yield context.df.callActivity("E1_SayHello", "London"));
return output;
});
All JavaScript orchestration functions must include the durable-functions
module. It's a library that enables you to write Durable Functions in JavaScript. There are three significant differences between an orchestrator function and other JavaScript functions:
- The orchestrator function is a generator function.
- The function is wrapped in a call to the
durable-functions
module's orchestrator
method (here df
).
- The function must be synchronous. Because the 'orchestrator' method handles the final call to 'context.done', the function should simply 'return'.
The context
object contains a df
durable orchestration context object that lets you call other activity functions and pass input parameters using its callActivity
method. The code calls E1_SayHello
three times in sequence with different parameter values, using yield
to indicate the execution should wait on the async activity function calls to be returned. The return value of each call is added to the outputs
array, which is returned at the end of the function.
const df = require("durable-functions");
const helloActivityName = "sayHello";
df.app.orchestration("helloSequence", function* (context) {
context.log("Starting chain sample");
const output = [];
output.push(yield context.df.callActivity(helloActivityName, "Tokyo"));
output.push(yield context.df.callActivity(helloActivityName, "Seattle"));
output.push(yield context.df.callActivity(helloActivityName, "Cairo"));
return output;
});
All JavaScript orchestration functions must include the durable-functions
module. This module enables you to write Durable Functions in JavaScript. To use the V4 node programming model, you need to install the preview v3.x
version of durable-functions
.
There are two significant differences between an orchestrator function and other JavaScript functions:
- The orchestrator function is a generator function.
- The function must be synchronous. The function should simply 'return'.
The context
object contains a df
durable orchestration context object that lets you call other activity functions and pass input parameters using its callActivity
method. The code calls sayHello
three times in sequence with different parameter values, using yield
to indicate the execution should wait on the async activity function calls to be returned. The return value of each call is added to the outputs
array, which is returned at the end of the function.
Note
Python Durable Functions are available for the Functions 3.0 runtime only.
If you use Visual Studio Code or the Azure portal for development, here's the content of the function.json file for the orchestrator function. Most orchestrator function.json files look almost exactly like this.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "context",
"type": "orchestrationTrigger",
"direction": "in"
}
]
}
The important thing is the orchestrationTrigger
binding type. All orchestrator functions must use this trigger type.
Warning
To abide by the "no I/O" rule of orchestrator functions, don't use any input or output bindings when using the orchestrationTrigger
trigger binding. If other input or output bindings are needed, they should instead be used in the context of activityTrigger
functions, which are called by the orchestrator. For more information, see the orchestrator function code constraints article.
Here is the orchestrator function:
import azure.functions as func
import azure.durable_functions as df
def orchestrator_function(context: df.DurableOrchestrationContext):
result1 = yield context.call_activity('E1_SayHello', "Tokyo")
result2 = yield context.call_activity('E1_SayHello', "Seattle")
result3 = yield context.call_activity('E1_SayHello', "London")
return [result1, result2, result3]
main = df.Orchestrator.create(orchestrator_function)
All Python orchestration functions must include the durable-functions
package. It's a library that enables you to write Durable Functions in Python. There are two significant differences between an orchestrator function and other Python functions:
- The orchestrator function is a generator function.
- The file should register the orchestrator function as an orchestrator by stating
main = df.Orchestrator.create(<orchestrator function name>)
at the end of the file. This helps distinguish it from other, helper, functions declared in the file.
The context
object lets you call other activity functions and pass input parameters using its call_activity
method. The code calls E1_SayHello
three times in sequence with different parameter values, using yield
to indicate the execution should wait on the async activity function calls to be returned. The return value of each call is returned at the end of the function.
E1_SayHello activity function
[FunctionName("E1_SayHello")]
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return $"Hello {name}!";
}
Activities use the ActivityTrigger
attribute. Use the provided IDurableActivityContext
to perform activity related actions, such as accessing the input value using GetInput<T>
.
The implementation of E1_SayHello
is a relatively trivial string formatting operation.
Instead of binding to an IDurableActivityContext
, you can bind directly to the type that is passed into the activity function. For example:
[FunctionName("E1_SayHello_DirectInput")]
public static string SayHelloDirectInput([ActivityTrigger] string name)
{
return $"Hello {name}!";
}
E1_SayHello/function.json
The function.json file for the activity function E1_SayHello
is similar to that of E1_HelloSequence
except that it uses an activityTrigger
binding type instead of an orchestrationTrigger
binding type.
{
"bindings": [
{
"name": "name",
"type": "activityTrigger",
"direction": "in"
}
],
"disabled": false
}
Note
All activity functions called by an orchestration function must use the activityTrigger
binding.
The implementation of E1_SayHello
is a relatively trivial string formatting operation.
module.exports = function (context) {
context.done(null, `Hello ${context.bindings.name}!`);
};
Unlike the orchestration function, an activity function needs no special setup. The input passed to it by the orchestrator function is located on the context.bindings
object under the name of the activityTrigger
binding - in this case, context.bindings.name
. The binding name can be set as a parameter of the exported function and accessed directly, which is what the sample code does.
The implementation of sayHello
is a relatively trivial string formatting operation.
const df = require("durable-functions");
const helloActivityName = "sayHello";
df.app.activity(helloActivityName, {
handler: function (input) {
return `Hello ${input}`;
},
});
Unlike the orchestration function, an activity function needs no special setup. The input passed to it by the orchestrator function is the first argument to the function. The second argument is the invocation context, which is not used in this example.
E1_SayHello/function.json
The function.json file for the activity function E1_SayHello
is similar to that of E1_HelloSequence
except that it uses an activityTrigger
binding type instead of an orchestrationTrigger
binding type.
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "name",
"type": "activityTrigger",
"direction": "in"
}
]
}
Note
All activity functions called by an orchestration function must use the activityTrigger
binding.
The implementation of E1_SayHello
is a relatively trivial string formatting operation.
def main(name: str) -> str:
return f"Hello {name}!"
Unlike the orchestrator function, an activity function needs no special setup. The input passed to it by the orchestrator function is directly accessible as the parameter to the function.
HttpStart client function
You can start an instance of orchestrator function using a client function. You will use the HttpStart
HTTP triggered function to start instances of E1_HelloSequence
.
public static class HttpStart
{
[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
[DurableClient] IDurableClient starter,
string functionName,
ILogger log)
{
// Function input comes from the request content.
object eventData = await req.Content.ReadAsAsync<object>();
string instanceId = await starter.StartNewAsync(functionName, eventData);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
}
To interact with orchestrators, the function must include a DurableClient
input binding. You use the client to start an orchestration. It can also help you return an HTTP response containing URLs for checking the status of the new orchestration.
{
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"route": "orchestrators/{functionName}",
"methods": ["post"]
},
{
"name": "$return",
"type": "http",
"direction": "out"
},
{
"name": "starter",
"type": "orchestrationClient",
"direction": "in"
}
],
"disabled": false
}
To interact with orchestrators, the function must include a durableClient
input binding.
const df = require("durable-functions");
module.exports = async function (context, req) {
const client = df.getClient(context);
const instanceId = await client.startNew(req.params.functionName, undefined, req.body);
context.log(`Started orchestration with ID = '${instanceId}'.`);
return client.createCheckStatusResponse(context.bindingData.req, instanceId);
};
Use df.getClient
to obtain a DurableOrchestrationClient
object. You use the client to start an orchestration. It can also help you return an HTTP response containing URLs for checking the status of the new orchestration.
const df = require("durable-functions");
const { app } = require("@azure/functions");
app.http("httpStart", {
route: "orchestrators/{orchestratorName}",
extraInputs: [df.input.durableClient()],
handler: async (request, context) => {
const client = df.getClient(context);
const body = await request.json();
const instanceId = await client.startNew(request.params.orchestratorName, { input: body });
context.log(`Started orchestration with ID = '${instanceId}'.`);
return client.createCheckStatusResponse(request, instanceId);
},
});
To manage and interact with orchestrators, the function needs a durableClient
input binding. This binding needs to be specified in the extraInputs
argument when registering the function. A durableClient
input can be obtained by calling df.input.durableClient()
.
Use df.getClient
to obtain a DurableClient
object. You use the client to start an orchestration. It can also help you return an HTTP response containing URLs for checking the status of the new orchestration.
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"route": "orchestrators/{functionName}",
"methods": [
"post",
"get"
]
},
{
"name": "$return",
"type": "http",
"direction": "out"
},
{
"name": "starter",
"type": "durableClient",
"direction": "in"
}
]
}
To interact with orchestrators, the function must include a durableClient
input binding.
import logging
import azure.functions as func
import azure.durable_functions as df
async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse:
client = df.DurableOrchestrationClient(starter)
instance_id = await client.start_new(req.route_params["functionName"], None, None)
logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)
Use the DurableOrchestrationClient
constructor to obtain a Durable Functions client. You use the client to start an orchestration. It can also help you return an HTTP response containing URLs for checking the status of the new orchestration.
To execute the E1_HelloSequence
orchestration, send the following HTTP POST request to the HttpStart
function.
POST http://{host}/orchestrators/E1_HelloSequence
Note
The previous HTTP snippet assumes there is an entry in the host.json
file which removes the default api/
prefix from all HTTP trigger functions URLs. You can find the markup for this configuration in the host.json
file in the samples.
For example, if you're running the sample in a function app named "myfunctionapp", replace "{host}" with "myfunctionapp.chinacloudsites.cn".
The result is an HTTP 202 response, like this (trimmed for brevity):
HTTP/1.1 202 Accepted
Content-Length: 719
Content-Type: application/json; charset=utf-8
Location: http://{host}/runtime/webhooks/durabletask/instances/96924899c16d43b08a536de376ac786b?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
(...trimmed...)
At this point, the orchestration is queued up and begins to run immediately. The URL in the Location
header can be used to check the status of the execution.
GET http://{host}/runtime/webhooks/durabletask/instances/96924899c16d43b08a536de376ac786b?taskHub=DurableFunctionsHub&connection=Storage&code={systemKey}
The result is the status of the orchestration. It runs and completes quickly, so you see it in the Completed state with a response that looks like this (trimmed for brevity):
HTTP/1.1 200 OK
Content-Length: 179
Content-Type: application/json; charset=utf-8
{"runtimeStatus":"Completed","input":null,"output":["Hello Tokyo!","Hello Seattle!","Hello London!"],"createdTime":"2017-06-29T05:24:57Z","lastUpdatedTime":"2017-06-29T05:24:59Z"}
As you can see, the runtimeStatus
of the instance is Completed and the output
contains the JSON-serialized result of the orchestrator function execution.
Note
You can implement similar starter logic for other trigger types, like queueTrigger
, eventHubTrigger
, or timerTrigger
.
Look at the function execution logs. The E1_HelloSequence
function started and completed multiple times due to the replay behavior described in the orchestration reliability topic. On the other hand, there were only three executions of E1_SayHello
since those function executions do not get replayed.
This sample has demonstrated a simple function-chaining orchestration. The next sample shows how to implement the fan-out/fan-in pattern.