Skip to main content

Event History walkthrough with the TypeScript SDK

View Markdown

In order to understand how Workflow Replay works, this page will go through the following walkthroughs:

  1. How Workflow Code Maps to Commands
  2. How Workflow Commands Map to Events
  3. How History Replay Provides Durable Execution
  4. Example of a Non-Deterministic Workflow

How Workflow Code Maps to Commands

This walkthrough will cover how the Workflow code maps to Commands that get sent to the Temporal Service, letting the Temporal Service know what to do.

Step through the Workflow Definition below. Each step highlights the statements the Worker is running and shows the Commands it has issued so far.

1const { sendBill, getDistance } = proxyActivities<typeof activities>({
2 startToCloseTimeout: '5 seconds',
3});
4
5export async function pizzaWorkflow(order: Order): Promise<string> {
6 let distance: Distance | undefined = undefined;
7 let totalPrice = 0;
8
9 // compute distance
10 distance = await getDistance(order.address);
11
12 if (distance.kilometers > 25) {
13 throw new ApplicationFailure('Customer too far away for delivery');
14 }
15
16 // Iterate over the items and calculate the cost of the order
17 for (const pizza of order.items) {
18 totalPrice += pizza.price;
19 }
20
21 // Wait 30 minutes before billing the customer
22 await sleep('30 minutes');
23
24 const bill = {
25 customerID: order.customer.customerID,
26 orderNumber: order.orderNumber,
27 amount: totalPrice,
28 description: 'Pizza',
29 };
30
31 const confirmation = await sendBill(bill);
32
33 return confirmation;
34}
Step 1/11

A basic Workflow Definition

This Workflow Definition takes a pizza order and does the work listed here. Step through it to see which statements the Worker handles on its own and which ones send a Command to the Temporal Service.

  • Defines a Start-to-Close Timeout
  • Determines the distance to the customer
  • Fails if the customer is too far away for delivery
  • Calculates the total price of the pizzas
  • Sleeps for 30 minutes
  • Populates an object with billing information
  • Sends a bill to the customer
Commands issued

Nothing yet.

Four statements in this Workflow Definition produce a Command:

StatementCommand
await getDistance(order.address)ScheduleActivityTask
await sleep('30 minutes')StartTimer
await sendBill(bill)ScheduleActivityTask
return confirmationCompleteWorkflowExecution

Everything else is an internal step. Setting the Start-to-Close Timeout, declaring the variables, evaluating the distance, totaling the order price, and populating the bill object all run in the Worker without contacting the Temporal Service.

How Workflow Commands Map to Events

The Commands that are sent to the Temporal Service are then turned into Events, which build up the Event History. The Event History is a detailed log of Events that occur during the lifecycle of a Workflow Execution, such as the execution of Workflow Tasks or Activity Tasks. Event Histories are persisted to the database used by the Temporal Service, so they're durable, and will even survive a crash of the Temporal Service itself.

These Events are what are used to recreate a Workflow Execution's state in the case of failure.

Step through the same Workflow Definition to see each Command the Worker issues and the Events the Temporal Service records in response.

1const { sendBill, getDistance } = proxyActivities<typeof activities>({
2 startToCloseTimeout: '5 seconds',
3});
4
5export async function pizzaWorkflow(order: Order): Promise<string> {
6 let distance: Distance | undefined = undefined;
7 let totalPrice = 0;
8
9 // compute distance
10 distance = await getDistance(order.address);
11
12 if (distance.kilometers > 25) {
13 throw new ApplicationFailure('Customer too far away for delivery');
14 }
15
16 // Iterate over the items and calculate the cost of the order
17 for (const pizza of order.items) {
18 totalPrice += pizza.price;
19 }
20
21 // Wait 30 minutes before billing the customer
22 await sleep('30 minutes');
23
24 const bill = {
25 customerID: order.customer.customerID,
26 orderNumber: order.orderNumber,
27 amount: totalPrice,
28 description: 'Pizza',
29 };
30
31 const confirmation = await sendBill(bill);
32
33 return confirmation;
34}
Step 1/10

Commands and the Events they produce

This walkthrough keeps a running list of the Commands the Worker issues and the Events the Temporal Service records in response. Blue Events are the direct result of a Command. Pink Events are an indirect result.

Commands

Nothing yet.

Events

Nothing yet.

Blue Events are the direct result of a Command. Pink Events are an indirect result, such as the Events the Temporal Service records when a Worker starts or finishes a Task:

CommandDirect EventIndirect Events
ScheduleActivityTaskActivityTaskScheduledActivityTaskStarted, ActivityTaskCompleted
StartTimerTimerStartedTimerFired

How History Replay Provides Durable Execution

Now that you have seen how code maps to Commands, and how Commands map to Events, this next walkthrough will take a look at how Temporal uses Replay with the Events to provide Durable Execution and restore a Workflow Execution in the case of a failure.

This code walkthrough will begin by walking through a Workflow Execution, describing how the code maps to Commands and Events. There will then be a Worker crash halfway through, explaining how Temporal uses Replay to recover the state of the Workflow Execution, ultimately resulting in a completed execution that's identical to one that had not crashed.

