DEV Community

Sanjay Vadhiya
Sanjay Vadhiya

Posted on

Beyond Background Jobs: Building Durable Workflows with Temporal and .NET 10

If you've worked with .NET applications, you've probably written code like this:

await ChargePayment();
await ReserveInventory();
await GenerateInvoice();
await SendEmail();

It looks simple.

  • But what happens if the application crashes after ReserveInventory()?
  • What happens if the payment API is temporarily unavailable?
  • What happens if the workflow needs to wait for a manager's approval for two days?
  • What happens if the server restarts while the workflow is running?

You can build solutions for all of these problems yourself, but the amount of infrastructure and error-handling code can grow quickly.

This is where Temporal comes in.

  • Temporal is a durable workflow execution platform that helps applications reliably execute long-running and failure-prone workflows.

In this article, we'll build a simple order-processing workflow using .NET 10 and Temporal and understand the concepts behind it.

What is Temporal?

Temporal allows you to define a business process as a workflow and execute it reliably.

For example:

Instead of treating these as unrelated background jobs, Temporal lets us model them as one workflow.

The important part isn't just running these steps.

The important part is that Temporal keeps track of the workflow's execution so that it can recover from failures.

Conceptually:

         Temporal
            |
            ↓
    Order Processing
            |
   ┌────────┼────────┐
   ↓        ↓        ↓
Payment  Inventory  Email
Enter fullscreen mode Exit fullscreen mode

If something goes wrong, the workflow can retry activities or continue from the appropriate point.

Why do we need Temporal?

Let's start with a normal ASP.NET Core application.

Imagine this method:

public async Task ProcessOrder()
{
    await ChargePayment();
    await ReserveInventory();
    await GenerateInvoice();
    await SendEmail();
}

Now imagine this sequence:

  • Charge Payment ✓
  • Reserve Inventory ✓
  • Generate Invoice ✗

Maybe the invoice service is temporarily unavailable.

You now need to think about:

  • Should the invoice operation be retried?
  • How many times?
  • How long should we wait between retries?
  • What happens if the application crashes?
  • How do we know which steps already completed?
  • How do we resume the process?
  • What happens if the workflow needs to wait for an external event?

As the workflow becomes more complicated, you end up building your own workflow infrastructure.

Temporal provides infrastructure for these problems.

Temporal's Core Concepts

Before writing code, let's understand four important concepts:

  1. Workflow
  2. Activity
  3. Worker
  4. Temporal Server

1. Workflow

A Workflow describes the overall business process.

For our order system:

Order Workflow

Order Workflow
|
├── Charge Payment
|
├── Reserve Inventory
|
├── Generate Invoice
|
└── Send Email

The workflow defines the order in which these operations happen.

2. Activity

An Activity performs actual external work.

For example:

  • ChargePayment()
  • ReserveInventory()
  • GenerateInvoice()
  • SendEmail()

Activities are where you normally interact with:

  • Databases
  • REST APIs
  • Payment gateways
  • Email services
  • File systems
  • External applications

A useful mental model is:

Workflow = What should happen?

Activity = Perform the actual work.

3. Worker

The Worker is your application process that executes workflows and activities.

A simplified architecture looks like this:

            Temporal Server
                  ↑
                  |
             .NET Worker
              /       \
             /         \
      Workflow       Activities
                        |
             ┌──────────┼──────────┐
             ↓          ↓          ↓
          Database    API       Email
Enter fullscreen mode Exit fullscreen mode

The Worker polls Temporal for work and executes it.

4. Temporal Server

The Temporal Server coordinates workflow execution and maintains the information needed for durable execution.

Your application doesn't simply keep workflow state in memory.

Instead:

Your Worker
     ↓
Temporal
     ↓
Workflow History

This is one of the fundamental reasons Temporal can recover workflows after failures.

Our Example

Let's build an order-processing system.

