HITL (Human In The Loop) For Durable Workflows In Microsoft Agent Framework

My previous article focused on implementing Durable workflows in MAF while an another article explored the implementation of HITL for MAF agents.
I would strongly recommend to have a thorough understanding of the concepts and the examples covered in both these articles.
To be fair, implementation of Durable workflows is far straightforward and simple compared to implementation of Durable agents. With durable workflows each registered workflow automatically gets an HTTP trigger without the need for implementing any custom routing logic from your end.
For example, with Durable agents you have to wire your own custom routing logic which starts with the StartOrchestration method.
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "youragent/run")] HttpRequestData req, [DurableClient] DurableTaskClient client)
To trigger the workflow execution you then require a RunOrchestrationAsync custom method.
[Function(nameof(RunOrchestrationAsync))]
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
Then invoke the workflow Orchestration
Invoke-RestMethod -Method Post -Uri http://localhost:7001/api/youragent/run
Refer to this article for step by step process that demonstrates Durable multi agent orchestration in MAF.
Implementation of HITL for Durable agents is even more complex.
You first have to define StartOrchestrationAsync and RunOrchestrationAsync methods and then under the RunOrchestrationAsync method, create an external event that waits for the user response.
[Function(nameof(RunOrchestrationAsync))]
public static async Task RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
HumanApproval humanResponse;
humanResponse = await context.WaitForExternalEvent<HumanApproval>(
eventName: "HumanApproval", timeout: TimeSpan.FromHours(1));
}
Then implement a custom routing trigger to handle the user input.
[Function(nameof(HumanApprovalAsync))]
public static async Task<HttpResponseData> HumanApprovalAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/notification/{instanceId}")] HttpRequestData req, string instanceId,[DurableClient] DurableTaskClient client)
{
var humanapproval = await req.ReadFromJsonAsync<HumanApproval>();
await client.RaiseEventAsync(instanceId, "HumanApproval", humanapproval);
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
return response;
}
To trigger the workflow execution
$body = @{
input = "Some Text"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:{Port no set in your launchsettings.json file}/api/hitl/run `
-ContentType application/json `
-Body $body
and to invoke the approval process
$json = '{"IsApproved":"Yes"}'
Invoke-RestMethod `
-Uri "http://localhost:{Port no set in your launchsettings.json file}/api/hitl/notification/{InstanceId from StartOrchestrationAsync method}" `
-Method Post `
-ContentType "application/json" `
-Body $json
Refer to this article for detailed steps involved for HITL in MAF durable agents.
But with HITL (Human In The Loop) for Durable workflows , there is no need to maintain all these complexities. Durable workflows natively handles all of it.
Implementation
We will use the use case from my previous article on MAF durable workflows.
Only addition in this case would be the introduction of property IsApproved to the _Response object and introducing RequestPort to the workflow.
_Response* *Object Earlier Version >>
public sealed class _Response
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("inputnumber")]
public string InputNumber { get; set; } = string.Empty;
[JsonPropertyName("squareroot")]
public string SquareRoot { get; set; } = string.Empty;
}
_Response Object New Version >>
public sealed class _Response
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("inputnumber")]
public string InputNumber { get; set; } = string.Empty;
[JsonPropertyName("squareroot")]
public string SquareRoot { get; set; } = string.Empty;
[JsonPropertyName("isapproved")]
public Boolean IsApproved { get; set; }
}
RequestPort
RequestPort requestPort = RequestPort.Create<_Response, _Response>("NotificationApproval");
add the RequestPort as an Edge to the workflow.
RequestPort requestPort = RequestPort.Create<_Response, _Response>("NotificationApproval");
WorkflowBuilder builder = new(typeDetectionExecutor);
Workflow workflow = builder.AddEdge(typeDetectionExecutor, squareRootcalculatorExecutor)
.AddEdge(squareRootcalculatorExecutor, requestPort)
.AddEdge(requestPort, sendNumberNotificationExecutor)
.WithOutputFrom(sendNumberNotificationExecutor).WithName("NumberDetector").Build();
That's all.. No need for any complex custom routing logic and handling external events through the code.
Execute the Durable Function and it auto creates the underlying endpoints
To trigger the workflow just invoke the endpoints through PowerShell
$json = '{"inputnumber":"12"}'
Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/run"`
-Method Post `
-ContentType "text/json" `
-Body $json
It creates an Orchestration Id in the DTS
Above, an OrchestrationId 782ad04696204eaa8161c66c27b11a20 is created.
Now use the OrchestrationId to invoke HITL for the worklfow.
$json = '{
"eventName": "NotificationApproval",
"response": { "isapproved": true}
}'
Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/respond/782ad04696204eaa8161c66c27b11a20" `
-Method Post `
-ContentType "text/json" `
-Body $json
where isapproved is the new added property in the _Response object, NumberDetector is the name of the workflow and the eventName NotificationApproval used in the invocation above is the name of the RequestPort that we created earlier.
RequestPort requestPort = RequestPort.Create<_Response, _Response>("NotificationApproval");
In the DTS (Durable Task Scheduler) dashboard the orchestration status is Completed.
The workflow setup
Workflow workflow = builder.AddEdge(typeDetectionExecutor, squareRootcalculatorExecutor)
.AddEdge(squareRootcalculatorExecutor, requestPort)
.AddEdge(requestPort, sendNumberNotificationExecutor)
.WithOutputFrom(sendNumberNotificationExecutor).WithName("NumberDetector").Build();
The SendNumberNotificationExecutor Executor code
internal sealed class SendNumberNotificationExecutor() : Executor<_Response>("SendNumberNotificationExecutor")
{
[YieldsOutput(typeof(string))]
public override async ValueTask HandleAsync(_Response response, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (response.IsApproved == true)
{
await context.YieldOutputAsync($"The approval is approved");
}
else
{
await context.YieldOutputAsync($"The approval is rejected");
}
}
}
One interesting aspect with Durable workflows is that, its possible to invoke the orchestration through your custom **runId .**In the below example , I used a custom runId = 123
$json = '{"inputnumber":"12"}'
Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/run?runid=123" `
-Method Post `
-ContentType "text/json" `
-Body $json
Invoking the approval process with runid =123
$json = '{
"eventName": "NotificationApproval",
"response": { "isapproved": true}
}'
Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/respond/123" `
-Method Post `
-ContentType "text/json" `
-Body $json
Execution >>
Conclusion
As mentioned earlier in the article, implementation of Human In The Loop (HITL) for Durable workflows is far less simpler and pretty straightforward compared to the Human In The Loop (HITL) implementation for Durable agents. This eases out lot of behind the scene complexities that is required to maintain the durable workflows in MAF.
I hope this article and the examples in it were detailed enough to get you started on Human In The Loop (HITL) process for Durable workflows.
Thanks for reading !!!



