Table of Contents
Introduction
Real-World Scenario: Healthcare Claims Processing Pipeline
Calling One Function from Another in the Same Function App
The Three Types of Durable Functions
Complete Implementation with Error Handling
Best Practices for Enterprise-Grade Durable Functions
Conclusion
Introduction
In enterprise cloud architectures, especially those built on serverless platforms like Azure Functions, orchestrating complex, stateful workflows across multiple functions is a common requirement. While simple HTTP-triggered functions are great for isolated tasks, real-world business logic often demands coordination—like chaining validations, external API calls, and human-in-the-loop approvals.
This is where Durable Functions, an extension of Azure Functions, shine. They enable you to write stateful workflows in a serverless compute model—without managing infrastructure.
In this article, we’ll explore how to call one function from another within the same Function App using Durable Functions, using a real-time healthcare claims adjudication pipeline as our scenario.
Real-World Scenario: Healthcare Claims Processing Pipeline
Imagine a health insurer processing thousands of medical claims daily. Each claim must:
Be validated for format and completeness
Checked against patient eligibility
Sent to a fraud detection AI service
Routed for manual review if suspicious
Finally approved or denied with audit logging
This is a multi-step, long-running, stateful workflow—perfect for Durable Functions.
![PlantUML Diagram]()
Calling One Function from Another in the Same Function App
In standard Azure Functions, you cannot directly call another function like a regular method. Instead, you invoke it via its trigger (e.g., HTTP, queue, or orchestration client).
But with Durable Functions, you use the orchestrator pattern: one orchestrator function coordinates calls to activity functions (which contain the actual business logic). All functions reside in the same Function App and share the same codebase.
Key principle
Orchestrator functions describe the workflow; activity functions do the work.
You call activity functions using context.callActivity() (in JavaScript) or IDurableOrchestrationContext.CallActivityAsync() (in C#).
The Three Types of Durable Functions
As a senior cloud architect, you must understand these three core types:
1. Orchestrator Functions
Stateful functions that define the workflow logic. They must be deterministic (no random, date/time, or direct I/O). They schedule and await activity functions.
2. Activity Functions
Stateless, idempotent functions that perform actual tasks (e.g., call APIs, query databases). These are the “workers” invoked by orchestrators.
3. Client Functions
Triggered by external events (HTTP, queue, timer). They start orchestrations by creating instances via DurableClient.
Complete Implementation with Error Handling (C#)
Below is a production-ready implementation of the healthcare claims pipeline:
// ClaimsProcessingOrchestrator.cs
public static class ClaimsProcessingOrchestrator
{
[FunctionName(nameof(RunClaimsWorkflow))]
public static async Task<ClaimResult> RunClaimsWorkflow(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var claim = context.GetInput<ClaimSubmission>();
try
{
// Step 1: Validate claim format
var validation = await context.CallActivityAsync<ValidationResult>(
nameof(ValidateClaim), claim);
if (!validation.IsValid)
return new ClaimResult { Status = "Rejected", Reason = validation.ErrorMessage };
// Step 2: Check patient eligibility
var eligibility = await context.CallActivityAsync<bool>(
nameof(CheckEligibility), claim.PatientId);
if (!eligibility)
return new ClaimResult { Status = "Denied", Reason = "Patient not eligible" };
// Step 3: Fraud detection (external AI service)
var fraudScore = await context.CallActivityAsync<decimal>(
nameof(AnalyzeFraudRisk), claim);
// Step 4: Human review if high risk
if (fraudScore > 0.8m)
{
// Wait for human approval (external event)
var reviewApproved = await context.WaitForExternalEvent<bool>("ReviewApproved");
if (!reviewApproved)
return new ClaimResult { Status = "Denied", Reason = "Failed manual review" };
}
// Step 5: Final approval
await context.CallActivityAsync(nameof(ApproveClaim), claim.ClaimId);
return new ClaimResult { Status = "Approved", ClaimId = claim.ClaimId };
}
catch (Exception ex)
{
// Log and fail gracefully
await context.CallActivityAsync(nameof(LogError),
new { ClaimId = claim.ClaimId, Error = ex.Message });
return new ClaimResult { Status = "Failed", Reason = "System error" };
}
}
}
// Activity Functions
public static class ClaimActivities
{
[FunctionName(nameof(ValidateClaim))]
public static ValidationResult ValidateClaim([ActivityTrigger] ClaimSubmission claim)
{
// Business logic: validate fields, schema, etc.
return string.IsNullOrEmpty(claim.ProviderId)
? new ValidationResult(false, "Missing provider ID")
: new ValidationResult(true);
}
[FunctionName(nameof(CheckEligibility))]
public static async Task<bool> CheckEligibility([ActivityTrigger] string patientId)
{
// Call eligibility microservice
using var client = new HttpClient();
var response = await client.GetAsync($"https://eligibility-api/verify/{patientId}");
return response.IsSuccessStatusCode;
}
[FunctionName(nameof(AnalyzeFraudRisk))]
public static async Task<decimal> AnalyzeFraudRisk([ActivityTrigger] ClaimSubmission claim)
{
// Call AI fraud detection model
var payload = JsonSerializer.Serialize(claim);
using var client = new HttpClient();
var res = await client.PostAsync("https://fraud-ai/predict",
new StringContent(payload, Encoding.UTF8, "application/json"));
var json = await res.Content.ReadAsStringAsync();
return JsonDocument.Parse(json).RootElement.GetProperty("riskScore").GetDecimal();
}
[FunctionName(nameof(ApproveClaim))]
public static async Task ApproveClaim([ActivityTrigger] string claimId)
{
// Update database, send notification, etc.
await CosmosDbService.UpdateClaimStatus(claimId, "Approved");
}
[FunctionName(nameof(LogError))]
public static void LogError([ActivityTrigger] object errorData)
{
// Send to Application Insights or Log Analytics
TelemetryClient.TrackException(new Exception($"Claim error: {errorData}"));
}
}
// HTTP Client Function (Entry Point)
public static class ClaimsHttpStarter
{
[FunctionName("StartClaimsProcessing")]
public static async Task<IActionResult> StartClaimsProcessing(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[DurableClient] IDurableClient client)
{
var claim = await req.ReadFromJsonAsync<ClaimSubmission>();
var instanceId = await client.StartNewAsync(nameof(RunClaimsWorkflow), claim);
return new AcceptedResult(
$"Started claim processing. Instance ID: {instanceId}",
new { instanceId });
}
// Endpoint to approve high-risk claims manually
[FunctionName("ApproveManualReview")]
public static async Task<IActionResult> ApproveManualReview(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "review/{instanceId}")]
HttpRequest req,
string instanceId,
[DurableClient] IDurableClient client)
{
var approve = await req.ReadFromJsonAsync<ReviewDecision>();
await client.RaiseEventAsync(instanceId, "ReviewApproved", approve.IsApproved);
return new OkResult();
}
}
Data Models:
public record ClaimSubmission(string ClaimId, string PatientId, string ProviderId, decimal Amount);
public record ValidationResult(bool IsValid, string ErrorMessage = null);
public record ClaimResult(string Status, string Reason = null, string ClaimId = null);
public record ReviewDecision(bool IsApproved);
![1]()
![2]()
![3]()
![4]()
Best Practices for Enterprise-Grade Durable Functions
Never perform I/O in orchestrators – only in activity functions.
Make activity functions idempotent – retries are common.
Use WaitForExternalEvent for human-in-the-loop steps – avoids polling.
Monitor with Application Insights – Durable Functions emit rich telemetry.
Version your orchestrators carefully – breaking changes can corrupt in-flight instances.
Use durable timers for SLA enforcement – e.g., auto-reject if review not completed in 72h.
Conclusion
Calling one function from another in Azure isn’t about direct method invocation—it’s about orchestrating stateful workflows using the right patterns. With Durable Functions’ three pillars—orchestrator, activity, and client functions—you can build resilient, scalable, and auditable enterprise workflows like healthcare claims processing, supply chain approvals, or loan underwriting. By leveraging this pattern, you maintain the serverless advantage (auto-scaling, pay-per-use) while solving real-world business complexity—all within a single Function App, with full observability and recovery semantics.
In cloud-native architecture, orchestration is not optional—it’s essential.