The workflow will look like this:

          Order Created
               |
               ↓
         Charge Payment
               |
               ↓
       Check Order Amount
          /          \
   > ₹10,000         <= ₹10,000
       |                  |
       ↓                  ↓
 Manager Approval     Normal Process
       \                  /
        \                /
         ↓              ↓
             Send Email
Enter fullscreen mode Exit fullscreen mode

We'll use:

  • .NET 10
  • C#
  • Temporal .NET SDK
  • ASP.NET Core

Step 1: Create the .NET Project

Create a new ASP.NET Core application:

  • dotnet new web -n TemporalDemo
  • cd TemporalDemo

Install the Temporal .NET SDK:

  • dotnet add package Temporalio

You can verify the project:

  • dotnet run

Step 2: Create the Order Model

Create:

Models/OrderRequest.cs

namespace TemporalDemo.Models;

public class OrderRequest
{
    public string CustomerName { get; set; } = "";

    public decimal Amount { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Our API will receive:

{
  "customerName": "Sanjay",
  "amount": 15000
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Activities

Create:

Activities/OrderActivities.cs

using Temporalio.Activities;

namespace TemporalDemo.Activities;

public class OrderActivities
{
    [Activity]
    public async Task<string> ChargePaymentAsync(
        string customerName,
        decimal amount)
    {
        Console.WriteLine(
            $"Charging ₹{amount} for {customerName}"
        );

        // In a real application:
        // Call your payment provider here.

        await Task.Delay(500);

        return "Payment successful";
    }

    [Activity]
    public async Task RequestManagerApprovalAsync(
        string customerName,
        decimal amount)
    {
        Console.WriteLine(
            $"Manager approval required for {customerName}: ₹{amount}"
        );

        // In a real application:
        // Save an approval request to your database
        // or notify a manager.

        await Task.Delay(500);
    }

    [Activity]
    public async Task SendEmailAsync(
        string customerName)
    {
        Console.WriteLine(
            $"Sending email to {customerName}"
        );

        // In a real application:
        // Call your email provider.

        await Task.Delay(500);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the Activities contain the actual work.

For example, this:

  • ChargePaymentAsync()

could eventually call Stripe, Razorpay, PayPal, or your own payment API.

Step 4: Create the Workflow

Create:

Workflows/OrderWorkflow.cs

using Temporalio.Workflows;
using TemporalDemo.Activities;

namespace TemporalDemo.Workflows;

[Workflow]
public class OrderWorkflow
{
    [WorkflowRun]
    public async Task<string> RunAsync(
        string customerName,
        decimal amount)
    {
        var paymentResult =
            await Workflow.ExecuteActivityAsync(
                (OrderActivities activities) =>
                    activities.ChargePaymentAsync(
                        customerName,
                        amount),
                new()
                {
                    StartToCloseTimeout =
                        TimeSpan.FromMinutes(5)
                });

        if (amount > 10000)
        {
            await Workflow.ExecuteActivityAsync(
                (OrderActivities activities) =>
                    activities.RequestManagerApprovalAsync(
                        customerName,
                        amount),
                new()
                {
                    StartToCloseTimeout =
                        TimeSpan.FromMinutes(5)
                });
        }

        await Workflow.ExecuteActivityAsync(
            (OrderActivities activities) =>
                activities.SendEmailAsync(customerName),
            new()
            {
                StartToCloseTimeout =
                    TimeSpan.FromMinutes(5)
            });

        return paymentResult;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a workflow.

The workflow says:

  1. Charge payment
  2. If amount > ₹10,000, request approval
  3. Send email
  4. Finish

Workflow vs Activity

This distinction is extremely important when learning Temporal.

Workflow

if (amount > 10000)
{
    // ...
}

The workflow defines the process.

Activity

[Activity]
public async Task<string> ChargePaymentAsync(...)
{
    // External API call
}

The Activity performs external work.

Think of it this way:

Workflow

Workflow
   |
   +-- Activity → Payment API
   |
   +-- Activity → Database
   |
   +-- Activity → Email API

Step 5: Create the Worker

The Worker executes our workflow and activities.

Create:

Worker.cs

using Temporalio.Client;
using Temporalio.Worker;
using TemporalDemo.Activities;
using TemporalDemo.Workflows;

var client = await TemporalClient.ConnectAsync(
    new("localhost:7233")
);

using var worker = new TemporalWorker(
    client,
    new TemporalWorkerOptions("order-task-queue")
        .AddWorkflow<OrderWorkflow>()
        .AddAllActivities<OrderActivities>()
);

Console.WriteLine("Temporal Worker started.");

await worker.ExecuteAsync();
Enter fullscreen mode Exit fullscreen mode

The Worker connects to:

  • localhost:7233

which is the Temporal service endpoint in our local setup.

It listens on:

  • order-task-queue

for workflow/activity tasks.

Step 6: Start a Workflow from ASP.NET Core

Your API can start a workflow.

For example:

app.MapPost("/orders", async ( 
    OrderRequest request, 
    TemporalClient client) =>; 
{ 
    var workflowId = 
        $"order-{Guid.NewGuid()}"; 

    await client.StartWorkflowAsync( 
        (OrderWorkflow workflow) =>; 
            workflow.RunAsync( 
                request.CustomerName, 
                request.Amount), 
        new WorkflowOptions 
        { 
            Id = workflowId, 
            TaskQueue = "order-task-queue" 
        }); 

    return Results.Ok(new 
    { 
        WorkflowId = workflowId 
    }); 
});
Enter fullscreen mode Exit fullscreen mode

The important idea is that your API doesn't need to perform the entire workflow itself.

It starts the workflow:

POST /orders
      |
      ↓
ASP.NET Core
      |
      ↓
Start Workflow
      |
      ↓
Temporal
      |
      ↓
.NET Worker
      |
      ↓
OrderWorkflow

What Happens When We Send an Order?

Suppose we send:

POST /orders

with:

{
  "customerName": "Sanjay",
  "amount": 15000
}

The workflow executes:

Order Created
     ↓
Charge Payment
     ↓
₹15,000 > ₹10,000
     ↓
Manager Approval
     ↓
Send Email
     ↓
Completed

For an order of ₹5,000:

Order Created
     ↓
Charge Payment
     ↓
₹5,000 <= ₹10,000
     ↓
Skip Manager Approval
     ↓
Send Email
     ↓
Completed

What Makes Temporal Different?

So far, this might look like ordinary C#.

The interesting part starts when things fail.

Imagine:

Charge Payment
      ↓
SUCCESS
      ↓
Manager Approval
      ↓
Server crashes

A normal in-memory process may lose the current execution state.

Temporal is designed around durable execution.

Conceptually:

Temporal
   |
   +-- Workflow started
   |
   +-- Payment completed
   |
   +-- Approval started

If the Worker crashes, a new Worker can reconnect to Temporal and continue processing the workflow.

This is one of Temporal's biggest advantages.

Activity Retries

External systems fail.

For example:

Your Application
      ↓
Payment API
      ↓
503 Service Unavailable

You don't necessarily want your entire workflow to fail permanently.

Temporal allows Activities to have retry policies.

For example, conceptually:

Payment Activity
      ↓
Attempt 1 → Failed
      ↓
Wait
      ↓
Attempt 2 → Failed
      ↓
Wait
      ↓
Attempt 3 → Success

This is much safer than implementing random retry loops throughout your application.

A production retry policy should also consider:

  • Maximum attempts
  • Backoff
  • Which errors are retryable
  • Which errors are permanent
  • Activity timeout
  • Long-Running Workflows

Temporal becomes even more useful when a process takes a long time.

Imagine a purchase that requires manager approval.

Order
 ↓
Payment
 ↓
Request Approval
 ↓
WAIT

The manager might approve it tomorrow.

You don't want a .NET thread sitting there doing:

await Task.Delay(
    TimeSpan.FromDays(1)
);

Instead, Temporal supports durable timers and workflow waiting mechanisms.

Conceptually:

Workflow
   ↓
Request Approval
   ↓
Wait for Signal
   |
   |  2 days later
   |
Manager Approves
   ↓
Workflow resumes
   ↓
Send Email

This is a very different problem from simply running a scheduled background job.

Temporal Signals

Suppose the workflow is waiting for approval.

A manager approves the order from an admin application.

The application can send a signal to the workflow.

Conceptually:

Admin Application
       |
       | "Approved"
       ↓
Temporal Workflow
       |
       ↓
Resume
       |
       ↓
Ship Order

This allows external events to interact with running workflows.

What if the Worker crashes?

This is one of the first questions you should ask when evaluating Temporal.

Consider:

Workflow
   ↓
Payment ✓
   ↓
Inventory ✓
   ↓
Worker crashes

When a Worker becomes available again, Temporal can provide the information needed to continue the workflow.

This is fundamentally different from relying on variables stored only in your application's memory.

Temporal Architecture

At a high level:

               Client
                 |
                 ↓
          ASP.NET Core API
                 |
                 ↓
          Temporal Server
                 |
          Task Queue
                 |
                 ↓
           .NET Worker
                 |
        ┌────────┼────────┐
        ↓        ↓        ↓
    Workflow  Activity  Activity
                  |        |
                  ↓        ↓
               Database  API
Enter fullscreen mode Exit fullscreen mode

There are three important pieces:

Client

Starts or interacts with workflows.

Temporal Server

Coordinates workflow execution and stores workflow history.

Worker

Runs your workflow and activity code.

Temporal vs Hangfire

If you've used Hangfire in .NET, you may wonder:

Why not just use Hangfire?

Hangfire is excellent for background jobs.

For example:

Every day at 9 AM
       ↓
Generate Report

Temporal is designed for more complex workflows:

Order
 ↓
Payment
 ↓
Inventory
 ↓
Wait for Approval
 ↓
Shipping
 ↓
Email

When Should You Use Temporal?

Temporal makes sense when your workflow is:

  • Long-running
  • Multi-step
  • Distributed
  • Failure-prone
  • Dependent on external services
  • Required to survive application restarts
  • Waiting for external events
  • Important enough that losing state is unacceptable

Examples include:

Payment processing

Travel booking

User onboarding

Document processing

create image for this flow

When Should You NOT Use Temporal?

Temporal isn't something you should add to every application.

If your requirement is simply:

Run this method every night

you probably don't need Temporal.

Hangfire, Quartz.NET, a cron job or a cloud scheduler may be simpler.

Likewise, if you just need:

HTTP Request
 ↓
Call API
 ↓
Send Email

a simple ASP.NET Core service or an automation platform such as n8n may be more appropriate.

Use Temporal when the workflow itself is important and needs durable execution.

A Practical Mental Model

Think of these tools like this:

Hangfire
    ↓
"Run this job."
Temporal
    ↓
"Make this workflow reliably execute
even when things fail."

Final Thoughts

Temporal isn't just another background-job library.

Its main idea is durable execution.

Instead of building your own infrastructure for:

  • retries
  • workflow state
  • timers
  • failure recovery
  • long-running processes
  • external events
  • workflow history

you can model your business process as a Temporal workflow and let the Temporal platform handle the execution infrastructure.

For .NET developers building distributed systems, payment systems, order processing, SaaS platforms, AI pipelines, or other long-running processes, this can be extremely valuable.

key takeaways:

Don't think of Temporal as a scheduler. Think of it as infrastructure for reliably executing business processes.

Don't just build workflows that work — build workflows that can survive failures.

Have you used Temporal or another workflow engine in your .NET projects? Share your experience in the comments.

Top comments (0)