1const { sendBill, getDistance } = proxyActivities<typeof activities>({
2 startToCloseTimeout: '5 seconds',
3});
4
5export async function pizzaWorkflow(order: Order): Promise<string> {
6 let distance: Distance | undefined = undefined;
7 let totalPrice = 0;
8
9 // compute distance
10 distance = await getDistance(order.address);
11
12 if (distance.kilometers > 25) {
13 throw new ApplicationFailure('Customer too far away for delivery');
14 }
15
16 // Iterate over the items and calculate the cost of the order
17 for (const pizza of order.items) {
18 totalPrice += pizza.price;
19 }
20
21 // Wait 30 minutes before billing the customer
22 await sleep('30 minutes');
23
24 const bill = {
25 customerID: order.customer.customerID,
26 orderNumber: order.orderNumber,
27 amount: totalPrice,
28 description: 'Pizza',
29 };
30
31 const confirmation = await sendBill(bill);
32
33 return confirmation;
34}
Step 1/24Temporal ServiceOriginal execution

A Client requests the Workflow Execution

The walkthrough begins with a request to execute this Workflow Definition with input data about the customer and the pizzas ordered. The Temporal Service records WorkflowExecutionStarted, always the first Event of a Workflow Execution, and that Event holds the input data.

Commands

Nothing yet.

Event History
  1. (customer, pizzas ordered)

The walkthrough covers four phases:

  1. Original execution: the Client starts the Workflow Execution, and the Commands the Worker issues become Events in the Event History.
  2. Worker crash: the Worker dies partway through a Workflow Task. When the Workflow Task Timeout elapses, 10 seconds by default, the Temporal Service records WorkflowTaskTimedOut and schedules a new Workflow Task.
  3. History Replay: a Worker requests the Event History and re-executes the Workflow code with the original input, which the WorkflowExecutionStarted Event stores. Commands the Worker creates during Replay are matched against the Event History instead of being issued, so Activities don't run again. The Worker uses the results stored in the ActivityTaskCompleted Events.
  4. Execution resumes: past the point of the crash, the Event History holds no matching Events, so the Worker issues Commands for real again until the Workflow Execution completes. The result is identical to an execution that never crashed.

Example of a Non-Deterministic Workflow

Now that Replay has been covered, this section will explain why Workflows need to be deterministic in order for Replay to work.

A Workflow is deterministic if every execution of its Workflow Definition produces the same Commands in the same sequence given the same input.

As mentioned in the How History Replay Provides Durable Execution walkthrough, in the case of a failure, a Worker requests the Event History to replay it. During Replay, the Worker runs the Workflow code again to produce a set of Commands which is compared against the sequence of Commands in the Event History. When there’s a mismatch between the expected sequence of Commands the Worker expects based on the Event History and the actual sequence produced during Replay (due to non-determinism), Replay will be unable to continue.

To better understand why Workflows need to be deterministic, it's helpful to look at a Workflow Definition that violates it. In this case, this code will walk through a Workflow Definition that breaks the determinism constraint with a random number generator.

1const { importSalesData, runDailyReport } = proxyActivities<typeof activities>({
2 startToCloseTimeout: '45 minutes',
3});
4
5export async function generateDailyReport(): Promise<void> {
6 await importSalesData();
7
8 if (getRandomNumber(1, 100) >= 50) {
9 await sleep('4 hours');
10 }
11
12 log.info('Preparing to run daily report', {});
13
14 await runDailyReport();
15}
Step 1/9Sends a CommandFirst execution

The importSalesData Activity runs

As this Workflow executes step by step, the first line that results in a Command is the call to the importSalesData Activity. The Activity Execution succeeds, so the Temporal Service logs three Events to the Event History.

Commands created
  1. (importSalesData)
Relevant History Events
  1. (importSalesData)

During the first execution, the random number is 84, so the Workflow starts a Timer and the Event History records TimerStarted and TimerFired. During Replay, the random number is 14, so the Workflow skips the Timer and produces a ScheduleActivityTask Command where the Event History expects StartTimer. That mismatch is what makes Replay fail.

Note that non-deterministic failures do not fail the Workflow Execution by default. A non-deterministic failure is considered a Workflow Task Failure which is considered a transient failure, meaning it retries over and over. Users can also fix the source of non-determinism, perhaps by removing the Activity, and then restart the Workers. This means that this type of failure can recover by itself. You can also use a strategy called versioning to address this non-determinism error. See versioning to learn more.

For more information on how Temporal handles Durable Execution or to see these walkthroughs in video format with more explanation, check out our free, self-paced courses: Temporal 102 and Versioning Workflows.

Temporal Applications Support Non-Deterministic Operations

We want to emphasize that although your Workflows themselves need to be deterministic, your application itself does not!

Remember that pretty much anything that interacts with the external world is inherently non-deterministic:

  • Calling LLM APIs
  • Querying databases
  • Reading or writing files
  • Making HTTP requests to external services

Good news: Your Temporal application can absolutely handle all of these operations. While your Workflow must be deterministic, your application absolutely can handle any type of non-deterministic operation, including those listed above. This gives you the best of both worlds—the crash-proof reliability of a Workflow and the resiliency of Activities which have built-in support for retries.