From 49216edc0f5b4c16f4a2c3ad545e1b8aba5f95dd Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Fri, 24 Oct 2025 12:35:15 -0500 Subject: [PATCH 01/40] starting place --- docs/build-your-first-basic-workflow.mdx | 173 ++++++ .../python.mdx | 543 ++++++++++++++++++ sidebars.js | 74 ++- 3 files changed, 764 insertions(+), 26 deletions(-) create mode 100644 docs/build-your-first-basic-workflow.mdx create mode 100644 docs/build-your-first-basic-workflow/python.mdx diff --git a/docs/build-your-first-basic-workflow.mdx b/docs/build-your-first-basic-workflow.mdx new file mode 100644 index 0000000000..4c6aca8c4a --- /dev/null +++ b/docs/build-your-first-basic-workflow.mdx @@ -0,0 +1,173 @@ +--- +id: build-your-first-basic-workflow +title: Build your First Basic Workflow +sidebar_label: Build your First Basic Workflow +description: Learn Temporal's core concepts by building and running a money transfer Workflow that demonstrates reliability, failure handling, and live debugging. +keywords: + - temporal + - workflow + - getting started + - tutorial + - reliability + - money transfer +tags: + - Getting Started + - Tutorial + - Workflows +--- + +import { CallToAction } from "@site/src/components/elements/CallToAction"; + +# Build your First Basic Workflow + +Learn Temporal's core concepts by building a money transfer application that demonstrates reliability, failure handling, and live debugging capabilities. + +## What you'll learn + +In this tutorial, you'll discover why Temporal is a "cure-all" for building reliable applications by: + +- ✅ **Running your first Temporal Workflow** with real failure scenarios +- ✅ **Understanding core concepts** like Workflows, Activities, and Workers +- ✅ **Exploring the Temporal Web UI** for application state visibility +- ✅ **Experiencing live debugging** - fix bugs in running applications without losing state +- ✅ **Seeing automatic retries** handle transient failures gracefully + +## Prerequisites + +Before starting this tutorial: + +- **Local development environment** set up for Temporal (choose your language below) +- **Git installed** to clone the example project +- **Basic programming knowledge** in your chosen language + + + +## The Money Transfer Application + +You'll build a **money transfer application** that simulates real-world financial transactions with these critical operations: + +1. **Withdraw** money from source account +2. **Deposit** money to target account +3. **Refund** money if deposit fails + +### Why This Matters + +In traditional applications, handling failures in this flow is complex: +- What if withdrawal succeeds but deposit fails? +- How do you retry operations safely? +- How do you maintain transaction state during server crashes? + +**Temporal solves all of these problems automatically.** 🎯 + +## Key Concepts You'll Experience + +### Workflows +Define the overall business logic flow. In our case: withdraw → deposit → (refund if needed). + +### Activities +Individual operations that can fail and be retried (calling external services, database operations, etc.). + +### Workers +Temporal processes that execute your Workflows and Activities reliably. + +### Retry Policies +Automatic retry configuration that handles transient failures without custom code. + +## What Makes This Tutorial Special + +Unlike typical "Hello World" examples, this tutorial shows: + +1. **Real failure scenarios** - See how Temporal handles actual problems +2. **Live debugging** - Fix bugs in running applications without restarting +3. **State persistence** - Applications survive server crashes and resume exactly where they left off +4. **Automatic retries** - Temporal handles retry logic so you don't have to + +## Choose Your Language to Get Started + +The money transfer tutorial is available in multiple languages. Pick your preferred SDK: + +import QuickstartCards from "@site/src/components/QuickstartCards"; + + + +## What You'll Experience Step-by-Step + +### 1. **Download and Explore** (5 minutes) +Clone the money transfer project and examine the Workflow and Activity definitions. + +### 2. **Run Your First Workflow** (3 minutes) +Start the application and see a successful money transfer in action. + +### 3. **Explore the Web UI** (2 minutes) +Use Temporal's built-in Web UI to inspect Workflow state and execution history. + +### 4. **Simulate Failures** (5 minutes) +Introduce failures and watch Temporal's automatic retry mechanisms in action. + +### 5. **Live Debug a Running Workflow** (5 minutes) +The magic moment - fix a bug in a running application without losing state or restarting the transaction. + +## Key Advantages You'll Discover + +By the end of this tutorial, you'll understand how Temporal provides: + +### 🔍 **Full Visibility** +See exactly what your application is doing at every step with detailed execution history. + +### 🛡️ **Guaranteed Execution** +Your Workflows run to completion even through server crashes, network failures, and code deployments. + +### ⚙️ **Declarative Retries** +Configure retry policies in your code instead of implementing complex retry logic. + +### 🔧 **Live Debugging** +Debug and fix issues in production applications while they're running. + +### 🏗️ **Focus on Business Logic** +Write application code without worrying about infrastructure concerns like state management and failure recovery. + +## Ready to Build? + + + +## Alternative Learning Paths + +**Prefer a different approach?** +- 📖 **[Conceptual Overview](/evaluate/why-temporal)** - Understand Temporal's value proposition first +- 🛠️ **[SDK-specific Quickstarts](/quickstarts)** - Set up your development environment +- 🎓 **[Courses and Tutorials](https://learn.temporal.io/)** - Comprehensive learning paths + +--- + +**Questions?** Join our [community forum](https://community.temporal.io/) or [Slack workspace](https://temporal.io/slack) for help from Temporal experts and fellow developers. diff --git a/docs/build-your-first-basic-workflow/python.mdx b/docs/build-your-first-basic-workflow/python.mdx new file mode 100644 index 0000000000..9285cce72e --- /dev/null +++ b/docs/build-your-first-basic-workflow/python.mdx @@ -0,0 +1,543 @@ +--- +id: python +title: Run your first Temporal application with the Python SDK +sidebar_label: Python +description: Learn Temporal's core concepts by building a money transfer Workflow with the Python SDK. Experience reliability, failure handling, and live debugging in a 10-minute tutorial. +keywords: + - temporal + - python + - workflow + - tutorial + - money transfer + - reliability +tags: + - Python + - SDK + - Getting Started +hide_table_of_contents: false +--- + +import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; +import { CallToAction } from "@site/src/components/elements/CallToAction"; + +# Run your first Temporal application with the Python SDK + +You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process. + +
+ ⭐ Temporal beginner + ⏱️ ~10 minutes + 🐍 Python SDK +
+ +## 🙌 Goals + +- **Explore** Temporal's core terminology and concepts +- **Complete** several runs of a Temporal Workflow application using a Temporal Cluster and the Python SDK +- **Practice** reviewing the state of the Workflow +- **Understand** the inherent reliability of Workflow methods + +## Introduction + +The language-specific SDK, in this case the Temporal Python SDK, provides a comprehensive solution to the complexities that arise from modern application development. + +Temporal provides reliability primitives to ensure durable execution of your code, such as seamless and fault-tolerant application state tracking, automatic retries, timeouts, rollbacks due to process failures, and more. + +In this tutorial, you'll run your first Temporal Application. You'll use Temporal's Web UI for application state visibility, and then explore how Temporal helps you recover from a couple of common failures. + +## Prerequisites + +Before starting this tutorial: + +- **Set up a local development environment** for developing Temporal Applications using the Python programming language +- **Ensure you have Git installed** to clone the project + + + +## Application overview + +This project in this tutorial simulates a **money transfer application**, focusing on essential transactions such as withdrawals, deposits, and refunds. The importance of Temporal in this context lies in its ability to handle your code efficiently and reliably. + +In this sample application, money comes out of one account and goes into another. However, there are a few things that can go wrong with this process. If the withdrawal fails, then there is no need to try to make a deposit. But if the withdrawal succeeds, but the deposit fails, then the money needs to go back to the original account. + +One of Temporal's most important features is its ability to **maintain the application state when something fails**. When failures happen, Temporal recovers processes where they left off or rolls them back correctly. This allows you to focus on business logic, instead of writing application code to recover from failure. + + + + +git clone https://github.com/temporalio/money-transfer-project-template-python +cd money-transfer-project-template-python + +}> + +## Download the example application + +The application you'll use in this tutorial is available in a GitHub repository. + +Open a new terminal window and use `git` to clone the repository, then change to the project directory. + +:::tip +The repository for this tutorial is a GitHub Template repository, which means you could clone it to your own account and use it as the foundation for your own Temporal application. +::: + +Now that you've downloaded the project, let's dive into the code. + + + + +
+ Temporal Application Components +
+
+
+
🔄
+
Workflow
+
Defines overall flow
+
+
+
+
⚙️
+
Activities
+
Business logic prone to failure
+
+
+
+
👷
+
Worker
+
Runs reliably and consistently
+
+
+ +}> + +## Explore the application's Workflow and Activity Definitions + +The Temporal Application will consist of the following pieces: + +1. **A Workflow** written in Python using the Python SDK. A Workflow defines the overall flow of the application. +2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. +3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. + +In the money transfer application, you have three Activity methods: `withdraw()`, `deposit()`, and `refund()`. These symbolize the movement of funds between accounts. + +:::important +None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. +::: + +
+ + +
+ workflows.py +
+ +from datetime import timedelta +from temporalio import workflow +from temporalio.common import RetryPolicy +from temporalio.exceptions import ActivityError + +with workflow.unsafe.imports_passed_through(): + from activities import BankingActivities + from shared import PaymentDetails + +@workflow.defn +class MoneyTransfer: + @workflow.run + async def run(self, payment_details: PaymentDetails): + retry_policy = RetryPolicy( + maximum_attempts=3, + maximum_interval=timedelta(seconds=2), + non_retryable_error_types=["InvalidAccountError", "InsufficientFundsError"], + ) + + # Withdraw money + withdraw_output = await workflow.execute_activity_method( + BankingActivities.withdraw, + payment_details, + start_to_close_timeout=timedelta(seconds=5), + retry_policy=retry_policy, + ) + + # Deposit money + try: + deposit_output = await workflow.execute_activity_method( + BankingActivities.deposit, + payment_details, + start_to_close_timeout=timedelta(seconds=5), + retry_policy=retry_policy, + ) + + result = "Transfer complete (transaction IDs: " + str(withdraw_output) + ", " + str(deposit_output) + ")" + return result + except ActivityError as deposit_err: + # Handle deposit error + workflow.logger.error("Deposit failed: " + str(deposit_err)) + # Attempt to refund + try: + refund_output = await workflow.execute_activity_method( + BankingActivities.refund, + payment_details, + start_to_close_timeout=timedelta(seconds=5), + retry_policy=retry_policy, + ) + workflow.logger.info("Refund successful. Confirmation ID: " + str(refund_output)) + raise deposit_err + except ActivityError as refund_error: + workflow.logger.error("Refund failed: " + str(refund_error)) + raise refund_error + + +}> + +## Workflow Definition + +A Workflow Definition in Python uses the `@workflow.defn` decorator on the Workflow class to identify a Workflow. + +The `MoneyTransfer` class takes in transaction details. It executes Activities to withdraw and deposit the money. It also returns the results of the process. + +The asynchronous `run` method signature includes an `input` variable typed as `PaymentDetails`. This class stores details that the Workflow uses to perform the money transfer. + +
+ + +
+ activities.py +
+ +import asyncio +from dataclasses import dataclass +from temporalio import activity +from shared import PaymentDetails + +@dataclass +class BankingActivities: + @activity.defn + @staticmethod + async def withdraw(data: PaymentDetails): + print("Withdrawing money from account") + + # Simulate time to call other services that may fail + await asyncio.sleep(1) + + return "Withdrew money from account" + + @activity.defn + @staticmethod + async def deposit(data: PaymentDetails): + print("Depositing money into account") + + # Simulate time to call other services that may fail + await asyncio.sleep(1) + + # Comment/uncomment the next line to simulate failures. + # raise Exception("This deposit has failed.") + + return "Deposited money into account" + + @activity.defn + @staticmethod + async def refund(data: PaymentDetails): + print("Refunding money back to account") + + # Simulate time to call other services that may fail + await asyncio.sleep(1) + + return "Refunded money back to account" + +
+ shared.py +
+ +from dataclasses import dataclass + +MONEY_TRANSFER_TASK_QUEUE_NAME = "money-transfer" + +@dataclass +class PaymentDetails: + source_account: str + target_account: str + amount: int + reference_id: str + + +}> + +## Activity Definition + +Each Activity method simulates calling an external service. Each method can fail or succeed independently. + +The Activities are defined as static methods with the `@activity.defn` decorator. + +Note the `PaymentDetails` type, defined in `shared.py`. This contains the transaction information passed between the Workflow and Activities. + +
+ + +
+ Terminal 1 - Start the Temporal server: + +temporal server start-dev + +
+
+ Terminal 2 - Start the Worker: + +python run_worker.py + +
+
+ Terminal 3 - Start the Workflow: + +python run_workflow.py + +
+
+ ✅ Expected Success Output: + +Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345) + +
+ +}> + +## Start the Workflow + +In a new terminal window, run the following command to start the Workflow: + +The `run_workflow.py` script starts a Workflow Execution. Each time you run this file, the Temporal Server starts a new Workflow Execution. + +A Workflow Execution has exclusive access to its local state and executes concurrently to all other Workflow Executions. + +
+ + +}> + +## View the state of the Workflow with the Temporal Web UI + +The Temporal Web UI lets you see details about the Workflow you just ran. + +**What you'll see in the UI:** +- List of Workflows with their execution status +- Workflow summary with input and result +- History tab showing all events in chronological order +- Query, Signal, and Update capabilities +- Stack Trace tab for debugging + +**Try This:** Click on a Workflow in the list to see all the details of the Workflow Execution. + + + + +
+ run_worker.py +
+ +import asyncio +from temporalio.client import Client +from temporalio.worker import Worker +from activities import BankingActivities +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME +from workflows import MoneyTransfer + +async def main(): + client = await Client.connect("localhost:7233") + + worker = Worker( + client, + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + workflows=[MoneyTransfer], + activities=[BankingActivities.withdraw, BankingActivities.deposit, BankingActivities.refund], + ) + + await worker.run() + +if __name__ == "__main__": + asyncio.run(main()) + + +}> + +## Start the Worker + +The Worker hosts the Workflow and Activity functions and executes them one at a time. + +The Worker is configured to execute Workflows and Activities from the Task Queue. Since the Worker and Workflow are both configured to use the same Task Queue, the Worker will execute any Workflows and Activities sent to the Task Queue. + +When you start the Worker, it begins polling the Temporal Server for work. + +
+ +
+ +## Simulate failures + +So far, you've seen how Temporal executes a Workflow. In this section, you'll explore Temporal's ability to handle failures. + +### Recover from a server crash + +Unlike other solutions, Temporal is designed with failure in mind. To demonstrate this, you'll simulate some failures for the Workflow to recover from. + +**Start by simulating a server crash.** When any process crashes, you lose the progress of your code, unless you've designed a way to handle such failures. + +1. **Make sure your Worker is stopped** before proceeding. If the Worker is running, press `Ctrl+C` to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + python run_worker.py + ``` + +3. **In Terminal 3, start the Workflow:** + ```bash + python run_workflow.py + ``` + +4. **Inspect the Workflow Execution** using the Web UI. You can see the Worker is executing the Workflow and its Activities: + +5. **Return to Terminal 2** and stop the Worker by pressing `Ctrl+C`. + +6. **Switch back to the Web UI** and refresh the page. Your Workflow is still listed as "Running". + + The Workflow is still in progress because the Temporal Server maintains the state of the Workflow, even when the Worker crashes. + +7. **Restart your Worker** by switching back to Terminal 2 and running the Worker command: + ```bash + python run_worker.py + ``` + +8. **Switch back to Terminal 3** where you ran `python run_workflow.py`. You'll see the program complete and you'll see the result message. + +You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. + +### Recover from an unknown error in an Activity + +Sometimes, the code has a bug. Let's simulate a bug in your code and see how Temporal reacts. + +1. **Make sure your Worker is stopped** before proceeding. + +2. **Edit the `activities.py` file** and uncomment the following line in the `deposit` method: + ```python + # Comment/uncomment the next line to simulate failures. + raise Exception("This deposit has failed.") + ``` + +3. **Save the file**. + +4. **Switch back to Terminal 2** and start the Worker: + ```bash + python run_worker.py + ``` + +5. **Switch to Terminal 3** and start the Workflow: + ```bash + python run_workflow.py + ``` + +6. **Let the Workflow run for a little bit**, then **switch back to Terminal 2** to see the Worker output. + + You'll see log output similar to this: + ``` + 2024/02/12 10:59:09 Withdrawing $250 from account 85-150. + 2024/02/12 10:59:09 Depositing $250 into account 43-812. + 2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. + 2024/02/12 10:59:10 Depositing $250 into account 43-812. + 2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. + 2024/02/12 10:59:12 Depositing $250 into account 43-812. + ``` + + The Workflow keeps retrying using the `RetryPolicy` specified when the Workflow first executes the Activity. + +7. **While the Activity continues to fail**, switch back to the Web UI to see more information about the process. You can see the state, the number of attempts run, and the next scheduled run time. + +8. **Pretend that you found a fix** for the issue. Switch the comments back to the `return` statements of the `deposit()` method in the `activities.py` file and save your changes. + +9. **To restart the Worker**, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: + ```bash + python run_worker.py + ``` + +10. **The Worker starts again**. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. + +11. **Switch back to Terminal 3** where your `run_workflow.py` program is running, and you'll see it complete: + ``` + Transfer complete. + Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} + Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} + ``` + +12. **Visit the Web UI again**, and you'll see the Workflow has completed successfully. + +**You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction!** + +## Conclusion + +You now know how to run a Temporal Workflow and understand some value Temporal offers. You explored Workflows and Activities, you started a Workflow Execution, and you ran a Worker to handle that execution. + +You also saw how Temporal recovers from failures and how it retries Activities. + +### Exploring the key advantages Temporal offers: + +1. **Temporal gives you full visibility** in the state of your Workflow and code execution. +2. **Temporal maintains the state** of your Workflow, even through server outages and errors. +3. **Temporal lets you time out and retry** Activity code using options that exist outside your business logic. +4. **Temporal enables you to perform "live debugging"** of your business logic while the Workflow is running. + +### Further exploration + +Try the following things before moving on to get more practice working with a Temporal application: + +- **Change the Retry Policy** in `workflows.py` so it only retries 1 time. Then change the `deposit()` Activity in `activities.py`, so it uses the `refund()` method. + - **Does the Workflow place the money back into the original account?** + +### Review + +Answer the following questions to see if you remember some of the more important concepts from this tutorial: + +**Why do we recommend defining a shared constant to store the Task Queue name?** + +Because the Task Queue name is specified in two different parts of the code (the first starts the Workflow and the second configures the Worker). If their values differ, the Worker and Temporal Cluster would not share the same Task Queue, and the Workflow Execution would not progress. + +**What do you have to do if you modify Activity code for a Workflow that is running?** + +Restart the Worker. + +## Continue Your Journey + +
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/sidebars.js b/sidebars.js index e44d82525a..aca6a6c503 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,7 +1,29 @@ module.exports = { documentation: [ "index", - "quickstarts", + { + type: "category", + label: "Quickstarts", + collapsed: false, + link: { + type: "doc", + id: "quickstarts", + }, + items: [ + { + type: "category", + label: "Build your First Basic Workflow", + collapsed: false, + link: { + type: "doc", + id: "build-your-first-basic-workflow", + }, + items: [ + "build-your-first-basic-workflow/python", + ], + }, + ], + }, { type: "link", label: "Courses and Tutorials", @@ -598,7 +620,7 @@ module.exports = { "references/server-options", "references/web-ui-configuration", "references/web-ui-environment-variables", - + ], }, { @@ -613,7 +635,7 @@ module.exports = { "troubleshooting/blob-size-limit-error", "troubleshooting/deadline-exceeded-error", "troubleshooting/last-connection-error", - "troubleshooting/performance-bottlenecks" + "troubleshooting/performance-bottlenecks" ], }, { @@ -642,37 +664,37 @@ module.exports = { "encyclopedia/temporal", "encyclopedia/temporal-sdks", { + type: "category", + label: "Workflows", + collapsed: true, + link: { + type: "doc", + id: "encyclopedia/workflow/workflow-overview", + }, + items: [ + "encyclopedia/workflow/workflow-definition", + { type: "category", - label: "Workflows", + label: "Workflow Execution", collapsed: true, link: { type: "doc", - id: "encyclopedia/workflow/workflow-overview", + id: "encyclopedia/workflow/workflow-execution/workflow-execution", }, items: [ - "encyclopedia/workflow/workflow-definition", - { - type: "category", - label: "Workflow Execution", - collapsed: true, - link: { - type: "doc", - id: "encyclopedia/workflow/workflow-execution/workflow-execution", - }, - items: [ - "encyclopedia/workflow/workflow-execution/workflowid-runid", - "encyclopedia/workflow/workflow-execution/event", - "encyclopedia/workflow/workflow-execution/continue-as-new", - "encyclopedia/workflow/workflow-execution/limits", - "encyclopedia/workflow/workflow-execution/timers-delays", - ], - }, - "encyclopedia/workflow/dynamic-handler", - "encyclopedia/workflow/workflow-schedule", - "encyclopedia/workflow/cron-job", - "encyclopedia/workflow/patching", + "encyclopedia/workflow/workflow-execution/workflowid-runid", + "encyclopedia/workflow/workflow-execution/event", + "encyclopedia/workflow/workflow-execution/continue-as-new", + "encyclopedia/workflow/workflow-execution/limits", + "encyclopedia/workflow/workflow-execution/timers-delays", ], }, + "encyclopedia/workflow/dynamic-handler", + "encyclopedia/workflow/workflow-schedule", + "encyclopedia/workflow/cron-job", + "encyclopedia/workflow/patching", + ], + }, { type: "category", label: "Activities", From b383f72ed36f2b55ab32ef387161df3e4b18e635 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Tue, 4 Nov 2025 15:03:51 -0600 Subject: [PATCH 02/40] first basic workflow python --- docs/build-your-first-basic-workflow.mdx | 31 +- .../python-failure-simulation.mdx | 173 +++++++++ .../python.mdx | 346 +++++------------- sidebars.js | 1 + src/components/elements/CallToAction.js | 25 +- .../elements/call-to-action.module.css | 82 ++--- static/img/moneytransfer/money-withdrawal.png | Bin 0 -> 4928 bytes static/img/moneytransfer/webuisample.png | Bin 0 -> 522898 bytes static/img/moneytransfer/yourapplication.png | Bin 0 -> 34705 bytes 9 files changed, 338 insertions(+), 320 deletions(-) create mode 100644 docs/build-your-first-basic-workflow/python-failure-simulation.mdx create mode 100644 static/img/moneytransfer/money-withdrawal.png create mode 100644 static/img/moneytransfer/webuisample.png create mode 100644 static/img/moneytransfer/yourapplication.png diff --git a/docs/build-your-first-basic-workflow.mdx b/docs/build-your-first-basic-workflow.mdx index 4c6aca8c4a..dbe6177a7a 100644 --- a/docs/build-your-first-basic-workflow.mdx +++ b/docs/build-your-first-basic-workflow.mdx @@ -119,20 +119,17 @@ import QuickstartCards from "@site/src/components/QuickstartCards"; ## What You'll Experience Step-by-Step -### 1. **Download and Explore** (5 minutes) -Clone the money transfer project and examine the Workflow and Activity definitions. - -### 2. **Run Your First Workflow** (3 minutes) -Start the application and see a successful money transfer in action. - -### 3. **Explore the Web UI** (2 minutes) -Use Temporal's built-in Web UI to inspect Workflow state and execution history. - -### 4. **Simulate Failures** (5 minutes) -Introduce failures and watch Temporal's automatic retry mechanisms in action. - -### 5. **Live Debug a Running Workflow** (5 minutes) -The magic moment - fix a bug in a running application without losing state or restarting the transaction. +### Part 1: **Build Your First Workflow** (~10 minutes) +- **Download and explore** the money transfer project +- **Run your first Workflow** and see a successful transaction +- **Explore the Web UI** to inspect Workflow state and execution history +- **Understand core concepts** like Workflows, Activities, and Workers + +### Part 2: **Test Reliability Features** (~10 minutes) +- **Simulate server crashes** and see workflows survive infrastructure failures +- **Introduce Activity failures** and watch automatic retry mechanisms +- **Live debug a running Workflow** - fix bugs without losing state +- **Experience Temporal's reliability superpowers** firsthand ## Key Advantages You'll Discover @@ -156,9 +153,9 @@ Write application code without worrying about infrastructure concerns like state ## Ready to Build? ## Alternative Learning Paths diff --git a/docs/build-your-first-basic-workflow/python-failure-simulation.mdx b/docs/build-your-first-basic-workflow/python-failure-simulation.mdx new file mode 100644 index 0000000000..fdcbc4cd22 --- /dev/null +++ b/docs/build-your-first-basic-workflow/python-failure-simulation.mdx @@ -0,0 +1,173 @@ +--- +id: python-failure-simulation +title: Simulate Failures with Temporal Python SDK +sidebar_label: Failure Simulation +description: Learn how Temporal handles failures, recovers from crashes, and enables live debugging of your Python workflows. +keywords: + - temporal + - python + - failure simulation + - crash recovery + - live debugging + - reliability +tags: + - Python + - SDK + - Testing + - Debugging +--- + +import { CallToAction } from "@site/src/components/elements/CallToAction"; + +# Simulate Failures with Temporal Python SDK + +So far, you've seen how Temporal executes a Workflow. In this section, you'll explore Temporal's ability to handle failures. + +## Recover from a server crash + +Unlike other solutions, Temporal is designed with failure in mind. To demonstrate this, you'll simulate some failures for the Workflow to recover from. + +**Start by simulating a server crash.** When any process crashes, you lose the progress of your code, unless you've designed a way to handle such failures. + +1. **Make sure your Worker is stopped** before proceeding. If the Worker is running, press `Ctrl+C` to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + python run_worker.py + ``` + +3. **In Terminal 3, start the Workflow:** + ```bash + python run_workflow.py + ``` + +4. **Inspect the Workflow Execution** using the Web UI. You can see the Worker is executing the Workflow and its Activities: + +5. **Return to Terminal 2** and stop the Worker by pressing `Ctrl+C`. + +6. **Switch back to the Web UI** and refresh the page. Your Workflow is still listed as "Running". + + The Workflow is still in progress because the Temporal Server maintains the state of the Workflow, even when the Worker crashes. + +7. **Restart your Worker** by switching back to Terminal 2 and running the Worker command: + ```bash + python run_worker.py + ``` + +8. **Switch back to Terminal 3** where you ran `python run_workflow.py`. You'll see the program complete and you'll see the result message. + +You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. + +## Recover from an unknown error in an Activity + +Sometimes, the code has a bug. Let's simulate a bug in your code and see how Temporal reacts. + +1. **Make sure your Worker is stopped** before proceeding. + +2. **Edit the `activities.py` file** and uncomment the following line in the `deposit` method: + ```python + # Comment/uncomment the next line to simulate failures. + raise Exception("This deposit has failed.") + ``` + +3. **Save the file**. + +4. **Switch back to Terminal 2** and start the Worker: + ```bash + python run_worker.py + ``` + +5. **Switch to Terminal 3** and start the Workflow: + ```bash + python run_workflow.py + ``` + +6. **Let the Workflow run for a little bit**, then **switch back to Terminal 2** to see the Worker output. + + You'll see log output similar to this: + ``` + 2024/02/12 10:59:09 Withdrawing $250 from account 85-150. + 2024/02/12 10:59:09 Depositing $250 into account 43-812. + 2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. + 2024/02/12 10:59:10 Depositing $250 into account 43-812. + 2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. + 2024/02/12 10:59:12 Depositing $250 into account 43-812. + ``` + + The Workflow keeps retrying using the `RetryPolicy` specified when the Workflow first executes the Activity. + +7. **While the Activity continues to fail**, switch back to the Web UI to see more information about the process. You can see the state, the number of attempts run, and the next scheduled run time. + +8. **Pretend that you found a fix** for the issue. Switch the comments back to the `return` statements of the `deposit()` method in the `activities.py` file and save your changes. + +9. **To restart the Worker**, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: + ```bash + python run_worker.py + ``` + +10. **The Worker starts again**. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. + +11. **Switch back to Terminal 3** where your `run_workflow.py` program is running, and you'll see it complete: + ``` + Transfer complete. + Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} + Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} + ``` + +12. **Visit the Web UI again**, and you'll see the Workflow has completed successfully. + +**You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction!** + +## Key Advantages Demonstrated + +### Exploring the key advantages Temporal offers: + +1. **Temporal gives you full visibility** in the state of your Workflow and code execution. +2. **Temporal maintains the state** of your Workflow, even through server outages and errors. +3. **Temporal lets you time out and retry** Activity code using options that exist outside your business logic. +4. **Temporal enables you to perform "live debugging"** of your business logic while the Workflow is running. + +## Further Exploration + +Try the following things before moving on to get more practice working with a Temporal application: + +- **Change the Retry Policy** in `workflows.py` so it only retries 1 time. Then change the `deposit()` Activity in `activities.py`, so it uses the `refund()` method. + - **Does the Workflow place the money back into the original account?** + +## Review Questions + +Answer the following questions to see if you remember some of the more important concepts from this tutorial: + +**Why do we recommend defining a shared constant to store the Task Queue name?** + +Because the Task Queue name is specified in two different parts of the code (the first starts the Workflow and the second configures the Worker). If their values differ, the Worker and Temporal Cluster would not share the same Task Queue, and the Workflow Execution would not progress. + +**What do you have to do if you modify Activity code for a Workflow that is running?** + +Restart the Worker. + +## Continue Your Learning + +
+
+ +
+
+ +
+
+ +
+
diff --git a/docs/build-your-first-basic-workflow/python.mdx b/docs/build-your-first-basic-workflow/python.mdx index 9285cce72e..ca6c95b6bc 100644 --- a/docs/build-your-first-basic-workflow/python.mdx +++ b/docs/build-your-first-basic-workflow/python.mdx @@ -20,32 +20,20 @@ hide_table_of_contents: false import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import { CallToAction } from "@site/src/components/elements/CallToAction"; -# Run your first Temporal application with the Python SDK - You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process.
⭐ Temporal beginner - ⏱️ ~10 minutes 🐍 Python SDK
-## 🙌 Goals - -- **Explore** Temporal's core terminology and concepts -- **Complete** several runs of a Temporal Workflow application using a Temporal Cluster and the Python SDK -- **Practice** reviewing the state of the Workflow -- **Understand** the inherent reliability of Workflow methods ## Introduction -The language-specific SDK, in this case the Temporal Python SDK, provides a comprehensive solution to the complexities that arise from modern application development. - -Temporal provides reliability primitives to ensure durable execution of your code, such as seamless and fault-tolerant application state tracking, automatic retries, timeouts, rollbacks due to process failures, and more. +In this tutorial, you'll run your first Temporal Application. +You'll use Temporal's Web UI for application state visibility, and then explore how Temporal helps you recover from a couple of common failures. -In this tutorial, you'll run your first Temporal Application. You'll use Temporal's Web UI for application state visibility, and then explore how Temporal helps you recover from a couple of common failures. - -## Prerequisites +### Prerequisites Before starting this tutorial: @@ -54,14 +42,19 @@ Before starting this tutorial: -## Application overview +### Application overview This project in this tutorial simulates a **money transfer application**, focusing on essential transactions such as withdrawals, deposits, and refunds. The importance of Temporal in this context lies in its ability to handle your code efficiently and reliably. +
+ Money Transfer Application Flow +
+ + In this sample application, money comes out of one account and goes into another. However, there are a few things that can go wrong with this process. If the withdrawal fails, then there is no need to try to make a deposit. But if the withdrawal succeeds, but the deposit fails, then the money needs to go back to the original account. One of Temporal's most important features is its ability to **maintain the application state when something fails**. When failures happen, Temporal recovers processes where they left off or rolls them back correctly. This allows you to focus on business logic, instead of writing application code to recover from failure. @@ -75,48 +68,38 @@ cd money-transfer-project-template-python }> -## Download the example application +### Download the example application The application you'll use in this tutorial is available in a GitHub repository. Open a new terminal window and use `git` to clone the repository, then change to the project directory. -:::tip -The repository for this tutorial is a GitHub Template repository, which means you could clone it to your own account and use it as the foundation for your own Temporal application. -::: Now that you've downloaded the project, let's dive into the code. + + +:::tip +The repository for this tutorial is a GitHub Template repository, which means you could clone it to your own account and use it as the foundation for your own Temporal application. +:::
Temporal Application Components
-
-
-
🔄
-
Workflow
-
Defines overall flow
-
-
-
-
⚙️
-
Activities
-
Business logic prone to failure
-
-
-
-
👷
-
Worker
-
Runs reliably and consistently
-
+
+ Your Temporal Application +
+
- + }> -## Explore the application's Workflow and Activity Definitions + + +### Let's Recap: Temporal's Application Structure The Temporal Application will consist of the following pieces: @@ -124,20 +107,21 @@ The Temporal Application will consist of the following pieces: 2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. 3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. -In the money transfer application, you have three Activity methods: `withdraw()`, `deposit()`, and `refund()`. These symbolize the movement of funds between accounts. + + +
:::important None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. ::: - +## Run a Money Transfer Flow +### Step 1: Workflow Definition - -
- workflows.py -
- +A Workflow Definition in Python uses the `@workflow.defn` decorator on the Workflow class to identify a Workflow. + +**workflows.py** +```python from datetime import timedelta from temporalio import workflow from temporalio.common import RetryPolicy @@ -192,26 +176,22 @@ class MoneyTransfer: except ActivityError as refund_error: workflow.logger.error("Refund failed: " + str(refund_error)) raise refund_error - - -}> - -## Workflow Definition - -A Workflow Definition in Python uses the `@workflow.defn` decorator on the Workflow class to identify a Workflow. +``` The `MoneyTransfer` class takes in transaction details. It executes Activities to withdraw and deposit the money. It also returns the results of the process. The asynchronous `run` method signature includes an `input` variable typed as `PaymentDetails`. This class stores details that the Workflow uses to perform the money transfer. -
+### Step 2: Activity Definition - -
- activities.py -
- +Each Activity method simulates calling an external service. +Each method can fail or succeed independently. + +In the money transfer application, you have three Activity methods: `withdraw()`, `deposit()`, and `refund()`. These symbolize the movement of funds between accounts. + + +**activities.py** +```python import asyncio from dataclasses import dataclass from temporalio import activity @@ -251,11 +231,10 @@ class BankingActivities: await asyncio.sleep(1) return "Refunded money back to account" - -
- shared.py -
- +``` + +**shared.py** +```python from dataclasses import dataclass MONEY_TRANSFER_TASK_QUEUE_NAME = "money-transfer" @@ -266,19 +245,46 @@ class PaymentDetails: target_account: str amount: int reference_id: str - - -}> - -## Activity Definition - -Each Activity method simulates calling an external service. Each method can fail or succeed independently. +``` The Activities are defined as static methods with the `@activity.defn` decorator. Note the `PaymentDetails` type, defined in `shared.py`. This contains the transaction information passed between the Workflow and Activities. -
+### Step 3: Start the Worker + +The Worker hosts the Workflow and Activity functions and executes them one at a time. + +The Worker is configured to execute Workflows and Activities from the Task Queue. Since the Worker and Workflow are both configured to use the same Task Queue, the Worker will execute any Workflows and Activities sent to the Task Queue. + +**run_worker.py** +```python +import asyncio +from temporalio.client import Client +from temporalio.worker import Worker +from activities import BankingActivities +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME +from workflows import MoneyTransfer + +async def main(): + client = await Client.connect("localhost:7233") + + worker = Worker( + client, + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + workflows=[MoneyTransfer], + activities=[BankingActivities.withdraw, BankingActivities.deposit, BankingActivities.refund], + ) + + await worker.run() + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +When you start the Worker, it begins polling the Temporal Server for work. @@ -309,9 +315,11 @@ Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. R }> -## Start the Workflow +### Run Your Application + +Now that your Worker is running and polling for tasks, you can start a Workflow execution. -In a new terminal window, run the following command to start the Workflow: +**In Terminal 3, start the Workflow:** The `run_workflow.py` script starts a Workflow Execution. Each time you run this file, the Temporal Server starts a new Workflow Execution. @@ -327,6 +335,7 @@ A Workflow Execution has exclusive access to its local state and executes concur /> }> + ## View the state of the Workflow with the Temporal Web UI The Temporal Web UI lets you see details about the Workflow you just ran. @@ -342,201 +351,34 @@ The Temporal Web UI lets you see details about the Workflow you just ran. - -
- run_worker.py -
- -import asyncio -from temporalio.client import Client -from temporalio.worker import Worker -from activities import BankingActivities -from shared import MONEY_TRANSFER_TASK_QUEUE_NAME -from workflows import MoneyTransfer - -async def main(): - client = await Client.connect("localhost:7233") - - worker = Worker( - client, - task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, - workflows=[MoneyTransfer], - activities=[BankingActivities.withdraw, BankingActivities.deposit, BankingActivities.refund], - ) - - await worker.run() - -if __name__ == "__main__": - asyncio.run(main()) - - -}> - -## Start the Worker - -The Worker hosts the Workflow and Activity functions and executes them one at a time. - -The Worker is configured to execute Workflows and Activities from the Task Queue. Since the Worker and Workflow are both configured to use the same Task Queue, the Worker will execute any Workflows and Activities sent to the Task Queue. - -When you start the Worker, it begins polling the Temporal Server for work. - -
+
+ Money Transfer Web UI +
-## Simulate failures - -So far, you've seen how Temporal executes a Workflow. In this section, you'll explore Temporal's ability to handle failures. - -### Recover from a server crash - -Unlike other solutions, Temporal is designed with failure in mind. To demonstrate this, you'll simulate some failures for the Workflow to recover from. - -**Start by simulating a server crash.** When any process crashes, you lose the progress of your code, unless you've designed a way to handle such failures. - -1. **Make sure your Worker is stopped** before proceeding. If the Worker is running, press `Ctrl+C` to stop it. - -2. **Start the Worker in Terminal 2:** - ```bash - python run_worker.py - ``` - -3. **In Terminal 3, start the Workflow:** - ```bash - python run_workflow.py - ``` - -4. **Inspect the Workflow Execution** using the Web UI. You can see the Worker is executing the Workflow and its Activities: - -5. **Return to Terminal 2** and stop the Worker by pressing `Ctrl+C`. - -6. **Switch back to the Web UI** and refresh the page. Your Workflow is still listed as "Running". - - The Workflow is still in progress because the Temporal Server maintains the state of the Workflow, even when the Worker crashes. - -7. **Restart your Worker** by switching back to Terminal 2 and running the Worker command: - ```bash - python run_worker.py - ``` - -8. **Switch back to Terminal 3** where you ran `python run_workflow.py`. You'll see the program complete and you'll see the result message. - -You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. - -### Recover from an unknown error in an Activity - -Sometimes, the code has a bug. Let's simulate a bug in your code and see how Temporal reacts. - -1. **Make sure your Worker is stopped** before proceeding. - -2. **Edit the `activities.py` file** and uncomment the following line in the `deposit` method: - ```python - # Comment/uncomment the next line to simulate failures. - raise Exception("This deposit has failed.") - ``` - -3. **Save the file**. - -4. **Switch back to Terminal 2** and start the Worker: - ```bash - python run_worker.py - ``` - -5. **Switch to Terminal 3** and start the Workflow: - ```bash - python run_workflow.py - ``` - -6. **Let the Workflow run for a little bit**, then **switch back to Terminal 2** to see the Worker output. - - You'll see log output similar to this: - ``` - 2024/02/12 10:59:09 Withdrawing $250 from account 85-150. - 2024/02/12 10:59:09 Depositing $250 into account 43-812. - 2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. - 2024/02/12 10:59:10 Depositing $250 into account 43-812. - 2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. - 2024/02/12 10:59:12 Depositing $250 into account 43-812. - ``` - - The Workflow keeps retrying using the `RetryPolicy` specified when the Workflow first executes the Activity. - -7. **While the Activity continues to fail**, switch back to the Web UI to see more information about the process. You can see the state, the number of attempts run, and the next scheduled run time. - -8. **Pretend that you found a fix** for the issue. Switch the comments back to the `return` statements of the `deposit()` method in the `activities.py` file and save your changes. - -9. **To restart the Worker**, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: - ```bash - python run_worker.py - ``` - -10. **The Worker starts again**. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. - -11. **Switch back to Terminal 3** where your `run_workflow.py` program is running, and you'll see it complete: - ``` - Transfer complete. - Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} - Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} - ``` - -12. **Visit the Web UI again**, and you'll see the Workflow has completed successfully. - -**You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction!** - -## Conclusion - -You now know how to run a Temporal Workflow and understand some value Temporal offers. You explored Workflows and Activities, you started a Workflow Execution, and you ran a Worker to handle that execution. - -You also saw how Temporal recovers from failures and how it retries Activities. - -### Exploring the key advantages Temporal offers: - -1. **Temporal gives you full visibility** in the state of your Workflow and code execution. -2. **Temporal maintains the state** of your Workflow, even through server outages and errors. -3. **Temporal lets you time out and retry** Activity code using options that exist outside your business logic. -4. **Temporal enables you to perform "live debugging"** of your business logic while the Workflow is running. - -### Further exploration - -Try the following things before moving on to get more practice working with a Temporal application: - -- **Change the Retry Policy** in `workflows.py` so it only retries 1 time. Then change the `deposit()` Activity in `activities.py`, so it uses the `refund()` method. - - **Does the Workflow place the money back into the original account?** - -### Review - -Answer the following questions to see if you remember some of the more important concepts from this tutorial: - -**Why do we recommend defining a shared constant to store the Task Queue name?** - -Because the Task Queue name is specified in two different parts of the code (the first starts the Workflow and the second configures the Worker). If their values differ, the Worker and Temporal Cluster would not share the same Task Queue, and the Workflow Execution would not progress. - -**What do you have to do if you modify Activity code for a Workflow that is running?** -Restart the Worker. -## Continue Your Journey
diff --git a/sidebars.js b/sidebars.js index aca6a6c503..bc70efd416 100644 --- a/sidebars.js +++ b/sidebars.js @@ -20,6 +20,7 @@ module.exports = { }, items: [ "build-your-first-basic-workflow/python", + "build-your-first-basic-workflow/python-failure-simulation", ], }, ], diff --git a/src/components/elements/CallToAction.js b/src/components/elements/CallToAction.js index 7be158145a..594148400d 100644 --- a/src/components/elements/CallToAction.js +++ b/src/components/elements/CallToAction.js @@ -1,13 +1,18 @@ import React from 'react'; import styles from './call-to-action.module.css'; - export const CallToAction = ({ href, children }) => { - return ( - -
- {children} -
-
-
- ); - }; \ No newline at end of file +export const CallToAction = ({ href, children, buttonText, description }) => { + return ( + +
+ {children || ( + <> + {buttonText &&

{buttonText}

} + {description &&

{description}

} + + )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/elements/call-to-action.module.css b/src/components/elements/call-to-action.module.css index f836f9d393..dc502439b7 100644 --- a/src/components/elements/call-to-action.module.css +++ b/src/components/elements/call-to-action.module.css @@ -1,42 +1,42 @@ .cta { - display: flex; - align-items: center; - justify-content: space-between; - background: linear-gradient(to right, #2e2e60, #1e1e40); - border: 1px solid #3b3b7c; - border-radius: 8px; - padding: 1.5rem; - margin: 2rem 0; - color: #e6e6fa; - text-decoration: none; - transition: all 0.3s ease; - } - - .cta:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - text-decoration: none; - color: #e6e6fa; - } - - .content { - flex: 1; - } - - .content h3 { - margin: 0; - font-size: 1.2rem; - color: #e6e6fa; - } - - .content p { - margin: 0.5rem 0 0 0; - color: #b4b4d4; - font-size: 0.95rem; - } - - .arrow { - font-size: 1.5rem; - margin-left: 1rem; - color: #8585ff; - } \ No newline at end of file + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(to right, #2e2e60, #1e1e40); + border: 1px solid #3b3b7c; + border-radius: 8px; + padding: 1.5rem; + margin: 2rem 0; + color: #e6e6fa; + text-decoration: none; + transition: all 0.3s ease; +} + +.cta:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + text-decoration: none; + color: #e6e6fa; +} + +.content { + flex: 1; +} + +.content h3 { + margin: 0; + font-size: 1.2rem; + color: white; +} + +.content p { + margin: 0.5rem 0 0 0; + color: white; + font-size: 0.9rem; +} + +.arrow { + font-size: 1.5rem; + margin-left: 1rem; + color: #8585ff; +} \ No newline at end of file diff --git a/static/img/moneytransfer/money-withdrawal.png b/static/img/moneytransfer/money-withdrawal.png new file mode 100644 index 0000000000000000000000000000000000000000..07bfa088009a7194e8d2e5fbd474d9b1275e0124 GIT binary patch literal 4928 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�<>1;(bv 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%0D6C}vhDqGG~cZ5Y^Y^McXpR6 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

?q@jn2s9Uf)f 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

+ + ## Introduction -In this tutorial, you'll run your first Temporal Application. -You'll use Temporal's Web UI for application state visibility, and then explore how Temporal helps you recover from a couple of common failures. +In this tutorial, you'll build and run your first Temporal Application. +You'll learn how to construct Workflows and Activities, understand the core building blocks, and see you can get full visibility into its execution. ### Prerequisites @@ -46,9 +55,11 @@ Before starting this tutorial: description="Run through the Quickstart to get your set up complete." /> -### Application overview +### What You'll Build + +You'll construct a **money transfer application** from the ground up, learning to design and implement essential transactions such as withdrawals, deposits, and refunds using Temporal's powerful building blocks. -This project in this tutorial simulates a **money transfer application**, focusing on essential transactions such as withdrawals, deposits, and refunds. The importance of Temporal in this context lies in its ability to handle your code efficiently and reliably. +**Why This Application?** It demonstrates real-world complexity while teaching you how to build reliable, fault-tolerant systems that handle money safely.
Money Transfer Application Flow @@ -281,11 +292,9 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) ``` - - - When you start the Worker, it begins polling the Temporal Server for work. +
@@ -307,9 +316,12 @@ python run_workflow.py
- ✅ Expected Success Output: + Expected Success Output: + + Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345) +
@@ -323,14 +335,19 @@ Now that your Worker is running and polling for tasks, you can start a Workflow The `run_workflow.py` script starts a Workflow Execution. Each time you run this file, the Temporal Server starts a new Workflow Execution. -A Workflow Execution has exclusive access to its local state and executes concurrently to all other Workflow Executions. + }> @@ -359,6 +376,7 @@ The Temporal Web UI lets you see details about the Workflow you just ran. +## Continue Your Learning
+}> -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". +### Step 2: Start the Workflow - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. +Now in Terminal 3, start the Workflow. Check the Web UI - you'll see your Worker busy executing the Workflow and its Activities. -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - dotnet run --project MoneyTransferWorker - ``` + -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + - + + The Crash Test +

Go back to Terminal 2 and kill the Worker with Ctrl+C

+ +}> - +### Step 3: Simulate the Crash -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. +**The moment of truth!** Kill your Worker while it's processing the transaction. -2. **Start the Worker in Terminal 2:** - ```bash - bundle exec ruby worker.rb - ``` +**Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running"! -3. **Now in Terminal 3, start the Workflow:** - ```bash - bundle exec ruby starter.rb - ``` +That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. -4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. +
+ + +
+ Terminal 2 - Recovery +
+ + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + + +}> -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". +### Step 4: Bring Your Worker Back - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. +Restart your Worker in Terminal 2. Watch Terminal 3 - you'll see the Workflow finish up and show the result! -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - bundle exec ruby worker.rb - ``` + -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! +
- + - - **Mission Accomplished!** You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. From bddc1ce807bf70eb9093692618a8de6c2809e1d7 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 19 Nov 2025 13:33:09 -0600 Subject: [PATCH 19/40] experiment 2 base by sdk --- .../failure-simulation.mdx | 409 +++++++++++++++--- 1 file changed, 360 insertions(+), 49 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index df8a7b260e..575f885225 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -306,37 +306,56 @@ This is like having version control for your running application state. ### Instructions -1. **Make sure your Worker is stopped** before proceeding. + + -2. **Edit the `activities.py` file** and uncomment the following line in the `deposit` method: - ```python - # Comment/uncomment the next line to simulate failures. - raise Exception("This deposit has failed.") - ``` +This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. -3. **Save the file**. +To test this out and see how Temporal responds, you'll simulate a bug in the `deposit()` Activity method. -4. **Switch back to Terminal 2** and start the Worker: - ```bash - python run_worker.py - ``` +Let your Workflow continue to run but don't start the Worker yet. -5. **Switch to Terminal 3** and start the Workflow: - ```bash - python run_workflow.py - ``` +Open the `activities.py` file and switch out the comments on the return statements so that the `deposit()` method returns an error: -6. **Let the Workflow run for a little bit**, then **switch back to Terminal 2** to see the Worker output. +**activities.py** +```python +@activity.defn +async def deposit(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-deposit" + try: + confirmation = await asyncio.to_thread( + self.bank.deposit, data.target_account, data.amount, reference_id + ) + """ + confirmation = await asyncio.to_thread( + self.bank.deposit_that_fails, + data.target_account, + data.amount, + reference_id, + ) + """ + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Deposit failed") + raise +``` - +Save your changes and switch to the terminal that was running your Worker. -You'll see log output similar to this: +Start the Worker again: +```bash +python run_worker.py ``` + +Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the `withdraw()` Activity method, but it errors when it attempts the `deposit()` Activity method. + +The important thing to note here is that the Worker keeps retrying the `deposit()` method: + +``` +2024/02/12 10:59:09 INFO No logger configured for temporal client. Created default one. +2024/02/12 10:59:09 INFO Started Worker Namespace default TaskQueue money-transfer WorkerID 77310@temporal.local@ 2024/02/12 10:59:09 Withdrawing $250 from account 85-150. 2024/02/12 10:59:09 Depositing $250 into account 43-812. 2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. @@ -345,43 +364,335 @@ You'll see log output similar to this: 2024/02/12 10:59:12 Depositing $250 into account 43-812. ``` -The Workflow keeps retrying using the `RetryPolicy` specified when the Workflow first executes the Activity. +The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. - +You can view more information about the process in the Temporal Web UI. Click the Workflow. You'll see more details including the state, the number of attempts run, and the next scheduled run time. -7. **While the Activity continues to fail**, switch back to the Web UI to see more information about the process. You can see the state, the number of attempts run, and the next scheduled run time. +Your Workflow is running, but only the `withdraw()` Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. -8. **Pretend that you found a fix** for the issue. Switch the comments back to the `return` statements of the `deposit()` method in the `activities.py` file and save your changes. +With Temporal, you can debug and resolve the issue while the Workflow is running. - +Pretend that you found a fix for the issue. Switch the comments back to the return statements of the `deposit()` method in the `activities.py` file and save your changes. -```python -# BROKEN VERSION: -# raise Exception("This deposit has failed.") +How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + +To restart the Worker, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: +```bash +python run_worker.py +``` + +The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. + +Switch back to the terminal where your `run_workflow.py` program is running, and you'll see it complete: +``` +Transfer complete. +Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} +Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} +``` + +Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! + + + + + +This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. + +To test this out and see how Temporal responds, you'll simulate a bug in the `Deposit()` Activity function. Let your Workflow continue to run but don't start the Worker yet. + +Open the `activity.go` file and switch out the comments on the return statements so that the `Deposit()` function returns an error: + +**activity.go** +```go +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Depositing $%d into account %s.\n\n", + data.Amount, + data.TargetAccount, + ) + + referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + // Uncomment the next line and comment the one after that to simulate an unknown failure + confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + // confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + return confirmation, err +} +``` + +Ensure you're calling `bank.DepositThatFails`. + +Save your changes and switch to the terminal that was running your Worker. Start the Worker again: +```bash +go run worker/main.go +``` + +You will see the Worker complete the `Withdraw()` Activity function, but it errors when it attempts the `Deposit()` Activity function. The important thing to note here is that the Worker keeps retrying the `Deposit()` function: + +``` +2022/11/14 10:59:09 INFO No logger configured for temporal client. Created default one. +2022/11/14 10:59:09 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 77310@temporal.local@ +2022/11/14 10:59:09 Withdrawing $250 from account 85-150. +2022/11/14 10:59:09 Depositing $250 into account 43-812. +2022/11/14 10:59:09 ERROR Activity error. This deposit has failed. +2022/11/14 10:59:10 Depositing $250 into account 43-812. +2022/11/14 10:59:10 ERROR Activity error. This deposit has failed. +2022/11/14 10:59:12 Depositing $250 into account 43-812. +``` + +The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + +Your Workflow is running, but only the `Withdraw()` Activity function has succeeded. In any other application, the whole process would likely have to be abandoned and rolled back. + +With Temporal, you can debug and fix the issue while the Workflow is running. + +Pretend that you found a fix for the issue. Switch the comments back on the return statements of the `Deposit()` function in the `activity.go` file and save your changes. + +How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + +First, cancel the currently running worker with `Ctrl+C`, then restart the worker: +```bash +go run worker/main.go +``` + +The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `Deposit()` Activity function. + +Switch back to the terminal where your `start/main.go` program is running, and you'll see it complete: +``` +Transfer complete (transaction IDs: W1779185060, D1779185060) +``` + +Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. + + + + + +This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. + +To test this out and see how Temporal responds, you'll simulate a bug in the deposit Activity method. + +Try it out by following these steps: + +Make sure your Worker is stopped before proceeding, so your Workflow doesn't finish. Switch to the terminal that's running your Worker and stop it by pressing `Ctrl+C`. + +Open the `AccountActivityImpl` file and modify the deposit method so `activityShouldSucceed` is set to false. + +Save your changes and switch to the terminal that was running your Worker. + +Verify the Workflow is running in the Web UI. If finished, restart it using the Maven command. + +Start the Worker again: +```bash +mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null +mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn +``` + +Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the withdraw Activity method, but it errors when it attempts the deposit Activity method. + +The important thing to note here is that the Worker keeps retrying the deposit method: + +``` +Withdrawing $32 from account 612849675. +[ReferenceId: d3d9bcf0-a897-4326] +Deposit failed +Deposit failed +Deposit failed +Deposit failed +``` + +The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + +Your Workflow is running, but only the withdraw Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. + +With Temporal, you can debug and resolve the issue while the Workflow is running. + +Pretend that you found a fix for the issue. Switch `activityShouldSucceed` back to true and save your changes. + +How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + +To restart the Worker, go to the terminal where the Worker is running and cancel the Worker with `Ctrl+C`. Then restart the Worker by running the following command: +```bash +mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null +mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn +``` + +The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled deposit Activity method: -# FIXED VERSION: -return "Deposited money into account" ``` +Depositing $32 into account 872878204. +[ReferenceId: d3d9bcf0-a897-4326] +[d3d9bcf0-a897-4326] Transaction succeeded. +``` + +Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! + + + + + +This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. + +To test this out and see how Temporal responds, you'll simulate a bug in the deposit Activity function. Let your Workflow continue to run but don't start the Worker yet. + +Open the `activities.ts` file and switch out the comments on the return statements so that the deposit function returns an error: + +Ensure you're calling `bank2.depositThatFails`. + +Save your changes and switch to the terminal that was running your Worker. Start the Worker again: +```bash +npm run worker +``` + +You will see the Worker complete the withdraw Activity function, but it errors when it attempts the deposit Activity function. The important thing to note here is that the Worker keeps retrying the deposit function: + +``` +2023-10-11T19:03:25.778Z [INFO] Worker state changed { state: 'RUNNING' } +Withdrawing $400 from account 85-150. +Depositing $400 into account 43-812. +2023-10-11T19:03:29.445Z [WARN] Activity failed { + attempt: 1, + activityType: 'deposit', + taskQueue: 'money-transfer', + error: Error: This deposit has failed +} +Depositing $400 into account 43-812. +``` + +The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + +You can view more information about the process in the Temporal Web UI. Click the Workflow. You'll see more details including the state, the number of times it has been attempted, and the next scheduled run time. + +Traditionally, you're forced to implement timeout and retry logic within the service code itself. This is repetitive and prone to errors. With Temporal, you can specify timeout configurations in the Workflow code as Activity options. + +Your Workflow is running, but only the withdraw Activity function has succeeded. In any other application, the whole process would likely have to be abandoned and rolled back. + +With Temporal, you can debug and fix the issue while the Workflow is running. + +Pretend that you found a fix for the issue. Switch the comments back on the return statements of the deposit function in the `activities.ts` file and save your changes. + +How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + +First, cancel the currently running worker with `Ctrl+C`, then restart the worker: +```bash +npm run worker +``` + +The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled deposit Activity function. + +Switch back to the terminal where your `npm run client` program is running, and you'll see it complete: +``` +Transfer complete (transaction IDs: W3436600150, D9270097234) +``` + +Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. + + + + + +This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. + +To test this out and see how Temporal responds, you'll simulate a bug in the `DepositAsync()` Activity method. + +Let your Workflow continue to run but don't start the Worker yet. + +Open the `Activities.cs` file and switch out the comments on the return statements so that the `DepositAsync()` method throws an exception: + +**MoneyTransferWorker/Activities.cs** +```csharp +[Activity] +public static async Task DepositAsync(PaymentDetails details) +{ + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + // Uncomment below and comment out the try-catch block below to simulate unknown failure + return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); + + /* + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } + */ +} +``` + +Save your changes and switch to the terminal that was running your Worker. + +Start the Worker again: +```bash +dotnet run --project MoneyTransferWorker +``` + +Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the `WithdrawAsync()` Activity method, but it errors when it attempts the `DepositAsync()` Activity method. + +The important thing to note here is that the Worker keeps retrying the `DepositAsync()` method: + +``` +Running worker... +Withdrawing $400 from account 85-150. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +``` + +The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + +Your Workflow is running, but only the `WithdrawAsync()` Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. + +With Temporal, you can debug and resolve the issue while the Workflow is running. + +Pretend that you found a fix for the issue. Switch the comments back to the return statements of the `DepositAsync()` method in the `Activities.cs` file and save your changes. + +How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + +To restart the Worker, go to the terminal where the Worker is running and cancel the Worker with `Ctrl+C`, then restart the Worker by running: +```bash +dotnet run --project MoneyTransferWorker +``` + +The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `DepositAsync()` Activity method. + +Switch back to the terminal where your `Program.cs` file in MoneyTransferClient folder is running, and you'll see it complete: +``` +Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) +``` + +Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! + + + + + +The Withdraw Activity contains a bug, although it hasn't been a problem yet because that statement is currently commented out. You will now uncomment it to expose the bug. You'll see that this causes the Activity to fail, but you'll also see that it retries automatically. More importantly, you'll observe that after you fix the bug, the Workflow Execution that was failing will complete successfully. + +Try it out by following these steps: + +1. **Whenever you modify the code, you must restart the Worker** for the changes to take effect. Press `Ctrl+C` in the terminal where your Worker is running to stop the Worker so that you can introduce the bug in the next step. + +2. **Edit the `activities.rb` file**, uncomment the line in the Withdraw Activity that causes a divide-by-zero error, and save the change + +3. **Start the Worker again** by running `bundle exec ruby worker.rb` + +4. **Start a new Workflow Execution** by running `bundle exec ruby starter.rb` + +5. **Go to the main page** in the Temporal Web UI and click the Workflow Execution you just started to view its detail page. You should see that the Withdraw Activity is failing. Click the Pending Activities tab to see the cause, and then click the History tab to return to the previous view. + +6. **Edit the `activities.rb` file**, comment out the line with the divide-by-zero error, and then save the change. -9. **To restart the Worker**, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: - ```bash - python run_worker.py - ``` +7. **Press `Ctrl+C`** in the terminal where your Worker is running and then run `bundle exec ruby worker.rb` to start it again. The Worker will now use the code that contains the change you made in the previous step. -10. **The Worker starts again**. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. +8. **You should see that the Workflow Execution completes successfully.** This will be visible both in the Web UI and in the terminal where you started the Workflow Execution. -11. **Switch back to Terminal 3** where your `run_workflow.py` program is running, and you'll see it complete: - ``` - Transfer complete. - Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} - Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} - ``` + -12. **Visit the Web UI again**, and you'll see the Workflow has completed successfully. + **Mission Accomplished!** You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! From 713e2203a3241b25061a01b065d21340c5e6f70f Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 19 Nov 2025 16:01:02 -0600 Subject: [PATCH 20/40] restructured live bug fix section --- .../failure-simulation.mdx | 576 ++++++++++++------ 1 file changed, 376 insertions(+), 200 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 575f885225..2d1ead39c1 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -22,7 +22,6 @@ import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { WorkflowDiagram } from "@site/src/components/WorkflowDiagram"; import { RetryCounter } from "@site/src/components/RetryCounter"; import { TemporalCheckbox } from "@site/src/components/TemporalCheckbox"; -import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import SdkTabs from "@site/src/components/elements/SdkTabs"; import { FaPython, FaJava } from 'react-icons/fa'; import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; @@ -36,6 +35,7 @@ export const TUTORIAL_LANGUAGE_ORDER = [ { key: 'dotnet', label: '.NET', icon: SiDotnet }, { key: 'rb', label: 'Ruby', icon: SiRuby }, ]; +import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import { CodeComparison } from "@site/src/components/CodeComparison"; # Part 2: Simulate Failures @@ -304,18 +304,24 @@ This is like having version control for your running application state. -### Instructions +## Instructions - - +### Step 1: Stop Your Worker -This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. +Before we can simulate a failure, we need to stop the current Worker process. This allows us to modify the Activity code safely. -To test this out and see how Temporal responds, you'll simulate a bug in the `deposit()` Activity method. +In Terminal 2 (where your Worker is running), stop it with `Ctrl+C`. -Let your Workflow continue to run but don't start the Worker yet. +**What's happening?** You're about to modify Activity code to introduce a deliberate failure. The Worker process needs to restart to pick up code changes, but the Workflow execution will continue running in Temporal's service - this separation between execution state and code is a core Temporal concept. + +### Step 2: Introduce the Bug + +Now we'll intentionally introduce a failure in the deposit Activity to simulate real-world scenarios like network timeouts, database connection issues, or external service failures. This demonstrates how Temporal handles partial failures in multi-step processes. + + + -Open the `activities.py` file and switch out the comments on the return statements so that the `deposit()` method returns an error: +Find the `deposit()` method and **uncomment the failing line** while **commenting out the working line**: **activities.py** ```python @@ -323,17 +329,18 @@ Open the `activities.py` file and switch out the comments on the return statemen async def deposit(self, data: PaymentDetails) -> str: reference_id = f"{data.reference_id}-deposit" try: - confirmation = await asyncio.to_thread( - self.bank.deposit, data.target_account, data.amount, reference_id - ) - """ + # Comment out this working line: + # confirmation = await asyncio.to_thread( + # self.bank.deposit, data.target_account, data.amount, reference_id + # ) + + # Uncomment this failing line: confirmation = await asyncio.to_thread( self.bank.deposit_that_fails, data.target_account, data.amount, reference_id, ) - """ return confirmation except InvalidAccountError: raise @@ -342,65 +349,13 @@ async def deposit(self, data: PaymentDetails) -> str: raise ``` -Save your changes and switch to the terminal that was running your Worker. - -Start the Worker again: -```bash -python run_worker.py -``` - -Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the `withdraw()` Activity method, but it errors when it attempts the `deposit()` Activity method. - -The important thing to note here is that the Worker keeps retrying the `deposit()` method: - -``` -2024/02/12 10:59:09 INFO No logger configured for temporal client. Created default one. -2024/02/12 10:59:09 INFO Started Worker Namespace default TaskQueue money-transfer WorkerID 77310@temporal.local@ -2024/02/12 10:59:09 Withdrawing $250 from account 85-150. -2024/02/12 10:59:09 Depositing $250 into account 43-812. -2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. -2024/02/12 10:59:10 Depositing $250 into account 43-812. -2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. -2024/02/12 10:59:12 Depositing $250 into account 43-812. -``` - -The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. - -You can view more information about the process in the Temporal Web UI. Click the Workflow. You'll see more details including the state, the number of attempts run, and the next scheduled run time. - -Your Workflow is running, but only the `withdraw()` Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. - -With Temporal, you can debug and resolve the issue while the Workflow is running. - -Pretend that you found a fix for the issue. Switch the comments back to the return statements of the `deposit()` method in the `activities.py` file and save your changes. - -How can you possibly update a Workflow that's already halfway complete? You restart the Worker. - -To restart the Worker, cancel the currently running worker with `Ctrl+C`, then restart the Worker by running: -```bash -python run_worker.py -``` - -The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `deposit()` Activity method. - -Switch back to the terminal where your `run_workflow.py` program is running, and you'll see it complete: -``` -Transfer complete. -Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} -Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} -``` - -Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. -This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. - -To test this out and see how Temporal responds, you'll simulate a bug in the `Deposit()` Activity function. Let your Workflow continue to run but don't start the Worker yet. - -Open the `activity.go` file and switch out the comments on the return statements so that the `Deposit()` function returns an error: +Find the `Deposit()` function and **uncomment the failing line** while **commenting out the working line**: **activity.go** ```go @@ -412,138 +367,188 @@ func Deposit(ctx context.Context, data PaymentDetails) (string, error) { referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) bank := BankingService{"bank-api.example.com"} - // Uncomment the next line and comment the one after that to simulate an unknown failure + + // Uncomment this failing line: confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + // Comment out this working line: // confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + return confirmation, err } ``` -Ensure you're calling `bank.DepositThatFails`. +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. -Save your changes and switch to the terminal that was running your Worker. Start the Worker again: -```bash -go run worker/main.go -``` + -You will see the Worker complete the `Withdraw()` Activity function, but it errors when it attempts the `Deposit()` Activity function. The important thing to note here is that the Worker keeps retrying the `Deposit()` function: + -``` -2022/11/14 10:59:09 INFO No logger configured for temporal client. Created default one. -2022/11/14 10:59:09 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 77310@temporal.local@ -2022/11/14 10:59:09 Withdrawing $250 from account 85-150. -2022/11/14 10:59:09 Depositing $250 into account 43-812. -2022/11/14 10:59:09 ERROR Activity error. This deposit has failed. -2022/11/14 10:59:10 Depositing $250 into account 43-812. -2022/11/14 10:59:10 ERROR Activity error. This deposit has failed. -2022/11/14 10:59:12 Depositing $250 into account 43-812. -``` +Find the `deposit()` method and **change `activityShouldSucceed` to `false`**: -The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. +**AccountActivityImpl.java** +```java +public String deposit(PaymentDetails details) { + // Change this to false to simulate failure: + boolean activityShouldSucceed = false; + + // ... rest of your method +} +``` -Your Workflow is running, but only the `Withdraw()` Activity function has succeeded. In any other application, the whole process would likely have to be abandoned and rolled back. +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. -With Temporal, you can debug and fix the issue while the Workflow is running. + -Pretend that you found a fix for the issue. Switch the comments back on the return statements of the `Deposit()` function in the `activity.go` file and save your changes. + -How can you possibly update a Workflow that's already halfway complete? You restart the Worker. +Find the `deposit()` function and **uncomment the failing line** while **commenting out the working line**: -First, cancel the currently running worker with `Ctrl+C`, then restart the worker: -```bash -go run worker/main.go +**activities.ts** +```typescript +export async function deposit(details: PaymentDetails): Promise { + // Comment out this working line: + // return await bank.deposit(details.targetAccount, details.amount, details.referenceId); + + // Uncomment this failing line: + return await bank.depositThatFails(details.targetAccount, details.amount, details.referenceId); +} ``` -The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `Deposit()` Activity function. +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. -Switch back to the terminal where your `start/main.go` program is running, and you'll see it complete: -``` -Transfer complete (transaction IDs: W1779185060, D1779185060) + + + + +Find the `DepositAsync()` method and **uncomment the failing line** while **commenting out the working block**: + +**MoneyTransferWorker/Activities.cs** +```csharp +[Activity] +public static async Task DepositAsync(PaymentDetails details) +{ + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + // Uncomment this failing line: + return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); + + // Comment out this working block: + /* + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } + */ +} ``` -Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. - + - + -This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. +Find the `deposit` method and **uncomment the failing line** that causes a divide-by-zero error: -To test this out and see how Temporal responds, you'll simulate a bug in the deposit Activity method. +**activities.rb** +```ruby +def deposit(details) + # Uncomment this line to introduce the bug: + result = 100 / 0 # This will cause a divide-by-zero error + + # Your existing deposit logic here... +end +``` -Try it out by following these steps: +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. -Make sure your Worker is stopped before proceeding, so your Workflow doesn't finish. Switch to the terminal that's running your Worker and stop it by pressing `Ctrl+C`. + + -Open the `AccountActivityImpl` file and modify the deposit method so `activityShouldSucceed` is set to false. +### Step 3: Start Worker & Observe Retry Behavior -Save your changes and switch to the terminal that was running your Worker. +Now let's see how Temporal handles this failure. When you start your Worker, it will execute the withdraw Activity successfully, but hit the failing deposit Activity. Instead of the entire Workflow failing permanently, Temporal will retry the failed Activity according to your retry policy. -Verify the Workflow is running in the Web UI. If finished, restart it using the Maven command. + + -Start the Worker again: ```bash -mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null -mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn +python run_worker.py ``` -Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the withdraw Activity method, but it errors when it attempts the deposit Activity method. - -The important thing to note here is that the Worker keeps retrying the deposit method: +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically ``` -Withdrawing $32 from account 612849675. -[ReferenceId: d3d9bcf0-a897-4326] -Deposit failed -Deposit failed -Deposit failed -Deposit failed +2024/02/12 10:59:09 Withdrawing $250 from account 85-150. +2024/02/12 10:59:09 Depositing $250 into account 43-812. +2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. +2024/02/12 10:59:10 Depositing $250 into account 43-812. +2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. +``` + + + + + +```bash +go run worker/main.go ``` -The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. +**Here's what you'll see:** +- The `Withdraw()` Activity completes successfully +- The `Deposit()` Activity fails and retries automatically -Your Workflow is running, but only the withdraw Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. +``` +2022/11/14 10:59:09 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE +2022/11/14 10:59:09 Withdrawing $250 from account 85-150. +2022/11/14 10:59:09 Depositing $250 into account 43-812. +2022/11/14 10:59:09 ERROR Activity error. This deposit has failed. +2022/11/14 10:59:10 Depositing $250 into account 43-812. +2022/11/14 10:59:10 ERROR Activity error. This deposit has failed. +``` -With Temporal, you can debug and resolve the issue while the Workflow is running. + -Pretend that you found a fix for the issue. Switch `activityShouldSucceed` back to true and save your changes. + -How can you possibly update a Workflow that's already halfway complete? You restart the Worker. +Make sure your Workflow is still running in the Web UI, then start your Worker: -To restart the Worker, go to the terminal where the Worker is running and cancel the Worker with `Ctrl+C`. Then restart the Worker by running the following command: ```bash mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn ``` -The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled deposit Activity method: +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically ``` -Depositing $32 into account 872878204. +Withdrawing $32 from account 612849675. [ReferenceId: d3d9bcf0-a897-4326] -[d3d9bcf0-a897-4326] Transaction succeeded. +Deposit failed +Deposit failed +Deposit failed +Deposit failed ``` -Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! - -This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. - -To test this out and see how Temporal responds, you'll simulate a bug in the deposit Activity function. Let your Workflow continue to run but don't start the Worker yet. - -Open the `activities.ts` file and switch out the comments on the return statements so that the deposit function returns an error: - -Ensure you're calling `bank2.depositThatFails`. - -Save your changes and switch to the terminal that was running your Worker. Start the Worker again: ```bash npm run worker ``` -You will see the Worker complete the withdraw Activity function, but it errors when it attempts the deposit Activity function. The important thing to note here is that the Worker keeps retrying the deposit function: +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically ``` 2023-10-11T19:03:25.778Z [INFO] Worker state changed { state: 'RUNNING' } @@ -558,45 +563,151 @@ Depositing $400 into account 43-812. Depositing $400 into account 43-812. ``` -The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + + + + +```bash +dotnet run --project MoneyTransferWorker +``` -You can view more information about the process in the Temporal Web UI. Click the Workflow. You'll see more details including the state, the number of times it has been attempted, and the next scheduled run time. +**Here's what you'll see:** +- The `WithdrawAsync()` Activity completes successfully +- The `DepositAsync()` Activity fails and retries automatically -Traditionally, you're forced to implement timeout and retry logic within the service code itself. This is repetitive and prone to errors. With Temporal, you can specify timeout configurations in the Workflow code as Activity options. +``` +Running worker... +Withdrawing $400 from account 85-150. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +Depositing $400 into account 43-812. +``` -Your Workflow is running, but only the withdraw Activity function has succeeded. In any other application, the whole process would likely have to be abandoned and rolled back. + -With Temporal, you can debug and fix the issue while the Workflow is running. + -Pretend that you found a fix for the issue. Switch the comments back on the return statements of the deposit function in the `activities.ts` file and save your changes. +```bash +bundle exec ruby worker.rb +``` -How can you possibly update a Workflow that's already halfway complete? You restart the Worker. +In another terminal, start a new Workflow: -First, cancel the currently running worker with `Ctrl+C`, then restart the worker: ```bash -npm run worker +bundle exec ruby starter.rb ``` -The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled deposit Activity function. +**Here's what you'll see:** +- The `withdraw` Activity completes successfully +- The `deposit` Activity fails and retries automatically + +Check the Web UI - click on your Workflow to see the failure details and retry attempts. + + + + +**Key observation:** Your Workflow isn't stuck or terminated. Temporal automatically retries the failed Activity according to your configured retry policy, while maintaining the overall Workflow state. The successful withdraw Activity doesn't get re-executed - only the failed deposit Activity is retried. + +### Step 4: Fix the Bug + +Here's where Temporal really shines - you can fix bugs in production code while Workflows are still executing. The Workflow state is preserved in Temporal's durable storage, so you can deploy fixes and let the retry mechanism pick up your corrected code. -Switch back to the terminal where your `npm run client` program is running, and you'll see it complete: + + + +Go back to `activities.py` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activities.py** +```python +@activity.defn +async def deposit(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-deposit" + try: + # Uncomment this working line: + confirmation = await asyncio.to_thread( + self.bank.deposit, data.target_account, data.amount, reference_id + ) + + # Comment out this failing line: + # confirmation = await asyncio.to_thread( + # self.bank.deposit_that_fails, + # data.target_account, + # data.amount, + # reference_id, + # ) + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Deposit failed") + raise ``` -Transfer complete (transaction IDs: W3436600150, D9270097234) + + + + + +Go back to `activity.go` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activity.go** +```go +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Depositing $%d into account %s.\n\n", + data.Amount, + data.TargetAccount, + ) + + referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + + // Comment out this failing line: + // confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + // Uncomment this working line: + confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + + return confirmation, err +} ``` -Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. + - + - +Go back to `AccountActivityImpl.java` and **change `activityShouldSucceed` back to `true`**: -This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. +**AccountActivityImpl.java** +```java +public String deposit(PaymentDetails details) { + // Change this back to true to fix the bug: + boolean activityShouldSucceed = true; + + // ... rest of your method +} +``` -To test this out and see how Temporal responds, you'll simulate a bug in the `DepositAsync()` Activity method. + -Let your Workflow continue to run but don't start the Worker yet. + -Open the `Activities.cs` file and switch out the comments on the return statements so that the `DepositAsync()` method throws an exception: +Go back to `activities.ts` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activities.ts** +```typescript +export async function deposit(details: PaymentDetails): Promise { + // Uncomment this working line: + return await bank.deposit(details.targetAccount, details.amount, details.referenceId); + + // Comment out this failing line: + // return await bank.depositThatFails(details.targetAccount, details.amount, details.referenceId); +} +``` + + + + + +Go back to `Activities.cs` and **reverse the comments** - comment out the failing line and uncomment the working block: **MoneyTransferWorker/Activities.cs** ```csharp @@ -606,10 +717,10 @@ public static async Task DepositAsync(PaymentDetails details) var bankService = new BankingService("bank2.example.com"); Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); - // Uncomment below and comment out the try-catch block below to simulate unknown failure - return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); + // Comment out this failing line: + // return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); - /* + // Uncomment this working block: try { return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); @@ -618,83 +729,148 @@ public static async Task DepositAsync(PaymentDetails details) { throw new ApplicationFailureException("Deposit failed", ex); } - */ } ``` -Save your changes and switch to the terminal that was running your Worker. + -Start the Worker again: -```bash -dotnet run --project MoneyTransferWorker + + +Go back to `activities.rb` and **comment out the failing line**: + +**activities.rb** +```ruby +def deposit(details) + # Comment out this problematic line: + # result = 100 / 0 # This will cause a divide-by-zero error + + # Your existing deposit logic here... +end ``` -Note, that you must restart the Worker every time there's a change in code. You will see the Worker complete the `WithdrawAsync()` Activity method, but it errors when it attempts the `DepositAsync()` Activity method. + + + +Save your changes. You've now restored the working implementation. The key insight here is that you can deploy fixes to Activities while Workflows are still executing - Temporal will pick up your changes on the next retry attempt. -The important thing to note here is that the Worker keeps retrying the `DepositAsync()` method: +### Step 5: Restart Worker +To apply your fix, you need to restart the Worker process so it picks up the code changes. Since the Workflow execution state is stored in Temporal's servers (not in your Worker process), restarting the Worker won't affect the running Workflow. + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +python run_worker.py ``` -Running worker... -Withdrawing $400 from account 85-150. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction in Terminal 3: + +``` +Transfer complete. +Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} +Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} ``` -The Workflow keeps retrying using the RetryPolicy specified when the Workflow first executes the Activity. + -Your Workflow is running, but only the `WithdrawAsync()` Activity method has succeeded. In any other application, you would likely have to abandon the entire process and perform a rollback. + + +```bash +# Stop the current Worker +Ctrl+C -With Temporal, you can debug and resolve the issue while the Workflow is running. +# Start it again with the fix +go run worker/main.go +``` + +**On the next retry attempt,** your fixed `Deposit()` Activity will succeed, and you'll see the completed transaction in your starter terminal: + +``` +Transfer complete (transaction IDs: W1779185060, D1779185060) +``` -Pretend that you found a fix for the issue. Switch the comments back to the return statements of the `DepositAsync()` method in the `Activities.cs` file and save your changes. + -How can you possibly update a Workflow that's already halfway complete? You restart the Worker. + -To restart the Worker, go to the terminal where the Worker is running and cancel the Worker with `Ctrl+C`, then restart the Worker by running: ```bash -dotnet run --project MoneyTransferWorker +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null +mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn ``` -The Worker starts again. On the next scheduled attempt, the Worker picks up right where the Workflow was failing and successfully executes the newly compiled `DepositAsync()` Activity method. +**On the next retry attempt,** your fixed `deposit()` Activity will succeed: -Switch back to the terminal where your `Program.cs` file in MoneyTransferClient folder is running, and you'll see it complete: ``` -Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) +Depositing $32 into account 872878204. +[ReferenceId: d3d9bcf0-a897-4326] +[d3d9bcf0-a897-4326] Transaction succeeded. ``` -Visit the Web UI again, and you'll see the Workflow has completed. You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! + - + - +```bash +# Stop the current Worker +Ctrl+C -The Withdraw Activity contains a bug, although it hasn't been a problem yet because that statement is currently commented out. You will now uncomment it to expose the bug. You'll see that this causes the Activity to fail, but you'll also see that it retries automatically. More importantly, you'll observe that after you fix the bug, the Workflow Execution that was failing will complete successfully. +# Start it again with the fix +npm run worker +``` -Try it out by following these steps: +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction in your client terminal: -1. **Whenever you modify the code, you must restart the Worker** for the changes to take effect. Press `Ctrl+C` in the terminal where your Worker is running to stop the Worker so that you can introduce the bug in the next step. +``` +Transfer complete (transaction IDs: W3436600150, D9270097234) +``` -2. **Edit the `activities.rb` file**, uncomment the line in the Withdraw Activity that causes a divide-by-zero error, and save the change + -3. **Start the Worker again** by running `bundle exec ruby worker.rb` + -4. **Start a new Workflow Execution** by running `bundle exec ruby starter.rb` +```bash +# Stop the current Worker +Ctrl+C -5. **Go to the main page** in the Temporal Web UI and click the Workflow Execution you just started to view its detail page. You should see that the Withdraw Activity is failing. Click the Pending Activities tab to see the cause, and then click the History tab to return to the previous view. +# Start it again with the fix +dotnet run --project MoneyTransferWorker +``` -6. **Edit the `activities.rb` file**, comment out the line with the divide-by-zero error, and then save the change. +**On the next retry attempt,** your fixed `DepositAsync()` Activity will succeed, and you'll see the completed transaction in your client terminal: -7. **Press `Ctrl+C`** in the terminal where your Worker is running and then run `bundle exec ruby worker.rb` to start it again. The Worker will now use the code that contains the change you made in the previous step. +``` +Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) +``` -8. **You should see that the Workflow Execution completes successfully.** This will be visible both in the Web UI and in the terminal where you started the Workflow Execution. + - + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +bundle exec ruby worker.rb +``` + +**On the next retry attempt,** your fixed `deposit` Activity will succeed, and you'll see the Workflow complete successfully. + + -**Mission Accomplished!** You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction! +Check the Web UI - your Workflow shows as completed. You've just demonstrated Temporal's key differentiator: the ability to fix production bugs in running applications without losing transaction state or progress. This is possible because Temporal stores execution state separately from your application code. + +**Mission Accomplished.** You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. :::tip **Try This Challenge** **Real-World Scenario**: Try this advanced experiment: @@ -728,7 +904,7 @@ You fixed a bug in running code without losing any state. Most systems require y
Automatic Retry Management -Temporal handled retries intelligently based on your policy, without cluttering your business logic with error-handling code. +Temporal handled retries according to your configured policy, without cluttering your business logic with error-handling code.
From a93783e17b42a2b7afe825b368d5edbd0e9a6baf Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 19 Nov 2025 16:58:43 -0600 Subject: [PATCH 21/40] react component for terminal animation --- .../failure-simulation.mdx | 176 ++++++++++++------ .../AnimatedTerminal/AnimatedTerminal.js | 123 ++++++++++++ .../AnimatedTerminal.module.css | 112 +++++++++++ src/components/AnimatedTerminal/index.js | 1 + .../WorkflowDiagram/WorkflowDiagram.js | 4 +- src/components/elements/SetupSteps.js | 54 +++--- 6 files changed, 388 insertions(+), 82 deletions(-) create mode 100644 src/components/AnimatedTerminal/AnimatedTerminal.js create mode 100644 src/components/AnimatedTerminal/AnimatedTerminal.module.css create mode 100644 src/components/AnimatedTerminal/index.js diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 2d1ead39c1..4422df0c46 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -37,27 +37,38 @@ export const TUTORIAL_LANGUAGE_ORDER = [ ]; import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import { CodeComparison } from "@site/src/components/CodeComparison"; +import { AnimatedTerminal } from "@site/src/components/AnimatedTerminal"; + +export const getTodayDate = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; +}; + +export const getTodayDateISO = () => { + return new Date().toISOString(); +}; # Part 2: Simulate Failures +In this part, you'll simulate failures to see how Temporal handles them. +This demonstrates why Temporal is particularly useful for building reliable systems. -In this part, you'll simulate failures to see how Temporal handles them. -This demonstrates why Temporal is particularly useful for building reliable systems. The key concept here is **durable execution**: your workflow's progress is saved after every step. When failures and crashes happen (network issues, bugs in your code, server restarts), Temporal resumes your workflow exactly where it stopped. No lost work, no restarting from the beginning. -**What you'll accomplish**: -- Crash a server mid-transaction and see zero data loss -- Inject bugs into code and fix them live +You'll crash a server mid-transaction and see zero data loss, then inject bugs into code and fix them live while your application continues running. -**Difficulty**: Intermediate - -Ready to break some stuff? Let's go. ## Experiment 1 of 2: Crash Recovery Test @@ -188,7 +199,7 @@ Now in Terminal 3, start the Workflow. Check the Web UI - you'll see your Worker The Crash Test

Go back to Terminal 2 and kill the Worker with Ctrl+C

-}> +} style={{background: 'transparent'}}> ### Step 3: Simulate the Crash @@ -252,6 +263,7 @@ Restart your Worker in Terminal 2. Watch Terminal 3 - you'll see the Workflow fi **Mission Accomplished!** You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. :::tip **Try This Challenge** + Try killing the Worker at different points during execution. Start the Workflow, kill the Worker during the withdrawal, then restart it. Kill it during the deposit. Each time, notice how Temporal maintains perfect state consistency. Check the Web UI while the Worker is down - you'll see the Workflow is still "Running" even though no code is executing. @@ -279,7 +291,7 @@ Check the Web UI while the Worker is down - you'll see the Workflow is still "Ru - Code editor open with `activities.py` + Code editor open with the Activities file @@ -485,13 +497,21 @@ python run_worker.py - The `withdraw()` Activity completes successfully - The `deposit()` Activity fails and retries automatically -``` -2024/02/12 10:59:09 Withdrawing $250 from account 85-150. -2024/02/12 10:59:09 Depositing $250 into account 43-812. -2024/02/12 10:59:09 ERROR Activity error. This deposit has failed. -2024/02/12 10:59:10 Depositing $250 into account 43-812. -2024/02/12 10:59:10 ERROR Activity error. This deposit has failed. -``` +
@@ -505,14 +525,22 @@ go run worker/main.go - The `Withdraw()` Activity completes successfully - The `Deposit()` Activity fails and retries automatically -``` -2022/11/14 10:59:09 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE -2022/11/14 10:59:09 Withdrawing $250 from account 85-150. -2022/11/14 10:59:09 Depositing $250 into account 43-812. -2022/11/14 10:59:09 ERROR Activity error. This deposit has failed. -2022/11/14 10:59:10 Depositing $250 into account 43-812. -2022/11/14 10:59:10 ERROR Activity error. This deposit has failed. -``` + @@ -529,14 +557,22 @@ mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -D - The `withdraw()` Activity completes successfully - The `deposit()` Activity fails and retries automatically -``` -Withdrawing $32 from account 612849675. -[ReferenceId: d3d9bcf0-a897-4326] -Deposit failed -Deposit failed -Deposit failed -Deposit failed -``` + @@ -550,18 +586,26 @@ npm run worker - The `withdraw()` Activity completes successfully - The `deposit()` Activity fails and retries automatically -``` -2023-10-11T19:03:25.778Z [INFO] Worker state changed { state: 'RUNNING' } -Withdrawing $400 from account 85-150. -Depositing $400 into account 43-812. -2023-10-11T19:03:29.445Z [WARN] Activity failed { - attempt: 1, - activityType: 'deposit', - taskQueue: 'money-transfer', - error: Error: This deposit has failed -} -Depositing $400 into account 43-812. -``` + @@ -575,14 +619,22 @@ dotnet run --project MoneyTransferWorker - The `WithdrawAsync()` Activity completes successfully - The `DepositAsync()` Activity fails and retries automatically -``` -Running worker... -Withdrawing $400 from account 85-150. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. -Depositing $400 into account 43-812. -``` + @@ -602,6 +654,24 @@ bundle exec ruby starter.rb - The `withdraw` Activity completes successfully - The `deposit` Activity fails and retries automatically + + Check the Web UI - click on your Workflow to see the failure details and retry attempts. diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.js b/src/components/AnimatedTerminal/AnimatedTerminal.js new file mode 100644 index 0000000000..c69ad7b30d --- /dev/null +++ b/src/components/AnimatedTerminal/AnimatedTerminal.js @@ -0,0 +1,123 @@ +import React, { useState, useEffect, useRef } from 'react'; +import styles from './AnimatedTerminal.module.css'; + +export const AnimatedTerminal = ({ + lines = [], + delay = 1000, + typingSpeed = 50, + prompt = "$", + autoStart = true, + startOnVisible = false, + loop = false, + restartDelay = 2000 +}) => { + const [displayedLines, setDisplayedLines] = useState([]); + const [currentLineIndex, setCurrentLineIndex] = useState(0); + const [isTyping, setIsTyping] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + const [shouldStart, setShouldStart] = useState(!startOnVisible); + const terminalRef = useRef(null); + + // Intersection Observer for scroll-triggered start + useEffect(() => { + if (!startOnVisible || shouldStart) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setShouldStart(true); + } + }); + }, + { + threshold: 0.3, // Trigger when 30% of the component is visible + rootMargin: '0px 0px -100px 0px' // Start slightly before it's fully visible + } + ); + + if (terminalRef.current) { + observer.observe(terminalRef.current); + } + + return () => { + observer.disconnect(); + }; + }, [startOnVisible, shouldStart]); + + useEffect(() => { + if (!shouldStart || !autoStart || lines.length === 0) return; + + const showNextLine = () => { + if (currentLineIndex < lines.length) { + setIsTyping(true); + + // Simulate typing effect for current line + const currentLine = lines[currentLineIndex]; + let charIndex = 0; + const typingInterval = setInterval(() => { + if (charIndex <= currentLine.length) { + setDisplayedLines(prev => { + const newLines = [...prev]; + newLines[currentLineIndex] = currentLine.slice(0, charIndex); + return newLines; + }); + charIndex++; + } else { + clearInterval(typingInterval); + setIsTyping(false); + setCurrentLineIndex(prev => prev + 1); + } + }, typingSpeed); + + return () => clearInterval(typingInterval); + } else if (loop && currentLineIndex === lines.length) { + // Animation complete, restart if looping + setIsRestarting(true); + const restartTimer = setTimeout(() => { + setDisplayedLines([]); + setCurrentLineIndex(0); + setIsRestarting(false); + }, restartDelay); + + return () => clearTimeout(restartTimer); + } + }; + + const timer = setTimeout(() => { + showNextLine(); + }, currentLineIndex === 0 ? 0 : delay); + + return () => clearTimeout(timer); + }, [currentLineIndex, lines, delay, typingSpeed, autoStart, loop, restartDelay, shouldStart]); + + return ( +
+
+
+ + + +
+
Terminal
+
+
+ {displayedLines.map((line, index) => ( +
+ {prompt} + {line} + {index === currentLineIndex - 1 && isTyping && ( + | + )} +
+ ))} + {(currentLineIndex < lines.length && !isTyping && !isRestarting) && ( +
+ {prompt} + | +
+ )} +
+
+ ); +}; diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.module.css b/src/components/AnimatedTerminal/AnimatedTerminal.module.css new file mode 100644 index 0000000000..80683a5d63 --- /dev/null +++ b/src/components/AnimatedTerminal/AnimatedTerminal.module.css @@ -0,0 +1,112 @@ +.terminal { + background: #1e1e1e; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin: 1.5rem 0; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Liberation Mono', monospace; + font-size: 14px; + overflow: hidden; +} + +.terminalHeader { + background: #323232; + padding: 8px 16px; + display: flex; + align-items: center; + border-bottom: 1px solid #444; +} + +.terminalButtons { + display: flex; + gap: 8px; + margin-right: 16px; +} + +.terminalButtons span { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.close { + background: #ff5f57; +} + +.minimize { + background: #ffbd2e; +} + +.maximize { + background: #28ca42; +} + +.terminalTitle { + color: #ccc; + font-size: 12px; + font-weight: 500; +} + +.terminalBody { + padding: 16px; + min-height: 120px; + background: #1e1e1e; + color: #f8f8f2; +} + +.terminalLine { + display: flex; + align-items: center; + margin: 4px 0; + line-height: 1.4; +} + +.prompt { + color: #50fa7b; + margin-right: 8px; + user-select: none; +} + +.command { + color: #f8f8f2; + white-space: pre-wrap; +} + +.cursor { + color: #f8f8f2; + animation: blink 1s infinite; + margin-left: 2px; +} + +@keyframes blink { + + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0; + } +} + +/* Different colors for different types of output */ +.terminalLine:has(.command:contains("ERROR")) .command, +.terminalLine .command:contains("ERROR") { + color: #ff5555; +} + +.terminalLine:has(.command:contains("Withdrawing")) .command, +.terminalLine .command:contains("Withdrawing") { + color: #8be9fd; +} + +.terminalLine:has(.command:contains("Depositing")) .command, +.terminalLine .command:contains("Depositing") { + color: #ffb86c; +} + +.terminalLine:has(.command:contains("Running")) .command, +.terminalLine .command:contains("Running") { + color: #50fa7b; +} \ No newline at end of file diff --git a/src/components/AnimatedTerminal/index.js b/src/components/AnimatedTerminal/index.js new file mode 100644 index 0000000000..6e8daa97de --- /dev/null +++ b/src/components/AnimatedTerminal/index.js @@ -0,0 +1 @@ +export { AnimatedTerminal } from './AnimatedTerminal'; diff --git a/src/components/WorkflowDiagram/WorkflowDiagram.js b/src/components/WorkflowDiagram/WorkflowDiagram.js index babd0a868c..ffe6871720 100644 --- a/src/components/WorkflowDiagram/WorkflowDiagram.js +++ b/src/components/WorkflowDiagram/WorkflowDiagram.js @@ -1,9 +1,9 @@ import React from 'react'; import styles from './WorkflowDiagram.module.css'; -export const WorkflowDiagram = ({ title, nodes }) => { +export const WorkflowDiagram = ({ title, nodes, style }) => { return ( -
+

{title}

{nodes.map((node, index) => ( diff --git a/src/components/elements/SetupSteps.js b/src/components/elements/SetupSteps.js index cfdf16d55d..7745afb4c6 100644 --- a/src/components/elements/SetupSteps.js +++ b/src/components/elements/SetupSteps.js @@ -1,32 +1,32 @@ import React from 'react'; - import styles from './setup-steps.module.css'; - import CodeBlock from '@theme/CodeBlock'; +import styles from './setup-steps.module.css'; +import CodeBlock from '@theme/CodeBlock'; - export const SetupStep = ({ children, code }) => { - return ( -
-
- {children} -
-
- {code} -
+export const SetupStep = ({ children, code, style }) => { + return ( +
+
+ {children} +
+
+ {code}
- ); - }; +
+ ); +}; - export const CodeSnippet = ({ language, children }) => { - return ( - - {children} - - ); - }; +export const CodeSnippet = ({ language, children }) => { + return ( + + {children} + + ); +}; - export const SetupSteps = ({ children }) => { - return ( -
- {children} -
- ); - }; \ No newline at end of file +export const SetupSteps = ({ children }) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file From 50b9decf009811da669e2387f0bf9ed3ec537549 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 19 Nov 2025 17:15:16 -0600 Subject: [PATCH 22/40] updating intro draft for failure simulation --- .../build-your-first-workflow.mdx | 2 +- .../failure-simulation.mdx | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index d2853beebf..a3c573a0e8 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -56,7 +56,7 @@ You'll learn how to construct Workflows and Activities, understand the core buil Before starting this tutorial: -- **Set up a local development environment** for developing Temporal Applications using the Python programming language +- **Set up a local development environment** for developing Temporal Applications - **Ensure you have Git installed** to clone the project { In this part, you'll simulate failures to see how Temporal handles them. This demonstrates why Temporal is particularly useful for building reliable systems. - -The key concept here is **durable execution**: your workflow's progress is saved after every step. -When failures and crashes happen (network issues, bugs in your code, server restarts), Temporal resumes your workflow exactly where it stopped. No lost work, no restarting from the beginning. +We've all been there: a seemingly harmless deployment brings down production, a database connection times out during peak traffic, or a third-party service decides to have an outage right when you need it most. +Despite our best efforts with comprehensive testing and monitoring, systems are inherently unpredictable and complex. +Networks fail, servers restart unexpectedly, and dependencies we trust can become unavailable without warning. + +Traditional systems aren't equipped to handle these realities. +When something fails halfway through a multi-step process, you're left with partial state, inconsistent data, and the complex task of figuring out where things went wrong and how to recover. +Most applications either lose progress entirely or require you to build extensive checkpointing and recovery logic. + +In this tutorial, you'll see Temporal's durable execution in action by running two tests: crashing a server while it's working and fixing code problems on the fly without stopping your application. -You'll crash a server mid-transaction and see zero data loss, then inject bugs into code and fix them live while your application continues running. ## Experiment 1 of 2: Crash Recovery Test From ecc3a22353f365cc9f1e98a60dc88cc6230de847 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 20 Nov 2025 08:44:52 -0600 Subject: [PATCH 23/40] update index with cards --- .../build-your-first-basic-workflow/index.mdx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/build-your-first-basic-workflow/index.mdx b/docs/build-your-first-basic-workflow/index.mdx index 2fc4b2cab3..669db50ea4 100644 --- a/docs/build-your-first-basic-workflow/index.mdx +++ b/docs/build-your-first-basic-workflow/index.mdx @@ -15,9 +15,23 @@ tags: - Tutorial --- - # Build your First Basic Workflow -import { LanguageSelector } from "@site/src/components/LanguageSelector"; +Build a money transfer app that survives crashes and handles failures gracefully. + +import QuickstartCards from "@site/src/components/QuickstartCards"; - + From 8f42e89f22f92aea89afecb21d992a65419b11de Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Sun, 23 Nov 2025 19:08:17 -0600 Subject: [PATCH 24/40] remove redundant components, edit intro --- .../build-your-first-workflow.mdx | 43 ++++-- .../LanguageSelector/LanguageSelector.js | 76 ----------- .../LanguageSelector.module.css | 126 ------------------ src/components/LanguageSelector/index.js | 1 - 4 files changed, 32 insertions(+), 214 deletions(-) delete mode 100644 src/components/LanguageSelector/LanguageSelector.js delete mode 100644 src/components/LanguageSelector/LanguageSelector.module.css delete mode 100644 src/components/LanguageSelector/index.js diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index a3c573a0e8..a96473a8f0 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -35,7 +35,9 @@ export const TUTORIAL_LANGUAGE_ORDER = [ { key: 'rb', label: 'Ruby', icon: SiRuby }, ]; -You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process. +In this tutorial, you'll build and run your first Temporal application. +You'll understand the core building blocks of Temporal and learn how Temporal helps you build crash proof applications through durable execution. +
Temporal beginner @@ -49,14 +51,12 @@ You can think of Temporal as a sort of "cure-all" for the pains you experience a ## Introduction -In this tutorial, you'll build and run your first Temporal Application. -You'll learn how to construct Workflows and Activities, understand the core building blocks, and see you can get full visibility into its execution. ### Prerequisites Before starting this tutorial: -- **Set up a local development environment** for developing Temporal Applications +- **Set up a local development environment** for developing Temporal applications - **Ensure you have Git installed** to clone the project Money Transfer Application Flow
-In this sample application, money comes out of one account and goes into another. However, there are a few things that can go wrong with this process. If the withdrawal fails, then there is no need to try to make a deposit. But if the withdrawal succeeds, but the deposit fails, then the money needs to go back to the original account. +In this sample application, money comes out of one account and goes into another. +However, there are a few things that can go wrong with this process. +If the withdrawal fails, then there is no need to try to make a deposit. +But if the withdrawal succeeds, but the deposit fails, then the money needs to go back to the original account. -One of Temporal's most important features is its ability to **maintain the application state when something fails**. When failures happen, Temporal recovers processes where they left off or rolls them back correctly. This allows you to focus on business logic, instead of writing application code to recover from failure. +One of Temporal's most important features is its ability to **maintain the application state when something fails**. +When failures happen, Temporal recovers processes where they left off or rolls them back correctly. +This allows you to focus on business logic, instead of writing application code to recover from failure. @@ -123,14 +129,29 @@ The repository for this tutorial is a GitHub Template repository, which means yo The Temporal Application will consist of the following pieces: -1. **A Workflow** written in Python using the Python SDK. A Workflow defines the overall flow of the application. -2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. They handle individual tasks like withdraw(), deposit(), and refund(). +1. **A Workflow** written in your programming language of choice and your installed Temporal SDK in that language. A Workflow defines the overall flow of the application. +2. **An Activity** is a function or method that does specific operation - like withdrawing money, sending an email, or calling an API. Since these operations often depend on external services that can be unreliable, Temporal automatically retries Activities when they fail. +In this application, you'll write Activities for withdraw, deposit, and refund operations. 3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. +
+What You'll Build and Run + +The project in this tutorial mimics a "money transfer" application. +It is implemented with a single Workflow, which orchestrates the execution of three Activities (Withdraw, Deposit, and Refund) that move money between the accounts. + +To perform a money transfer, you will do the following: + +1. **Launch a Worker**: Since a Worker is responsible for executing the Workflow and Activity code, at least one Worker must be running for the money transfer to make progress. + +2. **Submit a Workflow Execution request** to the Temporal Service: After the Worker communicates with the Temporal Service, the Worker will begin executing the Workflow and Activity code. It reports the results to the Temporal Service, which tracks the progress of the Workflow Execution. + +
+ :::important None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. ::: diff --git a/src/components/LanguageSelector/LanguageSelector.js b/src/components/LanguageSelector/LanguageSelector.js deleted file mode 100644 index 092d802d57..0000000000 --- a/src/components/LanguageSelector/LanguageSelector.js +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState } from 'react'; -import styles from './LanguageSelector.module.css'; - -export const LanguageSelector = () => { - const [hoveredLanguage, setHoveredLanguage] = useState(null); - - const languages = [ - { - name: 'Python', - icon: '/img/sdks/sdk-box-logos/python.svg', - href: '/build-your-first-basic-workflow/python' - }, - { - name: 'Go', - icon: '/img/sdks/sdk-box-logos/go.svg', - href: 'https://learn.temporal.io/getting_started/go/first_program_in_go/' - }, - { - name: 'Java', - icon: '/img/sdks/sdk-box-logos/java.svg', - href: 'https://learn.temporal.io/getting_started/java/first_program_in_java/' - }, - { - name: 'TypeScript', - icon: '/img/sdks/sdk-box-logos/typescript.svg', - href: 'https://learn.temporal.io/getting_started/typescript/first_program_in_typescript/' - }, - { - name: '.NET', - icon: '/img/sdks/sdk-box-logos/dotnet.svg', - href: 'https://learn.temporal.io/getting_started/dotnet/first_program_in_dotnet/' - }, - { - name: 'PHP', - icon: '/img/sdks/sdk-box-logos/php.svg', - href: 'https://learn.temporal.io/getting_started/php/first_program_in_php/' - }, - { - name: 'Ruby', - icon: '/img/sdks/sdk-box-logos/ruby.svg', - href: 'https://learn.temporal.io/getting_started/ruby/first_program_in_ruby/' - } - ]; - - return ( -
-
-

- Develop in -

-

- {hoveredLanguage || 'your language'} -

-
- - -
- ); -}; diff --git a/src/components/LanguageSelector/LanguageSelector.module.css b/src/components/LanguageSelector/LanguageSelector.module.css deleted file mode 100644 index 24a6fc8dc5..0000000000 --- a/src/components/LanguageSelector/LanguageSelector.module.css +++ /dev/null @@ -1,126 +0,0 @@ -.languageSelector { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2rem; - padding: 2rem 0; - background: transparent; - margin: 2rem 0; -} - -.textContainer { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.title { - font-size: 1.8rem; - font-weight: 400; - color: var(--ifm-color-content); - margin: 0; - line-height: 1.2; -} - -.language { - font-size: 2.2rem; - font-weight: 600; - background: linear-gradient(135deg, #444CE7, #7C3AED); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - margin: 0; - line-height: 1.2; - min-height: 2.6rem; - /* Prevent layout shift */ - transition: all 0.3s ease; -} - -.iconsContainer { - display: flex; - gap: 1.5rem; - align-items: center; - flex-wrap: wrap; -} - -.languageIcon { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 80px; - padding: 1rem; - border-radius: 12px; - background: rgba(255, 255, 255, 0.05); - border: 2px solid rgba(255, 255, 255, 0.1); - transition: all 0.3s ease; - text-decoration: none; - cursor: pointer; -} - -.languageIcon:hover, -.languageIcon.active { - background: rgba(255, 255, 255, 0.1); - border-color: #444CE7; - transform: translateY(-4px); - box-shadow: - 0 8px 32px rgba(68, 76, 231, 0.3), - 0 4px 16px rgba(0, 0, 0, 0.2); -} - -.icon { - width: 48px; - height: 48px; - object-fit: contain; - filter: grayscale(0.3) opacity(0.8); - transition: filter 0.3s ease; -} - -.languageIcon:hover .icon, -.languageIcon.active .icon { - filter: grayscale(0) opacity(1); -} - -/* Theme adjustments handled by CSS custom property var(--ifm-color-content) */ - -:global([data-theme='light']) .languageIcon { - background: rgba(255, 255, 255, 0.8); - border-color: rgba(0, 0, 0, 0.1); -} - -:global([data-theme='light']) .languageIcon:hover, -:global([data-theme='light']) .languageIcon.active { - background: rgba(255, 255, 255, 1); - border-color: #444CE7; -} - -/* Responsive design */ -@media (max-width: 768px) { - .languageSelector { - gap: 1.5rem; - } - - .title { - font-size: 1.4rem; - } - - .language { - font-size: 1.8rem; - } - - .iconsContainer { - gap: 1rem; - justify-content: center; - } - - .languageIcon { - width: 64px; - height: 64px; - padding: 0.75rem; - } - - .icon { - width: 32px; - height: 32px; - } -} \ No newline at end of file diff --git a/src/components/LanguageSelector/index.js b/src/components/LanguageSelector/index.js deleted file mode 100644 index 493cac82a0..0000000000 --- a/src/components/LanguageSelector/index.js +++ /dev/null @@ -1 +0,0 @@ -export { LanguageSelector } from './LanguageSelector'; From 5ed31fa58ab9696f52030d1a06b6e3e1992a41fe Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 24 Nov 2025 08:24:07 -0600 Subject: [PATCH 25/40] add set up steps for project install in each language --- .../build-your-first-workflow.mdx | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index a96473a8f0..ea4e481c99 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -89,10 +89,67 @@ This allows you to focus on business logic, instead of writing application code -git clone https://github.com/temporalio/money-transfer-project-template-python -cd money-transfer-project-template-python - + + + + git clone https://github.com/temporalio/money-transfer-project-template-python + + + cd money-transfer-project-template-python + + + + + git clone https://github.com/temporalio/money-transfer-project-template-go + + + cd money-transfer-project-template-go + + + + + git clone https://github.com/temporalio/money-transfer-project-java + + + cd money-transfer-project-java + + + + + git clone https://github.com/temporalio/money-transfer-project-template-ts + + + cd money-transfer-project-template-ts + + + + + git clone https://github.com/temporalio/money-transfer-project-template-dotnet + + + cd money-transfer-temporal-template-dotnet + + + + + git clone https://github.com/temporalio/money-transfer-project-template-ruby + + + cd money-transfer-project-template-ruby + + + + + git clone https://github.com/temporalio/money-transfer-project-template-php + + + cd money-transfer-project-template-php + + + composer install + + + }> ### Download the example application From 5360c553914bc4b5fc55e3814fc8599ee3e3fcd6 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Sun, 30 Nov 2025 17:56:46 -0600 Subject: [PATCH 26/40] failure simulation content editS --- .../failure-simulation.mdx | 138 ++++-------------- 1 file changed, 32 insertions(+), 106 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 45ea007502..5296080de7 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -63,7 +63,7 @@ This demonstrates why Temporal is particularly useful for building reliable syst { id: 'part2', label: 'Part 2: Failure Simulation', status: 'active' } ]} /> -We've all been there: a seemingly harmless deployment brings down production, a database connection times out during peak traffic, or a third-party service decides to have an outage right when you need it most. +Systems fail in unpredictable ways. A seemingly harmless deployment can bring down production, a database connection can time out during peak traffic, or a third-party service can decide to have an outage. Despite our best efforts with comprehensive testing and monitoring, systems are inherently unpredictable and complex. Networks fail, servers restart unexpectedly, and dependencies we trust can become unavailable without warning. @@ -73,16 +73,13 @@ Most applications either lose progress entirely or require you to build extensiv In this tutorial, you'll see Temporal's durable execution in action by running two tests: crashing a server while it's working and fixing code problems on the fly without stopping your application. - - -## Experiment 1 of 2: Crash Recovery Test +## Recover from a server crash Unlike other solutions, Temporal is designed with failure in mind. -You're about to simulate a server crash mid-transaction and watch Temporal handle it flawlessly. - -**The Challenge**: Kill your Worker process while money is being transferred. In traditional systems, this would corrupt the transaction or lose data entirely. - +In this part of the tutorial, you'll simulate a server crash mid-transaction and watch Temporal helps you recover from it. +**Here's the challenge:** Kill your Worker process while money is being transferred. +In traditional systems, this would corrupt the transaction or lose data entirely. What's happening behind the scenes? -The Temporal Server acts like a persistent state machine for your Workflow. When you kill the Worker, you're only killing the process that executes the code - but the Workflow state lives safely in Temporal's durable storage. When a new Worker starts, it picks up exactly where the previous one left off. +Unlike many modern applications that require complex leader election processes and external databases to handle failure, Temporal automatically preserves the state of your Workflow even if the server is down. +You can test this by stopping the Temporal Service while a Workflow Execution is in progress. -This is fundamentally different from traditional applications where process crashes mean lost work. +No data is lost once the Temporal Service went offline. +When it comes back online, the work picked up where it left off before the outage. +Keep in mind that this example uses a single instance of the service running on a single machine. +In a production deployment, the Temporal Service can be deployed as a cluster, spread across several machines for higher availability and increased throughput. @@ -264,20 +265,20 @@ Restart your Worker in Terminal 2. Watch Terminal 3 - you'll see the Workflow fi - -**Mission Accomplished!** You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. - :::tip **Try This Challenge** Try killing the Worker at different points during execution. Start the Workflow, kill the Worker during the withdrawal, then restart it. Kill it during the deposit. Each time, notice how Temporal maintains perfect state consistency. -Check the Web UI while the Worker is down - you'll see the Workflow is still "Running" even though no code is executing. +Check the Web UI while the Worker is down and you'll see the Workflow is still "Running" even though no code is executing. ::: -## Experiment 2 of 2: Live Bug Fixing +## Recover from an unknown error +In this part of the tutorial, you will inject a bug into your production code, watch Temporal retry automatically, then fix the bug while the Workflow is still running. +This demo application makes a call to an external service in an Activity. +If that call fails due to a bug in your code, the Activity produces an error. -**The Challenge**: Inject a bug into your production code, watch Temporal retry automatically, then fix the bug while the Workflow is still running. +To test this out and see how Temporal responds, you'll simulate a bug in the Deposit Activity function or method. -
-What makes live debugging possible? - -Traditional applications lose all context when they crash or fail. Temporal maintains the complete execution history and state of your Workflow in durable storage. This means you can: - -1. **Fix bugs in running code** without losing progress -2. **Deploy new versions** while Workflows continue executing -3. **Retry failed operations** with updated logic -4. **Maintain perfect audit trails** of what happened and when - -This is like having version control for your running application state. - -
## Instructions @@ -947,112 +935,50 @@ Check the Web UI - your Workflow shows as completed. You've just demonstrated Te **Mission Accomplished.** You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. -:::tip **Try This Challenge** -**Real-World Scenario**: Try this advanced experiment: -1. **Change the retry policy** in `workflows.py` to only retry 1 time -2. **Introduce a bug** that triggers the refund logic -3. **Watch the Web UI** as Temporal automatically executes the compensating transaction - -**Question to consider**: How would you handle this scenario in a traditional microservices architecture? -::: - -## Summary: What You Accomplished - -**Congratulations!** You've experienced firsthand why Temporal is a game-changer for reliable applications. Here's what you demonstrated: - -### What You Learned - -
-Crash-Proof Execution - -You killed a Worker mid-transaction and watched Temporal recover seamlessly. Traditional applications would lose this work entirely, requiring complex checkpointing and recovery logic. - -
- -
-Live Production Debugging - -You fixed a bug in running code without losing any state. Most systems require you to restart everything, losing all progress and context. -
- -
-Automatic Retry Management - -Temporal handled retries according to your configured policy, without cluttering your business logic with error-handling code. - -
- -
-Complete Observability - -The Web UI gave you full visibility into every step, retry attempt, and state transition. No more debugging mysterious failures. - -
- -#### Summary - - - Successfully recovered from a Worker crash - - - - Fixed a bug in a running Workflow - - - Observed automatic retry behavior - - - Used the Web UI for debugging - - - - Experienced zero data loss through failures - +:::tip Advanced Challenges -## Advanced Challenges +Try this advanced scenario of compensating transactions. -Try these advanced scenarios: -:::tip **Mission: Compensating Transactions** 1. **Modify the retry policy** in `workflows.py` to only retry 1 time 2. **Force the deposit to fail permanently** 3. **Watch the automatic refund** execute -**Mission objective**: Prove that Temporal can handle complex business logic flows even when things go wrong. ::: -:::tip **Mission: Network Partition Simulation** -1. **Start a long-running Workflow** -2. **Disconnect your network** (or pause the Temporal Server container) -3. **Reconnect after 30 seconds** - -**Mission objective**: Demonstrate Temporal's resilience to network failures. -::: ## Knowledge Check Test your understanding of what you just experienced: +
+Q: What are four of Temporal's value propositions that you learned about in this tutorial? + +**Answer**: +1. Temporal automatically maintains the state of your Workflow, despite crashes or even outages of the Temporal Service itself. +2. Temporal's built-in support for retries and timeouts enables your code to overcome transient and intermittent failures. +3. Temporal provides full visibility in the state of the Workflow Execution and its Web UI offers a convenient way to see the details of both current and past executions. +4. Temporal makes it possible to fix a bug in a Workflow Execution that you've already started. After updating the code and restarting the Worker, the failing Activity is retried using the code containing the bug fix, completes successfully, and execution continues with what comes next. +
+
Q: Why do we use a shared constant for the Task Queue name? -**Answer**: Because the Task Queue name connects your Workflow starter to your Worker. If they don't match exactly, your Worker will never see the Workflow tasks, and execution will stall indefinitely. +**Answer**: Because the Task Queue name is specified in two different parts of the code (the first starts the Workflow and the second configures the Worker). If their values differ, the Worker and Temporal Service would not share the same Task Queue, and the Workflow Execution would not progress. -**Real-world impact**: This is like having the wrong radio frequency - your messages never get delivered.
-Q: What happens when you modify Activity code for a running Workflow? +Q: What do you have to do if you make changes to Activity code for a Workflow that is running? -**Answer**: You must restart the Worker to load the new code. The Workflow will continue from where it left off, but with your updated Activity logic. +**Answer**: Restart the Worker. -**Real-world impact**: This enables hot-fixes in production without losing transaction state.
From 8fcad023b896c4aaa2e91b01c8b495161cbed86f Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 1 Dec 2025 08:42:55 -0600 Subject: [PATCH 27/40] adding php snippets for money transfer --- .../build-your-first-workflow.mdx | 264 ++++++++++++++++++ .../failure-simulation.mdx | 114 ++++++++ 2 files changed, 378 insertions(+) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index ea4e481c99..ae19cb6210 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -789,6 +789,135 @@ end + + +

Workflow Definition

+ +In the Temporal PHP SDK, a Workflow Definition is a class marked with the `#[WorkflowInterface]` attribute. + +This is what the Workflow Definition looks like for the money transfer process: + +**MoneyTransfer.php** + +```php +bankingActivity = Workflow::newActivityStub( + Banking::class, + ActivityOptions::new() + ->withStartToCloseTimeout('5 seconds') + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(3) + ->withMaximumInterval('2 seconds') + ->withNonRetryableExceptions([InvalidAccount::class, InsufficientFunds::class]), + ), + ); + } + + #[WorkflowMethod('money_transfer')] + #[ReturnType(Type::TYPE_STRING)] + public function handle(PaymentDetails $paymentDetails): \Generator + { + # Withdraw money + $withdrawOutput = yield $this->bankingActivity->withdraw($paymentDetails); + + # Deposit money + try { + $depositOutput = yield $this->bankingActivity->deposit($paymentDetails); + return "Transfer complete (transaction IDs: {$withdrawOutput}, {$depositOutput})"; + } catch (\Throwable $depositError) { + # Handle deposit error + Workflow::getLogger()->error("Deposit failed: {$depositError->getMessage()}"); + + # Attempt to refund + try { + $refundOutput = yield $this->bankingActivity->refund($paymentDetails); + Workflow::getLogger()->info('Refund successful. Confirmation ID: ' . $refundOutput); + } catch (ActivityFailure $refundError) { + Workflow::getLogger()->error("Refund failed: {$refundError->getMessage()}"); + throw $refundError; + } + + # Re-raise deposit error if refund was successful + throw $depositError; + } + } +} +``` + +

Activity Definition

+ +Activities handle the business logic. Each Activity method calls an external banking service: + +**BankingActivity.php** + +```php +referenceId . "-withdrawal"; + try { + $confirmation = $this->bank->withdraw( + $data->sourceAccount, + $data->amount, + $referenceId, + ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Withdrawal failed", ['exception' => $e]); + throw $e; + } + } +} +``` + +
+

Workflow Definition

@@ -1048,6 +1177,30 @@ class MoneyTransferWorkflow < Temporalio::Workflow::Definition + + +**MoneyTransfer.php** + +```php +// ... +$this->bankingActivity = Workflow::newActivityStub( + Banking::class, + ActivityOptions::new() + ->withStartToCloseTimeout('5 seconds') + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(3) // Stop after 3 tries + ->withMaximumInterval('2 seconds') // Don't wait longer than 2s + ->withNonRetryableExceptions([ // Never retry these errors + InvalidAccount::class, + InsufficientFunds::class + ]), + ), +); +``` + + + ### What Makes Errors Non-Retryable? @@ -1298,6 +1451,47 @@ worker.run(shutdown_signals: ['SIGINT']) + + +**Worker.php** + +```php +newWorker('default', logger: $logger); + +# Register Workflows +$worker->registerWorkflowTypes(\App\Workflow\MoneyTransfer::class); + +# Register Activities +$worker->registerActivity(BankingActivity::class, static fn(): BankingActivity => new BankingActivity( + $logger, + new Service('bank-api.example.com'), +)); + +$factory->run(); +``` + + + ## Step 4: Define the Task Queue @@ -1393,6 +1587,23 @@ end + + +**shared.php** or in your Worker.php + +```php + + :::tip Why Use Constants? @@ -1657,6 +1868,59 @@ end + + +**transfer.php** + +```php +newWorkflowStub( + MoneyTransfer::class, + WorkflowOptions::new() + ->withWorkflowIdReusePolicy(IdReusePolicy::AllowDuplicate) + ->withWorkflowRunTimeout(20) + ->withWorkflowExecutionTimeout(30), +); + +try { + $result = $workflow->handle($paymentDetails); + echo "\e[32mResult: $result\e[0m\n"; +} catch (WorkflowFailedException $e) { + echo "\e[31mWorkflow failed: {$e->getMessage()}\e[0m\n"; +} catch (\Throwable $e) { + echo "\e[31mError: {$e->getMessage()}\e[0m\n"; +} +``` + + + This code uses a Temporal Client to connect to the Temporal Service, calling its workflow start method to request execution. This returns a handle, and calling result on that handle will block until execution is complete, at which point it provides the result. diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 5296080de7..bae7a02950 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -146,6 +146,9 @@ In a production deployment, the Temporal Service can be deployed as a cluster, s bundle exec ruby worker.rb + + php Worker.php +
}> @@ -185,6 +188,9 @@ First, stop any running Worker (`Ctrl+C`) and start a fresh one in Terminal 2. bundle exec ruby starter.rb + + php transfer.php +
}> @@ -473,6 +479,45 @@ end Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + +Find the `deposit()` method in `BankingActivity.php` and **uncomment the failing line** while **commenting out the working line**: + +**BankingActivity.php** + +```php +#[\Override] +public function deposit(PaymentDetails $data): string +{ + $referenceId = $data->referenceId . "-deposit"; + try { + // Comment out this working line: + // $confirmation = $this->bank->deposit( + // $data->targetAccount, + // $data->amount, + // $referenceId, + // ); + + // Uncomment this failing line: + $confirmation = $this->bank->depositThatFails( + $data->targetAccount, + $data->amount, + $referenceId, + ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Deposit failed", ['exception' => $e]); + throw $e; + } +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + ### Step 3: Start Worker & Observe Retry Behavior @@ -668,6 +713,20 @@ bundle exec ruby starter.rb Check the Web UI - click on your Workflow to see the failure details and retry attempts. + + + +```bash +php Worker.php +``` + +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically + +Check the Web UI - click on your Workflow to see the failure details and retry attempts. + + **Key observation:** Your Workflow isn't stuck or terminated. Temporal automatically retries the failed Activity according to your configured retry policy, while maintaining the overall Workflow state. The successful withdraw Activity doesn't get re-executed - only the failed deposit Activity is retried. @@ -812,6 +871,43 @@ end ``` + + + +Go back to `BankingActivity.php` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**BankingActivity.php** + +```php +#[\Override] +public function deposit(PaymentDetails $data): string +{ + $referenceId = $data->referenceId . "-deposit"; + try { + // Uncomment this working line: + $confirmation = $this->bank->deposit( + $data->targetAccount, + $data->amount, + $referenceId, + ); + + // Comment out this failing line: + // $confirmation = $this->bank->depositThatFails( + // $data->targetAccount, + // $data->amount, + // $referenceId, + // ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Deposit failed", ['exception' => $e]); + throw $e; + } +} +``` + + Save your changes. You've now restored the working implementation. The key insight here is that you can deploy fixes to Activities while Workflows are still executing - Temporal will pick up your changes on the next retry attempt. @@ -929,6 +1025,24 @@ bundle exec ruby worker.rb **On the next retry attempt,** your fixed `deposit` Activity will succeed, and you'll see the Workflow complete successfully. + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +php Worker.php +``` + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction: + +``` +Result: Transfer complete (transaction IDs: W12345, D12345) +``` + + Check the Web UI - your Workflow shows as completed. You've just demonstrated Temporal's key differentiator: the ability to fix production bugs in running applications without losing transaction state or progress. This is possible because Temporal stores execution state separately from your application code. From f0c55c4d79a8a59faf46e575a793ee8bc2572691 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 1 Dec 2025 08:47:33 -0600 Subject: [PATCH 28/40] adding more php --- .../failure-simulation.mdx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index bae7a02950..5d8791357f 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -147,7 +147,7 @@ In a production deployment, the Temporal Service can be deployed as a cluster, s bundle exec ruby worker.rb - php Worker.php + ./rr serve
@@ -189,7 +189,7 @@ First, stop any running Worker (`Ctrl+C`) and start a fresh one in Terminal 2. bundle exec ruby starter.rb - php transfer.php + php src/transfer.php
@@ -252,6 +252,9 @@ That's the magic! The Workflow keeps running because Temporal saved its state, e bundle exec ruby worker.rb + + ./rr serve +
}> @@ -717,7 +720,7 @@ Check the Web UI - click on your Workflow to see the failure details and retry a ```bash -php Worker.php +./rr serve ``` **Here's what you'll see:** @@ -1033,7 +1036,7 @@ bundle exec ruby worker.rb Ctrl+C # Start it again with the fix -php Worker.php +./rr serve ``` **On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction: @@ -1054,7 +1057,7 @@ Check the Web UI - your Workflow shows as completed. You've just demonstrated Te -:::tip Advanced Challenges +:::tip Advanced Challenge Try this advanced scenario of compensating transactions. From 99cffe2489b5ec443b00b795e407f80c7fcd6ea2 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 1 Dec 2025 09:23:05 -0600 Subject: [PATCH 29/40] add language tabs on commands --- .../build-your-first-workflow.mdx | 122 +++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index ae19cb6210..9f9eb27114 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -1931,30 +1931,118 @@ This code uses a Temporal Client to connect to the Temporal Service, calling its
Terminal 1 - Start the Temporal server: - -temporal server start-dev - + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + +
Terminal 2 - Start the Worker: - -python run_worker.py - + + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + + ./rr serve + +
Terminal 3 - Start the Workflow: - -python run_workflow.py - + + + python run_workflow.py + + + go run start/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" + + + npm run client + + + dotnet run --project MoneyTransferClient + + + bundle exec ruby starter.rb + + + php src/transfer.php + +
- Expected Success Output: - - - -Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345) - - + Expected Success Output: + + + Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345) + + + Starting transfer from account 85-150 to account 43-812 for 250 +2022/11/14 10:52:20 WorkflowID: pay-invoice-701 RunID: 3312715c-9fea-4dc3-8040-cf8f270eb53c +Transfer complete (transaction IDs: W1779185060, D1779185060) + + + Worker is running and actively polling the Task Queue. +To quit, use ^C to interrupt. + +Withdrawing $62 from account 249946050. +[ReferenceId: 1480a22d-d0fc-4361] +Depositing $62 into account 591856595. +[ReferenceId: 1480a22d-d0fc-4361] +[1480a22d-d0fc-4361] Transaction succeeded. + + + Transfer complete (transaction IDs: W3436600150, D9270097234) + + + Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) + + + Initiated transfer of $100 from A1001 to B2002 +Workflow ID: moneytransfer-2926a650-1aaf-49d9-bf87-0e3a09ef7b32 +Workflow result: Transfer complete (transaction IDs: OKW-100-A1001, OKD-100-B2002) + + + Result: Transfer complete (transaction IDs: W12345, D12345) + +
}> @@ -1963,7 +2051,7 @@ Now that your Worker is running and polling for tasks, you can start a Workflow **In Terminal 3, start the Workflow:** -The `run_workflow.py` script starts a Workflow Execution. Each time you run this file, the Temporal Server starts a new Workflow Execution. +The workflow starter script starts a Workflow Execution. Each time you run it, the Temporal Server starts a new Workflow Execution. Date: Mon, 5 Jan 2026 11:11:44 -0600 Subject: [PATCH 30/40] Update docs/build-your-first-basic-workflow/build-your-first-workflow.mdx Co-authored-by: Milecia McG <47196133+flippedcoder@users.noreply.github.com> --- .../build-your-first-workflow.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 9f9eb27114..e73bb1729b 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -2051,7 +2051,7 @@ Now that your Worker is running and polling for tasks, you can start a Workflow **In Terminal 3, start the Workflow:** -The workflow starter script starts a Workflow Execution. Each time you run it, the Temporal Server starts a new Workflow Execution. +The Workflow starter script starts a Workflow Execution. Each time you run it, the Temporal Server starts a new Workflow Execution. Date: Mon, 5 Jan 2026 11:12:01 -0600 Subject: [PATCH 31/40] Update docs/build-your-first-basic-workflow/build-your-first-workflow.mdx Co-authored-by: Milecia McG <47196133+flippedcoder@users.noreply.github.com> --- .../build-your-first-workflow.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index e73bb1729b..7cc069fd7f 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -1923,7 +1923,7 @@ try { -This code uses a Temporal Client to connect to the Temporal Service, calling its workflow start method to request execution. This returns a handle, and calling result on that handle will block until execution is complete, at which point it provides the result. +This code uses a Temporal Client to connect to the Temporal Service, calling its Workflow start method to request execution. This returns a handle, and calling result on that handle will block until execution is complete, at which point it provides the result. ## Run Your Money Transfer From 3a316a0d60e236a5f26d43ef09e3940de86b9255 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 5 Jan 2026 13:41:54 -0600 Subject: [PATCH 32/40] capitalization, uncollapsed, removing sdk box logos --- .../build-your-first-workflow.mdx | 13 +++++-------- static/img/sdks/sdk-box-logos/dotnet.svg | 14 -------------- static/img/sdks/sdk-box-logos/go.svg | 8 -------- static/img/sdks/sdk-box-logos/java.svg | 5 ----- static/img/sdks/sdk-box-logos/php.svg | 4 ---- static/img/sdks/sdk-box-logos/python.svg | 5 ----- static/img/sdks/sdk-box-logos/ruby.png | Bin 2993 -> 0 bytes static/img/sdks/sdk-box-logos/ruby.svg | 10 ---------- static/img/sdks/sdk-box-logos/typescript.svg | 4 ---- 9 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 static/img/sdks/sdk-box-logos/dotnet.svg delete mode 100644 static/img/sdks/sdk-box-logos/go.svg delete mode 100644 static/img/sdks/sdk-box-logos/java.svg delete mode 100644 static/img/sdks/sdk-box-logos/php.svg delete mode 100644 static/img/sdks/sdk-box-logos/python.svg delete mode 100644 static/img/sdks/sdk-box-logos/ruby.png delete mode 100644 static/img/sdks/sdk-box-logos/ruby.svg delete mode 100644 static/img/sdks/sdk-box-logos/typescript.svg diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 7cc069fd7f..e6ea951e3d 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -54,10 +54,7 @@ You'll understand the core building blocks of Temporal and learn how Temporal he ### Prerequisites -Before starting this tutorial: - -- **Set up a local development environment** for developing Temporal applications -- **Ensure you have Git installed** to clone the project +Before you begin, set up your local development environment: -
+
What You'll Build and Run The project in this tutorial mimics a "money transfer" application. @@ -205,7 +202,7 @@ To perform a money transfer, you will do the following: 1. **Launch a Worker**: Since a Worker is responsible for executing the Workflow and Activity code, at least one Worker must be running for the money transfer to make progress. -2. **Submit a Workflow Execution request** to the Temporal Service: After the Worker communicates with the Temporal Service, the Worker will begin executing the Workflow and Activity code. It reports the results to the Temporal Service, which tracks the progress of the Workflow Execution. +2. **Start a Workflow Execution** through the Temporal Service: After the Worker communicates with the Temporal Service, the Worker will begin executing the Workflow and Activity code. It reports the results to the Temporal Service, which tracks the progress of the Workflow Execution.
@@ -1204,7 +1201,7 @@ $this->bankingActivity = Workflow::newActivityStub( ### What Makes Errors Non-Retryable? -Without retry policies, a temporary network glitch could cause your entire money transfer to fail. With Temporal's intelligent retries, your workflow becomes resilient to these common infrastructure issues. +Without retry policies, a temporary network glitch could cause your entire money transfer to fail. With Temporal's intelligent retries, your Workflow becomes resilient to these common infrastructure issues. @@ -2095,7 +2092,7 @@ The Temporal Web UI lets you see details about the Workflow you just ran. Continue to Part 2: Simulate Failures diff --git a/static/img/sdks/sdk-box-logos/dotnet.svg b/static/img/sdks/sdk-box-logos/dotnet.svg deleted file mode 100644 index 5116220593..0000000000 --- a/static/img/sdks/sdk-box-logos/dotnet.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/static/img/sdks/sdk-box-logos/go.svg b/static/img/sdks/sdk-box-logos/go.svg deleted file mode 100644 index fdefe31b6d..0000000000 --- a/static/img/sdks/sdk-box-logos/go.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/static/img/sdks/sdk-box-logos/java.svg b/static/img/sdks/sdk-box-logos/java.svg deleted file mode 100644 index 62d4525175..0000000000 --- a/static/img/sdks/sdk-box-logos/java.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/img/sdks/sdk-box-logos/php.svg b/static/img/sdks/sdk-box-logos/php.svg deleted file mode 100644 index 4a2c2e7d91..0000000000 --- a/static/img/sdks/sdk-box-logos/php.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/static/img/sdks/sdk-box-logos/python.svg b/static/img/sdks/sdk-box-logos/python.svg deleted file mode 100644 index 2d59a0af4c..0000000000 --- a/static/img/sdks/sdk-box-logos/python.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/static/img/sdks/sdk-box-logos/ruby.png b/static/img/sdks/sdk-box-logos/ruby.png deleted file mode 100644 index af1dc434d24b58d5c4c25e65a5622e54c304b3a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2993 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 P7B&$~U01CNY7_n+7n-%l diff --git a/static/img/sdks/sdk-box-logos/ruby.svg b/static/img/sdks/sdk-box-logos/ruby.svg deleted file mode 100644 index b6b8d205db..0000000000 --- a/static/img/sdks/sdk-box-logos/ruby.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/static/img/sdks/sdk-box-logos/typescript.svg b/static/img/sdks/sdk-box-logos/typescript.svg deleted file mode 100644 index 31c98dd6f5..0000000000 --- a/static/img/sdks/sdk-box-logos/typescript.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - From a4d89d1320ca0cc391268c583119675f8c8bcabb Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 5 Jan 2026 14:38:41 -0600 Subject: [PATCH 33/40] replace static reimburse img with built in themedimage component, created and added light and dark mode reimburse diagram, max width 60 --- .../build-your-first-workflow.mdx | 10 +++++++++- static/img/moneytransfer/money-withdrawal.png | Bin 4928 -> 0 bytes static/img/moneytransfer/reimbursedark.png | Bin 0 -> 14772 bytes static/img/moneytransfer/reimburselight.png | Bin 0 -> 12358 bytes 4 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 static/img/moneytransfer/money-withdrawal.png create mode 100644 static/img/moneytransfer/reimbursedark.png create mode 100644 static/img/moneytransfer/reimburselight.png diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index e6ea951e3d..d111e027bf 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -22,6 +22,7 @@ import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { RetryPolicyComparison } from "@site/src/components/RetryPolicyComparison"; import { NextButton } from "@site/src/components/TutorialNavigation"; import SdkTabs from "@site/src/components/elements/SdkTabs"; +import ThemedImage from '@theme/ThemedImage'; import { FaPython, FaJava } from 'react-icons/fa'; import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; @@ -70,7 +71,14 @@ You’ll build a basic money transfer app from the ground up, learning how to ha Most applications require multiple coordinated steps - processing payments, sending emails, updating databases. This tutorial uses money transfers to demonstrate how Temporal ensures these multi-step processes complete reliably, resuming exactly where they left off even after any failure.
- Money Transfer Application Flow +
diff --git a/static/img/moneytransfer/money-withdrawal.png b/static/img/moneytransfer/money-withdrawal.png deleted file mode 100644 index 07bfa088009a7194e8d2e5fbd474d9b1275e0124..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4928 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�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 I!|CO}0sn7c!~g&Q literal 0 HcmV?d00001 From 32219dc4416e8d0f0e373fc870316e5fd6a8b6c3 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 7 Jan 2026 09:37:50 -0600 Subject: [PATCH 34/40] changing progress --- .../build-your-first-workflow.mdx | 10 +- .../failure-simulation.mdx | 6 +- .../TemporalProgress/TemporalProgress.js | 17 ++- .../TemporalProgress.module.css | 105 ++++++++++++++---- 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index d111e027bf..5e6723490b 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -40,13 +40,9 @@ In this tutorial, you'll build and run your first Temporal application. You'll understand the core building blocks of Temporal and learn how Temporal helps you build crash proof applications through durable execution. -
- Temporal beginner -
- @@ -1215,7 +1211,7 @@ Without retry policies, a temporary network glitch could cause your entire money :::important This is a Simplified Example -This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling. +This tutorial shows core Temporal features and is not intended for production use. ::: ## Step 3: Create a Worker file diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 5d8791357f..05a15ed375 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -59,8 +59,8 @@ In this part, you'll simulate failures to see how Temporal handles them. This demonstrates why Temporal is particularly useful for building reliable systems. Systems fail in unpredictable ways. A seemingly harmless deployment can bring down production, a database connection can time out during peak traffic, or a third-party service can decide to have an outage. @@ -105,7 +105,7 @@ In traditional systems, this would corrupt the transaction or lose data entirely Web UI is open at `http://localhost:8233` -
+
What's happening behind the scenes? Unlike many modern applications that require complex leader election processes and external databases to handle failure, Temporal automatically preserves the state of your Workflow even if the server is down. diff --git a/src/components/TemporalProgress/TemporalProgress.js b/src/components/TemporalProgress/TemporalProgress.js index 10ab921253..50e9b294ee 100644 --- a/src/components/TemporalProgress/TemporalProgress.js +++ b/src/components/TemporalProgress/TemporalProgress.js @@ -1,4 +1,5 @@ import React from 'react'; +import Link from '@docusaurus/Link'; import styles from './TemporalProgress.module.css'; export const TemporalProgress = ({ steps }) => { @@ -6,11 +7,17 @@ export const TemporalProgress = ({ steps }) => {
{steps.map((step, index) => ( -
- {step.label} -
- {index < steps.length - 1 && ( -
+ {step.href ? ( + + {step.label} + + ) : ( +
+ {step.label} +
)}
))} diff --git a/src/components/TemporalProgress/TemporalProgress.module.css b/src/components/TemporalProgress/TemporalProgress.module.css index 19799fcefb..2390c02ea2 100644 --- a/src/components/TemporalProgress/TemporalProgress.module.css +++ b/src/components/TemporalProgress/TemporalProgress.module.css @@ -1,43 +1,102 @@ .temporalProgress { display: flex; align-items: center; + gap: 0.75rem; margin: 2rem 0; - padding: 1.5rem; - background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); - border-radius: 12px; - color: white; } .progressStep { - padding: 0.75rem 1.5rem; - border-radius: 8px; + padding: 0.6rem 1.25rem; + border-radius: 4px; font-weight: 600; - font-size: 0.9rem; - min-width: 200px; - text-align: center; + font-size: 0.875rem; + transition: all 0.15s ease; + text-decoration: none !important; + cursor: pointer; +} + +.progressStep:hover { + text-decoration: none !important; } +/* Light mode - completed */ .progressStep.completed { - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + background: #E0E7FF; + color: #3730A3; + opacity: 0.7; } +.progressStep.completed:hover { + opacity: 1; + background: #C7D2FE; +} + +/* Light mode - active */ .progressStep.active { - background: rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); + background: #4F46E5; + color: white; + box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3); +} + +.progressStep.active:hover { + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4); + transform: translateY(-1px); } +/* Light mode - pending */ .progressStep.pending { - background: rgba(255, 255, 255, 0.1); - opacity: 0.6; + background: #E0E7FF; + color: #3730A3; + opacity: 0.7; +} + +.progressStep.pending:hover { + opacity: 1; + background: #C7D2FE; } +/* Light mode - connector */ .progressConnector { - flex: 1; - height: 2px; - background: linear-gradient(90deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); - margin: 0 1rem; -} \ No newline at end of file + color: #A5B4FC; + font-size: 0.875rem; +} + +/* Dark mode - completed */ +[data-theme='dark'] .progressStep.completed { + background: rgba(124, 58, 237, 0.2); + color: #A78BFA; + opacity: 0.6; +} + +[data-theme='dark'] .progressStep.completed:hover { + opacity: 1; + background: rgba(124, 58, 237, 0.35); +} + +/* Dark mode - active */ +[data-theme='dark'] .progressStep.active { + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + color: white; + box-shadow: 0 2px 12px rgba(124, 58, 237, 0.35); +} + +[data-theme='dark'] .progressStep.active:hover { + box-shadow: 0 4px 16px rgba(124, 58, 237, 0.45); +} + +/* Dark mode - pending */ +[data-theme='dark'] .progressStep.pending { + background: rgba(124, 58, 237, 0.2); + color: #A78BFA; + opacity: 0.6; +} + +[data-theme='dark'] .progressStep.pending:hover { + opacity: 1; + background: rgba(124, 58, 237, 0.35); +} + +/* Dark mode - connector */ +[data-theme='dark'] .progressConnector { + color: rgba(124, 58, 237, 0.6); +} From f98283b8725482a0fbaa2f2805fbe88eef151dae Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Fri, 16 Jan 2026 17:18:13 -0600 Subject: [PATCH 35/40] fix page jumping --- src/components/AnimatedTerminal/AnimatedTerminal.js | 4 +++- src/components/AnimatedTerminal/AnimatedTerminal.module.css | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.js b/src/components/AnimatedTerminal/AnimatedTerminal.js index c69ad7b30d..ed2bd83ce8 100644 --- a/src/components/AnimatedTerminal/AnimatedTerminal.js +++ b/src/components/AnimatedTerminal/AnimatedTerminal.js @@ -91,6 +91,8 @@ export const AnimatedTerminal = ({ return () => clearTimeout(timer); }, [currentLineIndex, lines, delay, typingSpeed, autoStart, loop, restartDelay, shouldStart]); + const bodyHeight = lines.length * 24 + 32; + return (
@@ -101,7 +103,7 @@ export const AnimatedTerminal = ({
Terminal
-
+
{displayedLines.map((line, index) => (
{prompt} diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.module.css b/src/components/AnimatedTerminal/AnimatedTerminal.module.css index 80683a5d63..efc317856c 100644 --- a/src/components/AnimatedTerminal/AnimatedTerminal.module.css +++ b/src/components/AnimatedTerminal/AnimatedTerminal.module.css @@ -48,7 +48,6 @@ .terminalBody { padding: 16px; - min-height: 120px; background: #1e1e1e; color: #f8f8f2; } From 5d332ca8893cf00046a94e9fa3b711ccdde27c8d Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Sun, 18 Jan 2026 23:21:29 -0600 Subject: [PATCH 36/40] edits --- .../build-your-first-workflow.mdx | 204 ++++++++++++++---- .../failure-simulation.mdx | 135 +++++++----- 2 files changed, 248 insertions(+), 91 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 5e6723490b..b0dd4eed1e 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -98,6 +98,9 @@ This allows you to focus on business logic, instead of writing application code cd money-transfer-project-template-python + + python -m pip install temporalio + @@ -122,13 +125,16 @@ This allows you to focus on business logic, instead of writing application code cd money-transfer-project-template-ts + + npm install + git clone https://github.com/temporalio/money-transfer-project-template-dotnet - cd money-transfer-temporal-template-dotnet + cd money-transfer-project-template-dotnet @@ -138,6 +144,9 @@ This allows you to focus on business logic, instead of writing application code cd money-transfer-project-template-ruby + + bundle install + @@ -286,9 +295,14 @@ class MoneyTransfer: raise refund_error ``` +:::tip Understanding the imports +- **`workflow.unsafe.imports_passed_through()`**: This context manager allows importing non-Workflow-safe modules (like Activities) into Workflow code. +- **`PaymentDetails`**: A dataclass defined in `shared.py` containing `source_account`, `target_account`, `amount`, and `reference_id` fields. +::: +

Activity Definition

-Activities handle the business logic. Each activity method calls an external banking service: +Activities handle the business logic. Each Activity method calls an external banking service: **activities.py** @@ -311,8 +325,35 @@ class BankingActivities: except Exception: activity.logger.exception("Withdrawal failed") raise + + @activity.defn + async def deposit(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-deposit" + try: + # In Part 2, you'll comment out this line and uncomment the failing one below + confirmation = await asyncio.to_thread( + self.bank.deposit, data.target_account, data.amount, reference_id + ) + + # Uncomment this line in Part 2 to simulate a failure: + # confirmation = await asyncio.to_thread( + # self.bank.deposit_that_fails, + # data.target_account, + # data.amount, + # reference_id, + # ) + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Deposit failed") + raise ``` +:::note +The `self.bank` attribute is a `BankingService` client initialized in the class constructor (`__init__`). The full class also includes a `refund()` method. You can view the complete implementation in the cloned repository. +::: + @@ -415,7 +456,13 @@ func Deposit(ctx context.Context, data PaymentDetails) (string, error) { referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) bank := BankingService{"bank-api.example.com"} + + // In Part 2, you'll comment out this line and uncomment the failing one below confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + + // Uncomment this line in Part 2 to simulate a failure: + // confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + return confirmation, err } ``` @@ -545,8 +592,17 @@ public class AccountActivityImpl implements AccountActivity { @Override public void deposit(String accountId, String referenceId, int amount) { - System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", - amount, accountId, referenceId); + // This variable controls whether the deposit succeeds or fails. + // You'll change this to false in Part 2 to simulate a bug. + boolean activityShouldSucceed = true; + + if (activityShouldSucceed) { + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } else { + System.out.println("Deposit failed"); + throw new RuntimeException("Simulated deposit failure"); + } } @Override @@ -557,6 +613,10 @@ public class AccountActivityImpl implements AccountActivity { } ``` +:::tip View the complete code +For the full implementation, see [`AccountActivityImpl.java`](https://github.com/temporalio/money-transfer-project-java/blob/main/src/main/java/moneytransferapp/AccountActivityImpl.java) in the GitHub repository. +::: + @@ -659,12 +719,13 @@ export async function deposit(details: PaymentDetails): Promise { console.log( `Depositing $${details.amount} into account ${details.targetAccount}.\n\n` ); - const bank2 = new BankingService('bank2.example.com'); - return await bank2.deposit( - details.targetAccount, - details.amount, - details.referenceId - ); + const bank = new BankingService('bank2.example.com'); + + // In Part 2, you'll comment out this line and uncomment the failing one below + return await bank.deposit(details.targetAccount, details.amount, details.referenceId); + + // Uncomment this line in Part 2 to simulate a failure: + // return await bank.depositThatFails(details.targetAccount, details.amount, details.referenceId); } export async function refund(details: PaymentDetails): Promise { @@ -764,7 +825,10 @@ module BankActivities def execute(details) puts("Doing a withdrawal from #{details.source_account} for #{details.amount}") raise InsufficientFundsError, 'Transfer amount too large' if details.amount > 1000 - + + # Uncomment to expose a bug and cause the Activity to fail + # x = details.amount / 0 + "OKW-#{details.amount}-#{details.source_account}" end end @@ -773,7 +837,7 @@ module BankActivities def execute(details) puts("Doing a deposit into #{details.target_account} for #{details.amount}") raise InvalidAccountError, 'Invalid account number' if details.target_account == 'B5555' - + "OKD-#{details.amount}-#{details.target_account}" end end @@ -781,7 +845,7 @@ module BankActivities class Refund < Temporalio::Activity::Definition def execute(details) puts("Refunding #{details.amount} back to account #{details.source_account}") - + "OKR-#{details.amount}-#{details.source_account}" end end @@ -914,6 +978,52 @@ final class BankingActivity implements Banking throw $e; } } + + #[\Override] + public function deposit(PaymentDetails $data): string + { + $referenceId = $data->referenceId . "-deposit"; + try { + $confirmation = $this->bank->deposit( + $data->targetAccount, + $data->amount, + $referenceId, + ); + // In Part 2, uncomment the block below and comment out the line above + /* + $confirmation = $this->bank->depositThatFails( + $data->targetAccount, + $data->amount, + $referenceId + ); + */ + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Deposit failed", ['exception' => $e]); + throw $e; + } + } + + #[\Override] + public function refund(PaymentDetails $data): string + { + $referenceId = $data->referenceId . "-refund"; + try { + $confirmation = $this->bank->deposit( + $data->sourceAccount, + $data->amount, + $referenceId, + ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Refund failed", ['exception' => $e]); + throw $e; + } + } } ``` @@ -1029,6 +1139,9 @@ public class BankingActivities { var bankService = new BankingService("bank2.example.com"); Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + // In Part 2, you'll uncomment this failing line and comment out the try/catch block below + // return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); try { @@ -1100,7 +1213,7 @@ options := workflow.ActivityOptions{ -**src/main/java/moneytransfer/MoneyTransferWorkflowImpl.java** +**src/main/java/moneytransferapp/MoneyTransferWorkflowImpl.java** ```java // ... @@ -1290,7 +1403,7 @@ func main() { -**src/main/java/moneytransfer/MoneyTransferWorker.java** +**src/main/java/moneytransferapp/MoneyTransferWorker.java** ```java package moneytransferapp; @@ -1519,7 +1632,7 @@ MONEY_TRANSFER_TASK_QUEUE_NAME = "MONEY_TRANSFER_TASK_QUEUE" -**app/shared.go** +**shared.go** ```go package app @@ -1533,7 +1646,7 @@ const MoneyTransferTaskQueueName = "MONEY_TRANSFER_TASK_QUEUE" -**src/main/java/moneytransfer/Shared.java** +**src/main/java/moneytransferapp/Shared.java** ```java package moneytransferapp; @@ -1618,7 +1731,7 @@ Now you'll create a client program that starts a Workflow execution. This code c -**start_workflow.py** +**run_workflow.py** ```python import asyncio @@ -1708,7 +1821,7 @@ func main() { -**src/main/java/moneytransfer/TransferApp.java** +**src/main/java/moneytransferapp/TransferApp.java** ```java public class TransferApp { @@ -1720,7 +1833,6 @@ public class TransferApp { WorkflowClient client = WorkflowClient.newInstance(serviceStub); // Workflow options configure Workflow stubs. - // A WorkflowId prevents duplicate instances. WorkflowOptions options = WorkflowOptions.newBuilder() .setTaskQueue(Shared.MONEY_TRANSFER_TASK_QUEUE) .setWorkflowId("money-transfer-workflow") @@ -1730,12 +1842,15 @@ public class TransferApp { // but actually perform a gRPC call to the Temporal Service. MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); - // Configure the details for this money transfer request + // Generate random transaction details (values will differ each run) + Random random = new Random(); String referenceId = UUID.randomUUID().toString().substring(0, 18); - String fromAccount = "A1001"; - String toAccount = "B2002"; - int amountToTransfer = 100; - TransactionDetails transaction = new CoreTransactionDetails(fromAccount, toAccount, referenceId, amountToTransfer); + String fromAccount = String.valueOf(random.nextInt(1000000000)); + String toAccount = String.valueOf(random.nextInt(1000000000)); + int amountToTransfer = random.nextInt(1000); + + TransactionDetails transaction = new CoreTransactionDetails( + fromAccount, toAccount, referenceId, amountToTransfer); // Perform asynchronous execution WorkflowExecution we = WorkflowClient.start(workflow::transfer, transaction); @@ -1748,6 +1863,10 @@ public class TransferApp { } ``` +:::note +The Java template generates **random** account numbers and transfer amounts each time you run it. Your output values will differ from the examples shown. +::: + @@ -1797,7 +1916,7 @@ run().catch((err) => { -**MoneyTransferStarter/Program.cs** +**MoneyTransferClient/Program.cs** ```csharp using Temporalio.Client; @@ -1807,20 +1926,19 @@ using Temporalio.MoneyTransferProject.MoneyTransferWorker; var client = await TemporalClient.ConnectAsync(new("localhost:7233")); // Configure the money transfer details -var transferDetails = new TransferDetails -{ - SourceAccount = "A1001", - TargetAccount = "B2002", - Amount = 100, - ReferenceId = "12345" -}; +var paymentDetails = new PaymentDetails( + SourceAccount: "A1001", + TargetAccount: "B2002", + Amount: 100, + ReferenceId: "12345" +); -Console.WriteLine($"Starting transfer of ${transferDetails.Amount} from {transferDetails.SourceAccount} to {transferDetails.TargetAccount}"); +Console.WriteLine($"Starting transfer of ${paymentDetails.Amount} from {paymentDetails.SourceAccount} to {paymentDetails.TargetAccount}"); // Start the workflow var handle = await client.StartWorkflowAsync( - (MoneyTransferWorkflow wf) => wf.RunAsync(transferDetails), - new(id: $"money-transfer-{transferDetails.ReferenceId}", taskQueue: "MONEY_TRANSFER_TASK_QUEUE")); + (MoneyTransferWorkflow wf) => wf.RunAsync(paymentDetails), + new(id: $"money-transfer-{paymentDetails.ReferenceId}", taskQueue: "MONEY_TRANSFER_TASK_QUEUE")); Console.WriteLine($"Started workflow {handle.Id}"); @@ -2023,11 +2141,15 @@ Transfer complete (transaction IDs: W1779185060, D1779185060)
Worker is running and actively polling the Task Queue. To quit, use ^C to interrupt. -Withdrawing $62 from account 249946050. -[ReferenceId: 1480a22d-d0fc-4361] -Depositing $62 into account 591856595. -[ReferenceId: 1480a22d-d0fc-4361] -[1480a22d-d0fc-4361] Transaction succeeded. +Withdrawing $[AMOUNT] from account [SOURCE_ACCOUNT]. +[ReferenceId: [UUID]] +Depositing $[AMOUNT] into account [DEST_ACCOUNT]. +[ReferenceId: [UUID]] +[[UUID]] Transaction succeeded.
+ +:::note +Your account numbers and amount will be different - the Java template generates random values each run. +::: Transfer complete (transaction IDs: W3436600150, D9270097234) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 05a15ed375..c919ef01a5 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -287,7 +287,7 @@ In this part of the tutorial, you will inject a bug into your production code, w This demo application makes a call to an external service in an Activity. If that call fails due to a bug in your code, the Activity produces an error. -To test this out and see how Temporal responds, you'll simulate a bug in the Deposit Activity function or method. +To test this out and see how Temporal responds, you'll simulate a bug in one of the Activity functions or methods. -Find the `deposit()` method and **change `activityShouldSucceed` to `false`**: +Open `AccountActivityImpl.java` and find the `deposit()` method. **Change `activityShouldSucceed` to `false`**: -**AccountActivityImpl.java** +**src/main/java/moneytransferapp/AccountActivityImpl.java** ```java -public String deposit(PaymentDetails details) { +@Override +public void deposit(String accountId, String referenceId, int amount) { // Change this to false to simulate failure: boolean activityShouldSucceed = false; - // ... rest of your method + if (activityShouldSucceed) { + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } else { + System.out.println("Deposit failed"); + throw new RuntimeException("Simulated deposit failure"); + } } ``` @@ -415,11 +422,16 @@ Save your changes. You've now created a deliberate failure point in your deposit -Find the `deposit()` function and **uncomment the failing line** while **commenting out the working line**: +Find the `deposit()` function in `activities.ts` and **comment out the working line** while **uncommenting the failing line**: -**activities.ts** +**src/activities.ts** ```typescript export async function deposit(details: PaymentDetails): Promise { + console.log( + `Depositing $${details.amount} into account ${details.targetAccount}.\n\n` + ); + const bank = new BankingService('bank2.example.com'); + // Comment out this working line: // return await bank.deposit(details.targetAccount, details.amount, details.referenceId); @@ -467,27 +479,32 @@ Save your changes. You've now created a deliberate failure point in your deposit -Find the `deposit` method and **uncomment the failing line** that causes a divide-by-zero error: +Find the `Withdraw` class in `activities.rb` and **uncomment the failing line** that causes a divide-by-zero error: **activities.rb** ```ruby -def deposit(details) - # Uncomment this line to introduce the bug: - result = 100 / 0 # This will cause a divide-by-zero error - - # Your existing deposit logic here... +class Withdraw < Temporalio::Activity::Definition + def execute(details) + puts("Doing a withdrawal from #{details.source_account} for #{details.amount}") + raise InsufficientFundsError, 'Transfer amount too large' if details.amount > 1000 + + # Uncomment to expose a bug and cause the Activity to fail + x = details.amount / 0 + + "OKW-#{details.amount}-#{details.source_account}" + end end ``` -Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. +Save your changes. You've now created a deliberate failure point in your withdraw Activity. This simulates a real-world scenario where external service calls might fail intermittently. -Find the `deposit()` method in `BankingActivity.php` and **uncomment the failing line** while **commenting out the working line**: +Find the `deposit()` method in `src/Banking/Internal/BankingActivity.php` and **uncomment the failing block** while **commenting out the working line**: -**BankingActivity.php** +**src/Banking/Internal/BankingActivity.php** ```php #[\Override] @@ -496,18 +513,19 @@ public function deposit(PaymentDetails $data): string $referenceId = $data->referenceId . "-deposit"; try { // Comment out this working line: - // $confirmation = $this->bank->deposit( - // $data->targetAccount, - // $data->amount, - // $referenceId, - // ); - - // Uncomment this failing line: - $confirmation = $this->bank->depositThatFails( + /* + $confirmation = $this->bank->deposit( $data->targetAccount, $data->amount, $referenceId, ); + */ + // Uncomment this failing block: + $confirmation = $this->bank->depositThatFails( + $data->targetAccount, + $data->amount, + $referenceId + ); return $confirmation; } catch (InvalidAccount $e) { throw $e; @@ -525,7 +543,7 @@ Save your changes. You've now created a deliberate failure point in your deposit ### Step 3: Start Worker & Observe Retry Behavior -Now let's see how Temporal handles this failure. When you start your Worker, it will execute the withdraw Activity successfully, but hit the failing deposit Activity. Instead of the entire Workflow failing permanently, Temporal will retry the failed Activity according to your retry policy. +Now let's see how Temporal handles this failure. When you start your Worker, it will hit the failing Activity. Instead of the entire Workflow failing permanently, Temporal will retry the failed Activity according to your retry policy. @@ -692,18 +710,17 @@ bundle exec ruby starter.rb ``` **Here's what you'll see:** -- The `withdraw` Activity completes successfully -- The `deposit` Activity fails and retries automatically +- The `Withdraw` Activity fails and retries automatically (due to the divide-by-zero bug) +- The `Deposit` Activity never runs because the Workflow is stuck on the failing Withdraw -**Key observation:** Your Workflow isn't stuck or terminated. Temporal automatically retries the failed Activity according to your configured retry policy, while maintaining the overall Workflow state. The successful withdraw Activity doesn't get re-executed - only the failed deposit Activity is retried. +**Key observation:** Your Workflow isn't stuck or terminated. Temporal automatically retries the failed Activity according to your configured retry policy, while maintaining the overall Workflow state. Successfully completed Activities aren't re-executed - only the failed Activity is retried. ### Step 4: Fix the Bug @@ -801,13 +818,20 @@ func Deposit(ctx context.Context, data PaymentDetails) (string, error) { Go back to `AccountActivityImpl.java` and **change `activityShouldSucceed` back to `true`**: -**AccountActivityImpl.java** +**src/main/java/moneytransferapp/AccountActivityImpl.java** ```java -public String deposit(PaymentDetails details) { +@Override +public void deposit(String accountId, String referenceId, int amount) { // Change this back to true to fix the bug: boolean activityShouldSucceed = true; - // ... rest of your method + if (activityShouldSucceed) { + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } else { + System.out.println("Deposit failed"); + throw new RuntimeException("Simulated deposit failure"); + } } ``` @@ -817,9 +841,14 @@ public String deposit(PaymentDetails details) { Go back to `activities.ts` and **reverse the comments** - comment out the failing line and uncomment the working line: -**activities.ts** +**src/activities.ts** ```typescript export async function deposit(details: PaymentDetails): Promise { + console.log( + `Depositing $${details.amount} into account ${details.targetAccount}.\n\n` + ); + const bank = new BankingService('bank2.example.com'); + // Uncomment this working line: return await bank.deposit(details.targetAccount, details.amount, details.referenceId); @@ -865,11 +894,16 @@ Go back to `activities.rb` and **comment out the failing line**: **activities.rb** ```ruby -def deposit(details) - # Comment out this problematic line: - # result = 100 / 0 # This will cause a divide-by-zero error - - # Your existing deposit logic here... +class Withdraw < Temporalio::Activity::Definition + def execute(details) + puts("Doing a withdrawal from #{details.source_account} for #{details.amount}") + raise InsufficientFundsError, 'Transfer amount too large' if details.amount > 1000 + + # Comment out this failing line: + # x = details.amount / 0 + + "OKW-#{details.amount}-#{details.source_account}" + end end ``` @@ -879,7 +913,7 @@ end Go back to `BankingActivity.php` and **reverse the comments** - comment out the failing line and uncomment the working line: -**BankingActivity.php** +**src/Banking/Internal/BankingActivity.php** ```php #[\Override] @@ -893,13 +927,14 @@ public function deposit(PaymentDetails $data): string $data->amount, $referenceId, ); - - // Comment out this failing line: - // $confirmation = $this->bank->depositThatFails( - // $data->targetAccount, - // $data->amount, - // $referenceId, - // ); + // Comment out this failing block: + /* + $confirmation = $this->bank->depositThatFails( + $data->targetAccount, + $data->amount, + $referenceId + ); + */ return $confirmation; } catch (InvalidAccount $e) { throw $e; @@ -1025,7 +1060,7 @@ Ctrl+C bundle exec ruby worker.rb ``` -**On the next retry attempt,** your fixed `deposit` Activity will succeed, and you'll see the Workflow complete successfully. +**On the next retry attempt,** your fixed `Withdraw` Activity will succeed, and you'll see the Workflow complete successfully. From 238dc3a4d3a8e07579b4bf8d25fa57ee9a9c75e2 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 19 Jan 2026 12:43:51 -0600 Subject: [PATCH 37/40] adding code highlighting and making comments visible --- .../build-your-first-workflow.mdx | 128 ++++++++++++------ src/css/custom.css | 11 ++ 2 files changed, 99 insertions(+), 40 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index b0dd4eed1e..02e6fdeee7 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -246,9 +246,11 @@ with workflow.unsafe.imports_passed_through(): from activities import BankingActivities from shared import PaymentDetails -@workflow.defn +# highlight-next-line +@workflow.defn # Marks this class as a Workflow Definition class MoneyTransfer: - @workflow.run + # highlight-next-line + @workflow.run # Entry point method that Temporal calls when the Workflow starts async def run(self, payment_details: PaymentDetails) -> str: retry_policy = RetryPolicy( maximum_attempts=3, @@ -257,21 +259,25 @@ class MoneyTransfer: ) # Withdraw money - withdraw_output = await workflow.execute_activity_method( + # highlight-start + withdraw_output = await workflow.execute_activity_method( # Executes an Activity and waits for result BankingActivities.withdraw, payment_details, start_to_close_timeout=timedelta(seconds=5), retry_policy=retry_policy, ) + # highlight-end # Deposit money try: + # highlight-start deposit_output = await workflow.execute_activity_method( BankingActivities.deposit, payment_details, start_to_close_timeout=timedelta(seconds=5), retry_policy=retry_policy, ) + # highlight-end result = f"Transfer complete (transaction IDs: {withdraw_output}, {deposit_output})" return result @@ -312,7 +318,8 @@ from temporalio import activity from shared import PaymentDetails class BankingActivities: - @activity.defn + # highlight-next-line + @activity.defn # Marks this method as an Activity Definition async def withdraw(self, data: PaymentDetails) -> str: reference_id = f"{data.reference_id}-withdrawal" try: @@ -326,7 +333,8 @@ class BankingActivities: activity.logger.exception("Withdrawal failed") raise - @activity.defn + # highlight-next-line + @activity.defn # Marks this method as an Activity Definition async def deposit(self, data: PaymentDetails) -> str: reference_id = f"{data.reference_id}-deposit" try: @@ -367,7 +375,8 @@ This is what the Workflow Definition looks like for the money transfer process: **workflow.go** ```go -func MoneyTransfer(ctx workflow.Context, input PaymentDetails) (string, error) { +// highlight-next-line +func MoneyTransfer(ctx workflow.Context, input PaymentDetails) (string, error) { // Workflow Definition: accepts workflow.Context as first param // RetryPolicy specifies how to automatically handle retries if an Activity fails. retrypolicy := &temporal.RetryPolicy{ InitialInterval: time.Second, @@ -390,14 +399,16 @@ func MoneyTransfer(ctx workflow.Context, input PaymentDetails) (string, error) { // Withdraw money. var withdrawOutput string - withdrawErr := workflow.ExecuteActivity(ctx, Withdraw, input).Get(ctx, &withdrawOutput) + // highlight-next-line + withdrawErr := workflow.ExecuteActivity(ctx, Withdraw, input).Get(ctx, &withdrawOutput) // Executes Activity and waits for result if withdrawErr != nil { return "", withdrawErr } // Deposit money. var depositOutput string - depositErr := workflow.ExecuteActivity(ctx, Deposit, input).Get(ctx, &depositOutput) + // highlight-next-line + depositErr := workflow.ExecuteActivity(ctx, Deposit, input).Get(ctx, &depositOutput) // Executes Activity and waits for result if depositErr != nil { // The deposit failed; put money back in original account. var result string @@ -436,7 +447,8 @@ Activities handle the business logic. Each Activity function calls an external b **activity.go** ```go -func Withdraw(ctx context.Context, data PaymentDetails) (string, error) { +// highlight-next-line +func Withdraw(ctx context.Context, data PaymentDetails) (string, error) { // Activity Definition: accepts context.Context as first param log.Printf("Withdrawing $%d from account %s.\n\n", data.Amount, data.SourceAccount, @@ -448,7 +460,8 @@ func Withdraw(ctx context.Context, data PaymentDetails) (string, error) { return confirmation, err } -func Deposit(ctx context.Context, data PaymentDetails) (string, error) { +// highlight-next-line +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { // Activity Definition: accepts context.Context as first param log.Printf("Depositing $%d into account %s.\n\n", data.Amount, data.TargetAccount, @@ -478,9 +491,11 @@ In the Temporal Java SDK, a Workflow Definition is marked by the `@WorkflowInter **MoneyTransferWorkflow.java** ```java -@WorkflowInterface +// highlight-next-line +@WorkflowInterface // Marks this interface as a Workflow Definition public interface MoneyTransferWorkflow { - @WorkflowMethod + // highlight-next-line + @WorkflowMethod // Entry point method that Temporal calls when the Workflow starts void transfer(TransactionDetails transaction); } ``` @@ -516,7 +531,8 @@ public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { // Stage 1: Withdraw funds from source try { - accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); + // highlight-next-line + accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); // Executes Activity via stub } catch (Exception e) { System.out.printf("[%s] Withdrawal of $%d from account %s failed", transactionReferenceId, amountToTransfer, sourceAccountId); @@ -525,7 +541,8 @@ public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { // Stage 2: Deposit funds to destination try { - accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); + // highlight-next-line + accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); // Executes Activity via stub System.out.printf("[%s] Transaction succeeded.\n", transactionReferenceId); return; } catch (Exception e) { @@ -567,15 +584,19 @@ Activities handle the business logic. Each Activity method calls an external ban **AccountActivity.java** ```java -@ActivityInterface +// highlight-next-line +@ActivityInterface // Marks this interface as containing Activity Definitions public interface AccountActivity { - @ActivityMethod + // highlight-next-line + @ActivityMethod // Marks this method as an Activity Definition void withdraw(String accountId, String referenceId, int amount); - @ActivityMethod + // highlight-next-line + @ActivityMethod // Marks this method as an Activity Definition void deposit(String accountId, String referenceId, int amount); - @ActivityMethod + // highlight-next-line + @ActivityMethod // Marks this method as an Activity Definition void refund(String accountId, String referenceId, int amount); } ``` @@ -636,7 +657,8 @@ import { ApplicationFailure } from '@temporalio/common'; import type * as activities from './activities'; import type { PaymentDetails } from './shared'; -export async function moneyTransfer(details: PaymentDetails): Promise { +// highlight-next-line +export async function moneyTransfer(details: PaymentDetails): Promise { // Workflow Definition: an exported async function // Get the Activities for the Workflow and set up the Activity Options. const { withdraw, deposit, refund } = proxyActivities({ // RetryPolicy specifies how to automatically handle retries if an Activity fails. @@ -653,7 +675,8 @@ export async function moneyTransfer(details: PaymentDetails): Promise { // Execute the withdraw Activity let withdrawResult: string; try { - withdrawResult = await withdraw(details); + // highlight-next-line + withdrawResult = await withdraw(details); // Executes Activity via proxy and waits for result } catch (withdrawErr) { throw new ApplicationFailure(`Withdrawal failed. Error: ${withdrawErr}`); } @@ -661,7 +684,8 @@ export async function moneyTransfer(details: PaymentDetails): Promise { //Execute the deposit Activity let depositResult: string; try { - depositResult = await deposit(details); + // highlight-next-line + depositResult = await deposit(details); // Executes Activity via proxy and waits for result } catch (depositErr) { // The deposit failed; try to refund the money. let refundResult; @@ -703,7 +727,8 @@ Activities handle the business logic. Each Activity function calls an external b import type { PaymentDetails } from './shared'; import { BankingService } from './banking-client'; -export async function withdraw(details: PaymentDetails): Promise { +// highlight-next-line +export async function withdraw(details: PaymentDetails): Promise { // Activity Definition: an exported async function console.log( `Withdrawing $${details.amount} from account ${details.sourceAccount}.\n\n` ); @@ -715,7 +740,8 @@ export async function withdraw(details: PaymentDetails): Promise { ); } -export async function deposit(details: PaymentDetails): Promise { +// highlight-next-line +export async function deposit(details: PaymentDetails): Promise { // Activity Definition: an exported async function console.log( `Depositing $${details.amount} into account ${details.targetAccount}.\n\n` ); @@ -754,7 +780,8 @@ This is what the Workflow Definition looks like for the money transfer process: **workflow.rb** ```ruby -class MoneyTransferWorkflow < Temporalio::Workflow::Definition +# highlight-next-line +class MoneyTransferWorkflow < Temporalio::Workflow::Definition # Workflow Definition: extends Temporalio::Workflow::Definition def execute(details) retry_policy = Temporalio::RetryPolicy.new( max_interval: 10, @@ -766,20 +793,24 @@ class MoneyTransferWorkflow < Temporalio::Workflow::Definition Temporalio::Workflow.logger.info("Starting workflow (#{details})") - withdraw_result = Temporalio::Workflow.execute_activity( + # highlight-start + withdraw_result = Temporalio::Workflow.execute_activity( # Executes Activity and waits for result BankActivities::Withdraw, details, start_to_close_timeout: 5, retry_policy: retry_policy ) + # highlight-end begin - deposit_result = Temporalio::Workflow.execute_activity( + # highlight-start + deposit_result = Temporalio::Workflow.execute_activity( # Executes Activity and waits for result BankActivities::Deposit, details, start_to_close_timeout: 5, retry_policy: retry_policy ) + # highlight-end "Transfer complete (transaction IDs: #{withdraw_result}, #{deposit_result})" rescue Temporalio::Error::ActivityError => e @@ -821,7 +852,8 @@ Activities handle the business logic. Each Activity class calls an external bank ```ruby module BankActivities - class Withdraw < Temporalio::Activity::Definition + # highlight-next-line + class Withdraw < Temporalio::Activity::Definition # Activity Definition: extends Temporalio::Activity::Definition def execute(details) puts("Doing a withdrawal from #{details.source_account} for #{details.amount}") raise InsufficientFundsError, 'Transfer amount too large' if details.amount > 1000 @@ -833,7 +865,8 @@ module BankActivities end end - class Deposit < Temporalio::Activity::Definition + # highlight-next-line + class Deposit < Temporalio::Activity::Definition # Activity Definition: extends Temporalio::Activity::Definition def execute(details) puts("Doing a deposit into #{details.target_account} for #{details.amount}") raise InvalidAccountError, 'Invalid account number' if details.target_account == 'B5555' @@ -885,7 +918,8 @@ use Temporal\Workflow\ReturnType; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; -#[WorkflowInterface] +// highlight-next-line +#[WorkflowInterface] // Marks this class as a Workflow Definition final class MoneyTransfer { private Banking|ActivityProxy $bankingActivity; @@ -905,16 +939,20 @@ final class MoneyTransfer ); } - #[WorkflowMethod('money_transfer')] + // highlight-start + #[WorkflowMethod('money_transfer')] // Entry point method that Temporal calls when the Workflow starts #[ReturnType(Type::TYPE_STRING)] public function handle(PaymentDetails $paymentDetails): \Generator + // highlight-end { # Withdraw money - $withdrawOutput = yield $this->bankingActivity->withdraw($paymentDetails); + // highlight-next-line + $withdrawOutput = yield $this->bankingActivity->withdraw($paymentDetails); // Executes Activity via stub # Deposit money try { - $depositOutput = yield $this->bankingActivity->deposit($paymentDetails); + // highlight-next-line + $depositOutput = yield $this->bankingActivity->deposit($paymentDetails); // Executes Activity via stub return "Transfer complete (transaction IDs: {$withdrawOutput}, {$depositOutput})"; } catch (\Throwable $depositError) { # Handle deposit error @@ -960,7 +998,8 @@ final class BankingActivity implements Banking private readonly Service $bank, ) {} - #[\Override] + // highlight-next-line + #[\Override] // Activity method implementation public function withdraw(PaymentDetails $data): string { $referenceId = $data->referenceId . "-withdrawal"; @@ -979,7 +1018,8 @@ final class BankingActivity implements Banking } } - #[\Override] + // highlight-next-line + #[\Override] // Activity method implementation public function deposit(PaymentDetails $data): string { $referenceId = $data->referenceId . "-deposit"; @@ -1040,10 +1080,12 @@ This is what the Workflow Definition looks like for this process: **MoneyTransferWorker/Workflow.cs** ```csharp -[Workflow] +// highlight-next-line +[Workflow] // Marks this class as a Workflow Definition public class MoneyTransferWorkflow { - [WorkflowRun] + // highlight-next-line + [WorkflowRun] // Entry point method that Temporal calls when the Workflow starts public async Task RunAsync(PaymentDetails details) { // Retry policy @@ -1059,10 +1101,12 @@ public class MoneyTransferWorkflow string withdrawResult; try { - withdrawResult = await Workflow.ExecuteActivityAsync( + // highlight-start + withdrawResult = await Workflow.ExecuteActivityAsync( // Executes Activity and waits for result () => BankingActivities.WithdrawAsync(details), new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } ); + // highlight-end } catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException") { @@ -1072,10 +1116,12 @@ public class MoneyTransferWorkflow string depositResult; try { - depositResult = await Workflow.ExecuteActivityAsync( + // highlight-start + depositResult = await Workflow.ExecuteActivityAsync( // Executes Activity and waits for result () => BankingActivities.DepositAsync(details), new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } ); + // highlight-end return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})"; } catch (Exception depositEx) @@ -1119,7 +1165,8 @@ Activities handle the business logic. Each Activity method calls an external ban ```csharp public class BankingActivities { - [Activity] + // highlight-next-line + [Activity] // Marks this method as an Activity Definition public static async Task WithdrawAsync(PaymentDetails details) { var bankService = new BankingService("bank1.example.com"); @@ -1134,7 +1181,8 @@ public class BankingActivities } } - [Activity] + // highlight-next-line + [Activity] // Marks this method as an Activity Definition public static async Task DepositAsync(PaymentDetails details) { var bankService = new BankingService("bank2.example.com"); diff --git a/src/css/custom.css b/src/css/custom.css index 3e35bacafc..366fcfa91a 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1358,3 +1358,14 @@ code { padding: 0; width: 40px; } + +/* Make comments visible on highlighted code lines */ +.theme-code-block-highlighted-line .token.comment { + color: #89e5c3 !important; /* Light green that contrasts with the green highlight background */ + font-style: italic; +} + +/* For light theme, use a different color */ +html[data-theme='light'] .theme-code-block-highlighted-line .token.comment { + color: #8adfa7 !important; /* Darker green for light mode */ +} From df29cab6fa70e976d9fab119492d86d118a6ea52 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Tue, 20 Jan 2026 14:18:50 -0600 Subject: [PATCH 38/40] feedback --- .../build-your-first-workflow.mdx | 22 +++++++++++++++---- .../failure-simulation.mdx | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 02e6fdeee7..cf3256b446 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -90,7 +90,7 @@ This allows you to focus on business logic, instead of writing application code + git clone https://github.com/temporalio/money-transfer-project-template-python @@ -303,9 +303,23 @@ class MoneyTransfer: :::tip Understanding the imports - **`workflow.unsafe.imports_passed_through()`**: This context manager allows importing non-Workflow-safe modules (like Activities) into Workflow code. -- **`PaymentDetails`**: A dataclass defined in `shared.py` containing `source_account`, `target_account`, `amount`, and `reference_id` fields. ::: +The `PaymentDetails` dataclass is defined in `shared.py`: + +**shared.py** + +```python +from dataclasses import dataclass + +@dataclass +class PaymentDetails: + source_account: str + target_account: str + amount: int + reference_id: str +``` +

Activity Definition

Activities handle the business logic. Each Activity method calls an external banking service: @@ -2239,11 +2253,11 @@ The Workflow starter script starts a Workflow Execution. Each time you run it, t }> -The Temporal Web UI lets you see details about the Workflow you just ran. +The Temporal Web UI lets you see details about the Workflow you just ran. Since you're running Temporal locally with `temporal server start-dev`, the UI is available at `http://localhost:8233`. **What you'll see in the UI:** - List of Workflows with their execution status diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index c919ef01a5..6d2d97d756 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -1141,7 +1141,7 @@ Test your understanding of what you just experienced:
From d27388360d19b1e626bdd262b75d7a83e0bd71c1 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 21 Jan 2026 22:37:06 -0600 Subject: [PATCH 39/40] Update docs/build-your-first-basic-workflow/build-your-first-workflow.mdx Co-authored-by: Milecia McG <47196133+flippedcoder@users.noreply.github.com> --- .../build-your-first-workflow.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index cf3256b446..13fdffef61 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -197,7 +197,7 @@ The repository for this tutorial is a GitHub Template repository, which means yo The Temporal Application will consist of the following pieces: 1. **A Workflow** written in your programming language of choice and your installed Temporal SDK in that language. A Workflow defines the overall flow of the application. -2. **An Activity** is a function or method that does specific operation - like withdrawing money, sending an email, or calling an API. Since these operations often depend on external services that can be unreliable, Temporal automatically retries Activities when they fail. +2. **An Activity** is a function or method that does a specific operation - like withdrawing money, sending an email, or calling an API. Since these operations often depend on external services that can be unreliable, Temporal automatically retries Activities when they fail. In this application, you'll write Activities for withdraw, deposit, and refund operations. 3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. From 2a3f5926158d70228407f680f653e02d9b97bb63 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Wed, 21 Jan 2026 22:37:29 -0600 Subject: [PATCH 40/40] Update docs/build-your-first-basic-workflow/build-your-first-workflow.mdx Co-authored-by: Milecia McG <47196133+flippedcoder@users.noreply.github.com> --- .../build-your-first-workflow.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 13fdffef61..7d12c0c424 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -695,7 +695,7 @@ export async function moneyTransfer(details: PaymentDetails): Promise { throw new ApplicationFailure(`Withdrawal failed. Error: ${withdrawErr}`); } - //Execute the deposit Activity + // Execute the deposit Activity let depositResult: string; try { // highlight-next-line
diff --git a/sidebars.js b/sidebars.js index bc70efd416..c4367532ea 100644 --- a/sidebars.js +++ b/sidebars.js @@ -9,20 +9,19 @@ module.exports = { type: "doc", id: "quickstarts", }, + items: [], + }, + { + type: "category", + label: "Build your First Basic Workflow", + collapsed: false, + link: { + type: "doc", + id: "build-your-first-basic-workflow/index", + }, items: [ - { - type: "category", - label: "Build your First Basic Workflow", - collapsed: false, - link: { - type: "doc", - id: "build-your-first-basic-workflow", - }, - items: [ - "build-your-first-basic-workflow/python", - "build-your-first-basic-workflow/python-failure-simulation", - ], - }, + "build-your-first-basic-workflow/python", + "build-your-first-basic-workflow/python-failure-simulation", ], }, { diff --git a/src/components/CodeComparison/CodeComparison.js b/src/components/CodeComparison/CodeComparison.js new file mode 100644 index 0000000000..ca2ea675fb --- /dev/null +++ b/src/components/CodeComparison/CodeComparison.js @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; +import styles from './CodeComparison.module.css'; + +export const CodeComparison = ({ brokenLabel = "BROKEN CODE", fixedLabel = "FIXED CODE" }) => { + const [activeTab, setActiveTab] = useState('fixed'); + + return ( +
+ + +
+ ); +}; diff --git a/src/components/CodeComparison/CodeComparison.module.css b/src/components/CodeComparison/CodeComparison.module.css new file mode 100644 index 0000000000..c6ea3a40d3 --- /dev/null +++ b/src/components/CodeComparison/CodeComparison.module.css @@ -0,0 +1,25 @@ +.codeComparison { + display: flex; + gap: 1rem; + margin: 1.5rem 0; +} + +.codeToggle { + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + transition: all 0.3s ease; +} + +.codeToggle:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(68, 76, 231, 0.3); +} + +.codeToggle.active { + background: linear-gradient(135deg, #10B981, #059669); +} \ No newline at end of file diff --git a/src/components/CodeComparison/index.js b/src/components/CodeComparison/index.js new file mode 100644 index 0000000000..d05af4d8b3 --- /dev/null +++ b/src/components/CodeComparison/index.js @@ -0,0 +1 @@ +export { CodeComparison } from './CodeComparison'; diff --git a/src/components/LanguageSelector/LanguageSelector.js b/src/components/LanguageSelector/LanguageSelector.js new file mode 100644 index 0000000000..092d802d57 --- /dev/null +++ b/src/components/LanguageSelector/LanguageSelector.js @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import styles from './LanguageSelector.module.css'; + +export const LanguageSelector = () => { + const [hoveredLanguage, setHoveredLanguage] = useState(null); + + const languages = [ + { + name: 'Python', + icon: '/img/sdks/sdk-box-logos/python.svg', + href: '/build-your-first-basic-workflow/python' + }, + { + name: 'Go', + icon: '/img/sdks/sdk-box-logos/go.svg', + href: 'https://learn.temporal.io/getting_started/go/first_program_in_go/' + }, + { + name: 'Java', + icon: '/img/sdks/sdk-box-logos/java.svg', + href: 'https://learn.temporal.io/getting_started/java/first_program_in_java/' + }, + { + name: 'TypeScript', + icon: '/img/sdks/sdk-box-logos/typescript.svg', + href: 'https://learn.temporal.io/getting_started/typescript/first_program_in_typescript/' + }, + { + name: '.NET', + icon: '/img/sdks/sdk-box-logos/dotnet.svg', + href: 'https://learn.temporal.io/getting_started/dotnet/first_program_in_dotnet/' + }, + { + name: 'PHP', + icon: '/img/sdks/sdk-box-logos/php.svg', + href: 'https://learn.temporal.io/getting_started/php/first_program_in_php/' + }, + { + name: 'Ruby', + icon: '/img/sdks/sdk-box-logos/ruby.svg', + href: 'https://learn.temporal.io/getting_started/ruby/first_program_in_ruby/' + } + ]; + + return ( +
+ ); +}; diff --git a/src/components/LanguageSelector/LanguageSelector.module.css b/src/components/LanguageSelector/LanguageSelector.module.css new file mode 100644 index 0000000000..24a6fc8dc5 --- /dev/null +++ b/src/components/LanguageSelector/LanguageSelector.module.css @@ -0,0 +1,126 @@ +.languageSelector { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + padding: 2rem 0; + background: transparent; + margin: 2rem 0; +} + +.textContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.title { + font-size: 1.8rem; + font-weight: 400; + color: var(--ifm-color-content); + margin: 0; + line-height: 1.2; +} + +.language { + font-size: 2.2rem; + font-weight: 600; + background: linear-gradient(135deg, #444CE7, #7C3AED); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin: 0; + line-height: 1.2; + min-height: 2.6rem; + /* Prevent layout shift */ + transition: all 0.3s ease; +} + +.iconsContainer { + display: flex; + gap: 1.5rem; + align-items: center; + flex-wrap: wrap; +} + +.languageIcon { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + padding: 1rem; + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + text-decoration: none; + cursor: pointer; +} + +.languageIcon:hover, +.languageIcon.active { + background: rgba(255, 255, 255, 0.1); + border-color: #444CE7; + transform: translateY(-4px); + box-shadow: + 0 8px 32px rgba(68, 76, 231, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.icon { + width: 48px; + height: 48px; + object-fit: contain; + filter: grayscale(0.3) opacity(0.8); + transition: filter 0.3s ease; +} + +.languageIcon:hover .icon, +.languageIcon.active .icon { + filter: grayscale(0) opacity(1); +} + +/* Theme adjustments handled by CSS custom property var(--ifm-color-content) */ + +:global([data-theme='light']) .languageIcon { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(0, 0, 0, 0.1); +} + +:global([data-theme='light']) .languageIcon:hover, +:global([data-theme='light']) .languageIcon.active { + background: rgba(255, 255, 255, 1); + border-color: #444CE7; +} + +/* Responsive design */ +@media (max-width: 768px) { + .languageSelector { + gap: 1.5rem; + } + + .title { + font-size: 1.4rem; + } + + .language { + font-size: 1.8rem; + } + + .iconsContainer { + gap: 1rem; + justify-content: center; + } + + .languageIcon { + width: 64px; + height: 64px; + padding: 0.75rem; + } + + .icon { + width: 32px; + height: 32px; + } +} \ No newline at end of file diff --git a/src/components/LanguageSelector/index.js b/src/components/LanguageSelector/index.js new file mode 100644 index 0000000000..493cac82a0 --- /dev/null +++ b/src/components/LanguageSelector/index.js @@ -0,0 +1 @@ +export { LanguageSelector } from './LanguageSelector'; diff --git a/src/components/MissionStatus/MissionStatus.js b/src/components/MissionStatus/MissionStatus.js new file mode 100644 index 0000000000..548c44b1b4 --- /dev/null +++ b/src/components/MissionStatus/MissionStatus.js @@ -0,0 +1,17 @@ +import React from 'react'; +import styles from './MissionStatus.module.css'; + +export const MissionStatus = ({ items }) => { + return ( +
+ {items.map((item) => ( +
+
+ {item.number} +
+ {item.label} +
+ ))} +
+ ); +}; diff --git a/src/components/MissionStatus/MissionStatus.module.css b/src/components/MissionStatus/MissionStatus.module.css new file mode 100644 index 0000000000..e4faea59a7 --- /dev/null +++ b/src/components/MissionStatus/MissionStatus.module.css @@ -0,0 +1,58 @@ +.missionStatus { + background: linear-gradient(135deg, #1F203F 0%, #444CE7 100%); + border: 1px solid rgba(68, 76, 231, 0.3); + border-radius: 12px; + padding: 1.5rem; + margin: 1.5rem 0; + color: white; +} + +.statusCard { + display: flex; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.statusCard:last-child { + border-bottom: none; +} + +.statusIcon { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + font-size: 12px; + font-weight: bold; +} + +.statusIcon.success { + background: linear-gradient(135deg, #10B981, #059669); +} + +.statusIcon.active { + background: linear-gradient(135deg, #F59E0B, #D97706); + animation: pulse 2s infinite; +} + +.statusIcon.pending { + background: rgba(255, 255, 255, 0.2); +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); + } + + 70% { + box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); + } +} \ No newline at end of file diff --git a/src/components/MissionStatus/index.js b/src/components/MissionStatus/index.js new file mode 100644 index 0000000000..82b886097e --- /dev/null +++ b/src/components/MissionStatus/index.js @@ -0,0 +1 @@ +export { MissionStatus } from './MissionStatus'; diff --git a/src/components/RetryCounter/RetryCounter.js b/src/components/RetryCounter/RetryCounter.js new file mode 100644 index 0000000000..4c1c734a9f --- /dev/null +++ b/src/components/RetryCounter/RetryCounter.js @@ -0,0 +1,22 @@ +import React from 'react'; +import styles from './RetryCounter.module.css'; + +export const RetryCounter = ({ title, attempt, maxAttempts, nextRetryIn }) => { + const progress = (attempt / maxAttempts) * 100; + + return ( +
+ {title} +
Attempt {attempt} of {maxAttempts}
+
+
+
+ {nextRetryIn && ( +
Next retry in {nextRetryIn}
+ )} +
+ ); +}; diff --git a/src/components/RetryCounter/RetryCounter.module.css b/src/components/RetryCounter/RetryCounter.module.css new file mode 100644 index 0000000000..3529d8cab1 --- /dev/null +++ b/src/components/RetryCounter/RetryCounter.module.css @@ -0,0 +1,28 @@ +.retryCounter { + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + border-radius: 8px; + padding: 1rem; + margin: 1rem 0; + color: white; + font-family: 'Monaco', 'Consolas', monospace; +} + +.retryBar { + background: rgba(255, 255, 255, 0.2); + height: 8px; + border-radius: 4px; + overflow: hidden; + margin: 0.5rem 0; +} + +.retryProgress { + background: linear-gradient(90deg, #10B981, #059669); + height: 100%; + border-radius: 4px; + transition: width 0.5s ease; +} + +.nextRetry { + font-size: 0.8rem; + opacity: 0.8; +} \ No newline at end of file diff --git a/src/components/RetryCounter/index.js b/src/components/RetryCounter/index.js new file mode 100644 index 0000000000..b63d8638ad --- /dev/null +++ b/src/components/RetryCounter/index.js @@ -0,0 +1 @@ +export { RetryCounter } from './RetryCounter'; diff --git a/src/components/StatusIndicators/StatusIndicators.js b/src/components/StatusIndicators/StatusIndicators.js new file mode 100644 index 0000000000..c49b832f23 --- /dev/null +++ b/src/components/StatusIndicators/StatusIndicators.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './StatusIndicators.module.css'; + +export const StatusIndicators = ({ items }) => { + return ( +
+ {items.map((item) => ( +
+
+ {item.label}: {item.value} +
+ ))} +
+ ); +}; diff --git a/src/components/StatusIndicators/StatusIndicators.module.css b/src/components/StatusIndicators/StatusIndicators.module.css new file mode 100644 index 0000000000..16122636d9 --- /dev/null +++ b/src/components/StatusIndicators/StatusIndicators.module.css @@ -0,0 +1,71 @@ +.statusIndicators { + background: linear-gradient(135deg, #1F203F 0%, #312E81 100%); + border-radius: 12px; + padding: 1.5rem; + margin: 1.5rem 0; +} + +.statusRow { + display: flex; + align-items: center; + margin: 0.75rem 0; + color: white; +} + +.statusDot { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 0.75rem; + flex-shrink: 0; +} + +.statusDot.running { + background: #10B981; + animation: pulseGreen 2s infinite; +} + +.statusDot.crashed { + background: #EF4444; +} + +.statusDot.retrying { + background: #F59E0B; + animation: pulseYellow 2s infinite; +} + +.statusDot.pending { + background: rgba(255, 255, 255, 0.3); +} + +.statusDot.success { + background: #10B981; +} + +@keyframes pulseGreen { + 0% { + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + } + + 70% { + box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); + } +} + +@keyframes pulseYellow { + 0% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); + } + + 70% { + box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); + } +} \ No newline at end of file diff --git a/src/components/StatusIndicators/index.js b/src/components/StatusIndicators/index.js new file mode 100644 index 0000000000..5054478a0e --- /dev/null +++ b/src/components/StatusIndicators/index.js @@ -0,0 +1 @@ +export { StatusIndicators } from './StatusIndicators'; diff --git a/src/components/TemporalCheckbox/TemporalCheckbox.js b/src/components/TemporalCheckbox/TemporalCheckbox.js new file mode 100644 index 0000000000..bf35d1479d --- /dev/null +++ b/src/components/TemporalCheckbox/TemporalCheckbox.js @@ -0,0 +1,19 @@ +import React from 'react'; +import styles from './TemporalCheckbox.module.css'; +import { CheckIcon } from '../icons'; + +export const TemporalCheckbox = ({ id, children, defaultChecked = false }) => { + return ( +
+ +
+ +
+ +
+ ); +}; diff --git a/src/components/TemporalCheckbox/TemporalCheckbox.module.css b/src/components/TemporalCheckbox/TemporalCheckbox.module.css new file mode 100644 index 0000000000..5195c95883 --- /dev/null +++ b/src/components/TemporalCheckbox/TemporalCheckbox.module.css @@ -0,0 +1,39 @@ +.temporalCheckbox { + display: flex; + align-items: center; + margin: 0.5rem 0; + cursor: pointer; +} + +.temporalCheckbox input[type="checkbox"] { + appearance: none; + width: 20px; + height: 20px; + border: 2px solid #444CE7; + border-radius: 4px; + margin-right: 0.75rem; + position: relative; + cursor: pointer; +} + +.temporalCheckbox input[type="checkbox"]:checked { + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); +} + +.checkboxIcon { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + opacity: 0; + transition: opacity 0.2s ease; +} + +.temporalCheckbox input[type="checkbox"]:checked + .checkboxIcon { + opacity: 1; +} + +.temporalCheckbox label { + font-weight: 500; + cursor: pointer; +} diff --git a/src/components/TemporalCheckbox/index.js b/src/components/TemporalCheckbox/index.js new file mode 100644 index 0000000000..5ebe2c657a --- /dev/null +++ b/src/components/TemporalCheckbox/index.js @@ -0,0 +1 @@ +export { TemporalCheckbox } from './TemporalCheckbox'; diff --git a/src/components/TemporalProgress/TemporalProgress.js b/src/components/TemporalProgress/TemporalProgress.js new file mode 100644 index 0000000000..10ab921253 --- /dev/null +++ b/src/components/TemporalProgress/TemporalProgress.js @@ -0,0 +1,19 @@ +import React from 'react'; +import styles from './TemporalProgress.module.css'; + +export const TemporalProgress = ({ steps }) => { + return ( +
+ {steps.map((step, index) => ( + +
+ {step.label} +
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+ ); +}; diff --git a/src/components/TemporalProgress/TemporalProgress.module.css b/src/components/TemporalProgress/TemporalProgress.module.css new file mode 100644 index 0000000000..19799fcefb --- /dev/null +++ b/src/components/TemporalProgress/TemporalProgress.module.css @@ -0,0 +1,43 @@ +.temporalProgress { + display: flex; + align-items: center; + margin: 2rem 0; + padding: 1.5rem; + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + border-radius: 12px; + color: white; +} + +.progressStep { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + font-size: 0.9rem; + min-width: 200px; + text-align: center; +} + +.progressStep.completed { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.progressStep.active { + background: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); +} + +.progressStep.pending { + background: rgba(255, 255, 255, 0.1); + opacity: 0.6; +} + +.progressConnector { + flex: 1; + height: 2px; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); + margin: 0 1rem; +} \ No newline at end of file diff --git a/src/components/TemporalProgress/index.js b/src/components/TemporalProgress/index.js new file mode 100644 index 0000000000..1851c1d8f1 --- /dev/null +++ b/src/components/TemporalProgress/index.js @@ -0,0 +1 @@ +export { TemporalProgress } from './TemporalProgress'; diff --git a/src/components/WorkflowDiagram/WorkflowDiagram.js b/src/components/WorkflowDiagram/WorkflowDiagram.js new file mode 100644 index 0000000000..babd0a868c --- /dev/null +++ b/src/components/WorkflowDiagram/WorkflowDiagram.js @@ -0,0 +1,22 @@ +import React from 'react'; +import styles from './WorkflowDiagram.module.css'; + +export const WorkflowDiagram = ({ title, nodes }) => { + return ( +
+

{title}

+
+ {nodes.map((node, index) => ( + +
+ {node.label} +
+ {index < nodes.length - 1 && ( +
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src/components/WorkflowDiagram/WorkflowDiagram.module.css b/src/components/WorkflowDiagram/WorkflowDiagram.module.css new file mode 100644 index 0000000000..7b37c435da --- /dev/null +++ b/src/components/WorkflowDiagram/WorkflowDiagram.module.css @@ -0,0 +1,41 @@ +.workflowDiagram { + background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%); + border-radius: 12px; + padding: 2rem; + margin: 2rem 0; + text-align: center; + color: white; + font-family: 'Monaco', 'Consolas', monospace; +} + +.diagramFlow { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; + margin: 1rem 0; +} + +.diagramNode { + padding: 0.5rem 1rem; + background: rgba(68, 76, 231, 0.3); + border: 2px solid rgba(68, 76, 231, 0.5); + border-radius: 8px; + font-weight: 600; +} + +.diagramNode.crashed { + background: rgba(239, 68, 68, 0.3); + border-color: rgba(239, 68, 68, 0.5); +} + +.diagramNode.recovered { + background: rgba(16, 185, 129, 0.3); + border-color: rgba(16, 185, 129, 0.5); +} + +.diagramArrow { + font-size: 1.5rem; + color: rgba(255, 255, 255, 0.7); +} \ No newline at end of file diff --git a/src/components/WorkflowDiagram/index.js b/src/components/WorkflowDiagram/index.js new file mode 100644 index 0000000000..2a92275154 --- /dev/null +++ b/src/components/WorkflowDiagram/index.js @@ -0,0 +1 @@ +export { WorkflowDiagram } from './WorkflowDiagram'; diff --git a/src/components/icons/CheckIcon.js b/src/components/icons/CheckIcon.js new file mode 100644 index 0000000000..288c37aae5 --- /dev/null +++ b/src/components/icons/CheckIcon.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export const CheckIcon = ({ size = 16, color = '#065f46' }) => ( + + + +); diff --git a/src/components/icons/SearchIcon.js b/src/components/icons/SearchIcon.js new file mode 100644 index 0000000000..5c82a5a2e9 --- /dev/null +++ b/src/components/icons/SearchIcon.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export const SearchIcon = ({ size = 16, color = '#065f46' }) => ( + + + +); diff --git a/src/components/icons/index.js b/src/components/icons/index.js new file mode 100644 index 0000000000..8a29069645 --- /dev/null +++ b/src/components/icons/index.js @@ -0,0 +1,2 @@ +export { CheckIcon } from './CheckIcon'; +export { SearchIcon } from './SearchIcon'; diff --git a/static/img/sdks/sdk-box-logos/dotnet.svg b/static/img/sdks/sdk-box-logos/dotnet.svg new file mode 100644 index 0000000000..5116220593 --- /dev/null +++ b/static/img/sdks/sdk-box-logos/dotnet.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/static/img/sdks/sdk-box-logos/go.svg b/static/img/sdks/sdk-box-logos/go.svg new file mode 100644 index 0000000000..fdefe31b6d --- /dev/null +++ b/static/img/sdks/sdk-box-logos/go.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/img/sdks/sdk-box-logos/java.svg b/static/img/sdks/sdk-box-logos/java.svg new file mode 100644 index 0000000000..62d4525175 --- /dev/null +++ b/static/img/sdks/sdk-box-logos/java.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/img/sdks/sdk-box-logos/php.svg b/static/img/sdks/sdk-box-logos/php.svg new file mode 100644 index 0000000000..4a2c2e7d91 --- /dev/null +++ b/static/img/sdks/sdk-box-logos/php.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/img/sdks/sdk-box-logos/python.svg b/static/img/sdks/sdk-box-logos/python.svg new file mode 100644 index 0000000000..2d59a0af4c --- /dev/null +++ b/static/img/sdks/sdk-box-logos/python.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/img/sdks/sdk-box-logos/ruby.png b/static/img/sdks/sdk-box-logos/ruby.png new file mode 100644 index 0000000000000000000000000000000000000000..af1dc434d24b58d5c4c25e65a5622e54c304b3a1 GIT binary patch literal 2993 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 P7B&$~U01CNY7_n+7n-%l literal 0 HcmV?d00001 diff --git a/static/img/sdks/sdk-box-logos/ruby.svg b/static/img/sdks/sdk-box-logos/ruby.svg new file mode 100644 index 0000000000..b6b8d205db --- /dev/null +++ b/static/img/sdks/sdk-box-logos/ruby.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/static/img/sdks/sdk-box-logos/typescript.svg b/static/img/sdks/sdk-box-logos/typescript.svg new file mode 100644 index 0000000000..31c98dd6f5 --- /dev/null +++ b/static/img/sdks/sdk-box-logos/typescript.svg @@ -0,0 +1,4 @@ + + + + From eab2cb43e05781fc2573d564866461be5d46a2b3 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 13:00:11 -0600 Subject: [PATCH 04/40] language tabs --- .../python-failure-simulation.mdx | 70 ++-- .../python.mdx | 302 ++++++++++++------ src/components/MissionStatus/MissionStatus.js | 17 - .../MissionStatus/MissionStatus.module.css | 58 ---- src/components/MissionStatus/index.js | 1 - .../TemporalCheckbox.module.css | 38 ++- .../TutorialNavigation/TutorialNavigation.js | 38 +++ .../TutorialNavigation.module.css | 147 +++++++++ src/components/TutorialNavigation/index.js | 1 + 9 files changed, 437 insertions(+), 235 deletions(-) delete mode 100644 src/components/MissionStatus/MissionStatus.js delete mode 100644 src/components/MissionStatus/MissionStatus.module.css delete mode 100644 src/components/MissionStatus/index.js create mode 100644 src/components/TutorialNavigation/TutorialNavigation.js create mode 100644 src/components/TutorialNavigation/TutorialNavigation.module.css create mode 100644 src/components/TutorialNavigation/index.js diff --git a/docs/build-your-first-basic-workflow/python-failure-simulation.mdx b/docs/build-your-first-basic-workflow/python-failure-simulation.mdx index 54740a2457..d2f28c5d70 100644 --- a/docs/build-your-first-basic-workflow/python-failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/python-failure-simulation.mdx @@ -1,8 +1,9 @@ --- id: python-failure-simulation title: Simulate Failures with Temporal Python SDK -sidebar_label: Failure Simulation +sidebar_label: "Part 2: Failure Simulation" description: Learn how Temporal handles failures, recovers from crashes, and enables live debugging of your Python workflows. +hide_table_of_contents: true keywords: - temporal - python @@ -19,50 +20,46 @@ tags: import { CallToAction } from "@site/src/components/elements/CallToAction"; import { TemporalProgress } from "@site/src/components/TemporalProgress"; -import { MissionStatus } from "@site/src/components/MissionStatus"; + import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { WorkflowDiagram } from "@site/src/components/WorkflowDiagram"; import { RetryCounter } from "@site/src/components/RetryCounter"; import { TemporalCheckbox } from "@site/src/components/TemporalCheckbox"; import { CodeComparison } from "@site/src/components/CodeComparison"; -# Simulate Failures with Temporal Python SDK +# Part 2: Simulate Failures -**Your mission**: Break things on purpose and watch Temporal work its magic. -You're about to discover why developers sleep better at night knowing Temporal is handling their critical processes. - +**Your mission**: Break things on purpose and watch Temporal work its magic. +You're about to discover why developers sleep better at night knowing Temporal is handling their critical processes. - + +**Durable execution** means your workflow's progress is saved after every step. When failures happen - server crashes, network issues, bugs in your code - Temporal resumes your workflow exactly where it stopped. No lost work, no restarting from the beginning. **What you'll accomplish**: - Crash a server mid-transaction and see zero data loss - Inject bugs into production code and fix them live - Experience reliability that makes traditional error handling look primitive -**Difficulty**: Intermediate | **Time**: 15 minutes +**Difficulty**: Intermediate Ready to break some stuff? Let's go. ## Experiment 1 of 2: Crash Recovery Test - -Unlike other solutions, Temporal is designed with failure in mind. You're about to simulate a server crash mid-transaction and watch Temporal handle it flawlessly. + +Unlike other solutions, Temporal is designed with failure in mind. +You're about to simulate a server crash mid-transaction and watch Temporal handle it flawlessly. **The Challenge**: Kill your Worker process while money is being transferred. In traditional systems, this would corrupt the transaction or lose data entirely. + + -### Pre-Flight Checklist +### Before You Start Worker is currently stopped @@ -96,7 +93,10 @@ This is fundamentally different from traditional applications where process cras -1. **Make sure your Worker is stopped** before proceeding. If the Worker is running, press `Ctrl+C` to stop it. +### Instructions + +1. **Make sure your Worker is stopped** before proceeding. +If the Worker is running, press `Ctrl+C` to stop it. 2. **Start the Worker in Terminal 2:** ```bash @@ -132,19 +132,15 @@ This is fundamentally different from traditional applications where process cras **Mission Accomplished!** You just simulated killing the Worker process and restarting it. The Workflow resumed where it left off without losing any application state. :::tip **Try This Challenge** -**Advanced Experiment**: Try killing the Worker at different points during execution. Start the Workflow, kill the Worker during the withdrawal, then restart it. Kill it during the deposit. Each time, notice how Temporal maintains perfect state consistency. +Try killing the Worker at different points during execution. Start the Workflow, kill the Worker during the withdrawal, then restart it. Kill it during the deposit. Each time, notice how Temporal maintains perfect state consistency. -**Bonus points**: Check the Web UI while the Worker is down - you'll see the Workflow is still "Running" even though no code is executing. +Check the Web UI while the Worker is down - you'll see the Workflow is still "Running" even though no code is executing. ::: ## Experiment 2 of 2: Live Bug Fixing - -**The Challenge**: Inject a bug into your production code, watch Temporal retry automatically, then fix the bug while the Workflow is still running. This is every developer's dream scenario. +**The Challenge**: Inject a bug into your production code, watch Temporal retry automatically, then fix the bug while the Workflow is still running. -### Pre-Flight Checklist +### Before You Start Worker is stopped @@ -188,6 +184,8 @@ This is like having version control for your running application state. +### Instructions + 1. **Make sure your Worker is stopped** before proceeding. 2. **Edit the `activities.py` file** and uncomment the following line in the `deposit` method: @@ -276,11 +274,11 @@ return "Deposited money into account" **Question to consider**: How would you handle this scenario in a traditional microservices architecture? ::: -## Mission Debrief: What You Just Accomplished +## Summary: What You Accomplished -**Congratulations!** You've experienced firsthand why Temporal is a game-changer for reliable applications. Here's what you proved: +**Congratulations!** You've experienced firsthand why Temporal is a game-changer for reliable applications. Here's what you demonstrated: -### Your New Superpowers +### What You Learned
Crash-Proof Execution @@ -310,7 +308,7 @@ The Web UI gave you full visibility into every step, retry attempt, and state tr
-### Mission Complete Checklist +#### Summary Successfully recovered from a Worker crash @@ -332,15 +330,11 @@ The Web UI gave you full visibility into every step, retry attempt, and state tr Experienced zero data loss through failures - -## Advanced Training Missions -Ready for more chaos engineering? Try these advanced scenarios: +## Advanced Challenges + +Try these advanced scenarios: :::tip **Mission: Compensating Transactions** 1. **Modify the retry policy** in `workflows.py` to only retry 1 time diff --git a/docs/build-your-first-basic-workflow/python.mdx b/docs/build-your-first-basic-workflow/python.mdx index bed51fa889..3db68706bc 100644 --- a/docs/build-your-first-basic-workflow/python.mdx +++ b/docs/build-your-first-basic-workflow/python.mdx @@ -1,7 +1,7 @@ --- id: python title: Run your first Temporal application with the Python SDK -sidebar_label: Python +sidebar_label: "Part 1: Python" description: Learn Temporal's core concepts by building a money transfer Workflow with the Python SDK. Experience reliability, failure handling, and live debugging in a 10-minute tutorial. keywords: - temporal @@ -20,9 +20,10 @@ hide_table_of_contents: false import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import { CallToAction } from "@site/src/components/elements/CallToAction"; import { TemporalProgress } from "@site/src/components/TemporalProgress"; -import { MissionStatus } from "@site/src/components/MissionStatus"; import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { SearchIcon } from "@site/src/components/icons"; +import { NextButton } from "@site/src/components/TutorialNavigation"; +import SdkTabs from "@site/src/components/elements/SdkTabs"; You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process. @@ -118,20 +119,34 @@ The Temporal Application will consist of the following pieces: 2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. 3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. + + - - :::important None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. ::: -## Run a Money Transfer Flow -### Step 1: Workflow Definition +## Explore the Application's Workflow and Activity Definitions + + + + +In the money transfer application, you have: +- **Workflow**: Orchestrates the overall money transfer process +- **Activities**: Handle individual tasks like `withdraw()`, `deposit()`, and `refund()` +- **Worker**: Executes your Workflow and Activities + +![High-level project design](/img/moneytransfer/money-withdrawal.png) + +

Workflow Definition

A Workflow Definition in Python uses the `@workflow.defn` decorator on the Workflow class to identify a Workflow. +This is what the Workflow Definition looks like for this kind of process: + **workflows.py** + ```python from datetime import timedelta from temporalio import workflow @@ -145,7 +160,7 @@ with workflow.unsafe.imports_passed_through(): @workflow.defn class MoneyTransfer: @workflow.run - async def run(self, payment_details: PaymentDetails): + async def run(self, payment_details: PaymentDetails) -> str: retry_policy = RetryPolicy( maximum_attempts=3, maximum_interval=timedelta(seconds=2), @@ -169,11 +184,11 @@ class MoneyTransfer: retry_policy=retry_policy, ) - result = "Transfer complete (transaction IDs: " + str(withdraw_output) + ", " + str(deposit_output) + ")" + result = f"Transfer complete (transaction IDs: {withdraw_output}, {deposit_output})" return result except ActivityError as deposit_err: # Handle deposit error - workflow.logger.error("Deposit failed: " + str(deposit_err)) + workflow.logger.error(f"Deposit failed: {deposit_err}") # Attempt to refund try: refund_output = await workflow.execute_activity_method( @@ -182,118 +197,182 @@ class MoneyTransfer: start_to_close_timeout=timedelta(seconds=5), retry_policy=retry_policy, ) - workflow.logger.info("Refund successful. Confirmation ID: " + str(refund_output)) + workflow.logger.info( + f"Refund successful. Confirmation ID: {refund_output}" + ) raise deposit_err except ActivityError as refund_error: - workflow.logger.error("Refund failed: " + str(refund_error)) + workflow.logger.error(f"Refund failed: {refund_error}") raise refund_error ``` -The `MoneyTransfer` class takes in transaction details. It executes Activities to withdraw and deposit the money. It also returns the results of the process. - -The asynchronous `run` method signature includes an `input` variable typed as `PaymentDetails`. This class stores details that the Workflow uses to perform the money transfer. - -### Step 2: Activity Definition - -Each Activity method simulates calling an external service. -Each method can fail or succeed independently. - -In the money transfer application, you have three Activity methods: `withdraw()`, `deposit()`, and `refund()`. These symbolize the movement of funds between accounts. +

Activity Definition

+Activities handle the business logic. Each activity method calls an external banking service: **activities.py** + ```python import asyncio -from dataclasses import dataclass from temporalio import activity from shared import PaymentDetails -@dataclass class BankingActivities: @activity.defn - @staticmethod - async def withdraw(data: PaymentDetails): - print("Withdrawing money from account") - - # Simulate time to call other services that may fail - await asyncio.sleep(1) - - return "Withdrew money from account" - - @activity.defn - @staticmethod - async def deposit(data: PaymentDetails): - print("Depositing money into account") - - # Simulate time to call other services that may fail - await asyncio.sleep(1) - - # Comment/uncomment the next line to simulate failures. - # raise Exception("This deposit has failed.") - - return "Deposited money into account" - - @activity.defn - @staticmethod - async def refund(data: PaymentDetails): - print("Refunding money back to account") - - # Simulate time to call other services that may fail - await asyncio.sleep(1) - - return "Refunded money back to account" + async def withdraw(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-withdrawal" + try: + confirmation = await asyncio.to_thread( + self.bank.withdraw, data.source_account, data.amount, reference_id + ) + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Withdrawal failed") + raise ``` -**shared.py** -```python -from dataclasses import dataclass - -MONEY_TRANSFER_TASK_QUEUE_NAME = "money-transfer" +
+ + + +In the money transfer application, you have: +- **Workflow**: Orchestrates the overall money transfer process using C# +- **Activities**: Handle individual tasks like `WithdrawAsync()`, `DepositAsync()`, and `RefundAsync()` +- **Worker**: Executes your Workflow and Activities + +![High-level project design](/img/moneytransfer/money-withdrawal.png) + +

Workflow Definition

+ +In the Temporal .NET SDK, a Workflow Definition is marked by the `[Workflow]` attribute placed above the class. + +This is what the Workflow Definition looks like for this process: + +**MoneyTransferWorker/Workflow.cs** + +```csharp +namespace Temporalio.MoneyTransferProject.MoneyTransferWorker; +using Temporalio.MoneyTransferProject.BankingService.Exceptions; +using Temporalio.Workflows; +using Temporalio.Common; +using Temporalio.Exceptions; + +[Workflow] +public class MoneyTransferWorkflow +{ + [WorkflowRun] + public async Task RunAsync(PaymentDetails details) + { + // Retry policy + var retryPolicy = new RetryPolicy + { + InitialInterval = TimeSpan.FromSeconds(1), + MaximumInterval = TimeSpan.FromSeconds(100), + BackoffCoefficient = 2, + MaximumAttempts = 3, + NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } + }; + + string withdrawResult; + try + { + withdrawResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.WithdrawAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + } + catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException") + { + throw new ApplicationFailureException("Withdrawal failed due to insufficient funds.", ex); + } + + string depositResult; + try + { + depositResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.DepositAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + // If everything succeeds, return transfer complete + return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})"; + } + catch (Exception depositEx) + { + try + { + // if the deposit fails, attempt to refund the withdrawal + string refundResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.RefundAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + // If refund is successful, but deposit failed + throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money returned to {details.SourceAccount}.", depositEx); + } + catch (Exception refundEx) + { + // If both deposit and refund fail + throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money could not be returned to {details.SourceAccount}. Cause: {refundEx.Message}", refundEx); + } + } + } +} +``` -@dataclass -class PaymentDetails: - source_account: str - target_account: str - amount: int - reference_id: str +

Activity Definition

+ +Activities handle the business logic. Each activity method calls an external banking service: + +**MoneyTransferWorker/Activities.cs** + +```csharp +namespace Temporalio.MoneyTransferProject.MoneyTransferWorker; +using Temporalio.Activities; +using Temporalio.Exceptions; + +public class BankingActivities +{ + [Activity] + public static async Task WithdrawAsync(PaymentDetails details) + { + var bankService = new BankingService("bank1.example.com"); + Console.WriteLine($"Withdrawing ${details.Amount} from account {details.SourceAccount}."); + try + { + return await bankService.WithdrawAsync(details.SourceAccount, details.Amount, details.ReferenceId).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Withdrawal failed", ex); + } + } +} ``` -The Activities are defined as static methods with the `@activity.defn` decorator. +
-Note the `PaymentDetails` type, defined in `shared.py`. This contains the transaction information passed between the Workflow and Activities. +
-### Step 3: Start the Worker +## Set the Retry Policy -The Worker hosts the Workflow and Activity functions and executes them one at a time. +Temporal makes your software durable and fault tolerant by default. If an Activity fails, Temporal automatically retries it, but you can customize this behavior through a Retry Policy. -The Worker is configured to execute Workflows and Activities from the Task Queue. Since the Worker and Workflow are both configured to use the same Task Queue, the Worker will execute any Workflows and Activities sent to the Task Queue. +In the `MoneyTransfer` Workflow, you'll see a Retry Policy that retries failed Activities up to 3 times, with specific errors that shouldn't be retried: -**run_worker.py** ```python -import asyncio -from temporalio.client import Client -from temporalio.worker import Worker -from activities import BankingActivities -from shared import MONEY_TRANSFER_TASK_QUEUE_NAME -from workflows import MoneyTransfer - -async def main(): - client = await Client.connect("localhost:7233") - - worker = Worker( - client, - task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, - workflows=[MoneyTransfer], - activities=[BankingActivities.withdraw, BankingActivities.deposit, BankingActivities.refund], - ) - - await worker.run() - -if __name__ == "__main__": - asyncio.run(main()) +retry_policy = RetryPolicy( + maximum_attempts=3, + maximum_interval=timedelta(seconds=2), + non_retryable_error_types=["InvalidAccountError", "InsufficientFundsError"], +) ``` -When you start the Worker, it begins polling the Temporal Server for work. +:::important This is a Simplified Example +This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling. +::: + +## Run Your Money Transfer @@ -327,8 +406,6 @@ Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. R
}> -### Run Your Application - Now that your Worker is running and polling for tasks, you can start a Workflow execution. **In Terminal 3, start the Workflow:** @@ -344,13 +421,23 @@ The `run_workflow.py` script starts a Workflow Execution. Each time you run this - -}> + + +
+ +
+

Python Environment Setup

+

Run through the Quickstart to get your set up complete.

+
+
+ +
+ + ## View the state of the Workflow with the Temporal Web UI @@ -366,15 +453,20 @@ The Temporal Web UI lets you see details about the Workflow you just ran. **Try This:** Click on a Workflow in the list to see all the details of the Workflow Execution. -
-
Money Transfer Web UI
- +## Ready for Part 2? + + + Continue to Part 2: Simulate Failures + ## Continue Your Learning diff --git a/src/components/MissionStatus/MissionStatus.js b/src/components/MissionStatus/MissionStatus.js deleted file mode 100644 index 548c44b1b4..0000000000 --- a/src/components/MissionStatus/MissionStatus.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import styles from './MissionStatus.module.css'; - -export const MissionStatus = ({ items }) => { - return ( -
- {items.map((item) => ( -
-
- {item.number} -
- {item.label} -
- ))} -
- ); -}; diff --git a/src/components/MissionStatus/MissionStatus.module.css b/src/components/MissionStatus/MissionStatus.module.css deleted file mode 100644 index e4faea59a7..0000000000 --- a/src/components/MissionStatus/MissionStatus.module.css +++ /dev/null @@ -1,58 +0,0 @@ -.missionStatus { - background: linear-gradient(135deg, #1F203F 0%, #444CE7 100%); - border: 1px solid rgba(68, 76, 231, 0.3); - border-radius: 12px; - padding: 1.5rem; - margin: 1.5rem 0; - color: white; -} - -.statusCard { - display: flex; - align-items: center; - padding: 0.75rem 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.statusCard:last-child { - border-bottom: none; -} - -.statusIcon { - width: 24px; - height: 24px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; - font-size: 12px; - font-weight: bold; -} - -.statusIcon.success { - background: linear-gradient(135deg, #10B981, #059669); -} - -.statusIcon.active { - background: linear-gradient(135deg, #F59E0B, #D97706); - animation: pulse 2s infinite; -} - -.statusIcon.pending { - background: rgba(255, 255, 255, 0.2); -} - -@keyframes pulse { - 0% { - box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); - } - - 70% { - box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); - } - - 100% { - box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); - } -} \ No newline at end of file diff --git a/src/components/MissionStatus/index.js b/src/components/MissionStatus/index.js deleted file mode 100644 index 82b886097e..0000000000 --- a/src/components/MissionStatus/index.js +++ /dev/null @@ -1 +0,0 @@ -export { MissionStatus } from './MissionStatus'; diff --git a/src/components/TemporalCheckbox/TemporalCheckbox.module.css b/src/components/TemporalCheckbox/TemporalCheckbox.module.css index 5195c95883..0b01588b0e 100644 --- a/src/components/TemporalCheckbox/TemporalCheckbox.module.css +++ b/src/components/TemporalCheckbox/TemporalCheckbox.module.css @@ -1,19 +1,23 @@ .temporalCheckbox { - display: flex; - align-items: center; - margin: 0.5rem 0; - cursor: pointer; + display: flex; + align-items: flex-start; + margin: 0.5rem 0; + cursor: pointer; + gap: 0.75rem; } .temporalCheckbox input[type="checkbox"] { - appearance: none; - width: 20px; - height: 20px; - border: 2px solid #444CE7; - border-radius: 4px; - margin-right: 0.75rem; - position: relative; - cursor: pointer; + appearance: none; + width: 20px; + height: 20px; + border: 2px solid #444CE7; + border-radius: 4px; + position: relative; + cursor: pointer; + margin: 0; + flex-shrink: 0; + margin-top: 2px; + /* Align with first line of text */ } .temporalCheckbox input[type="checkbox"]:checked { @@ -29,11 +33,13 @@ transition: opacity 0.2s ease; } -.temporalCheckbox input[type="checkbox"]:checked + .checkboxIcon { +.temporalCheckbox input[type="checkbox"]:checked+.checkboxIcon { opacity: 1; } .temporalCheckbox label { - font-weight: 500; - cursor: pointer; -} + font-weight: 500; + cursor: pointer; + line-height: 1.5; + margin: 0; +} \ No newline at end of file diff --git a/src/components/TutorialNavigation/TutorialNavigation.js b/src/components/TutorialNavigation/TutorialNavigation.js new file mode 100644 index 0000000000..4f1628503c --- /dev/null +++ b/src/components/TutorialNavigation/TutorialNavigation.js @@ -0,0 +1,38 @@ +import React from 'react'; +import styles from './TutorialNavigation.module.css'; + +export const BackButton = ({ href, children }) => ( + + + + + {children} + +); + +export const NextButton = ({ href, children, description }) => ( + +
+
{children}
+ {description &&
{description}
} +
+ + + +
+); + +export const TutorialProgress = ({ currentStep, totalSteps, stepTitle }) => ( +
+
+ Part {currentStep} of {totalSteps} + {stepTitle} +
+
+
+
+
+); diff --git a/src/components/TutorialNavigation/TutorialNavigation.module.css b/src/components/TutorialNavigation/TutorialNavigation.module.css new file mode 100644 index 0000000000..2ce020246f --- /dev/null +++ b/src/components/TutorialNavigation/TutorialNavigation.module.css @@ -0,0 +1,147 @@ +.backButton { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: transparent; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + color: var(--ifm-color-content); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.2s ease; + margin-bottom: 2rem; +} + +.backButton:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + text-decoration: none; + transform: translateX(-2px); +} + +.backIcon { + width: 16px; + height: 16px; +} + +.nextButton { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 2rem; + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + border-radius: 12px; + color: white; + text-decoration: none; + margin: 2rem 0; + transition: all 0.3s ease; + box-shadow: 0 4px 16px rgba(68, 76, 231, 0.2); +} + +.nextButton:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(68, 76, 231, 0.3); + text-decoration: none; + color: white; +} + +.nextContent { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.nextTitle { + font-size: 1.2rem; + font-weight: 600; + margin: 0; +} + +.nextDescription { + font-size: 0.9rem; + opacity: 0.9; + margin: 0; +} + +.nextIcon { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.tutorialProgress { + background: var(--ifm-card-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + padding: 1rem; + margin: 2rem 0; +} + +.progressHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.stepIndicator { + font-size: 0.8rem; + color: var(--ifm-color-content-secondary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stepTitle { + font-size: 1rem; + font-weight: 500; + color: var(--ifm-color-content); +} + +.progressBar { + width: 100%; + height: 4px; + background: var(--ifm-color-emphasis-200); + border-radius: 2px; + overflow: hidden; +} + +.progressFill { + height: 100%; + background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); + border-radius: 2px; + transition: width 0.5s ease; +} + +/* Dark mode adjustments */ +:global([data-theme='dark']) .backButton { + border-color: var(--ifm-color-emphasis-400); +} + +:global([data-theme='dark']) .backButton:hover { + background: var(--ifm-color-emphasis-200); +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .nextButton { + padding: 1rem 1.5rem; + } + + .nextTitle { + font-size: 1rem; + } + + .nextDescription { + font-size: 0.8rem; + } + + .progressHeader { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } +} \ No newline at end of file diff --git a/src/components/TutorialNavigation/index.js b/src/components/TutorialNavigation/index.js new file mode 100644 index 0000000000..159feb283b --- /dev/null +++ b/src/components/TutorialNavigation/index.js @@ -0,0 +1 @@ +export { BackButton, NextButton, TutorialProgress } from './TutorialNavigation'; From 249d92af592b953eb56a9a5f0c3673b48144b5d7 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 13:42:01 -0600 Subject: [PATCH 05/40] rename for multilanguage --- ...thon.mdx => build-your-first-workflow.mdx} | 63 ++++++------------- ...-simulation.mdx => failure-simulation.mdx} | 10 ++- sidebars.js | 4 +- 3 files changed, 25 insertions(+), 52 deletions(-) rename docs/build-your-first-basic-workflow/{python.mdx => build-your-first-workflow.mdx} (91%) rename docs/build-your-first-basic-workflow/{python-failure-simulation.mdx => failure-simulation.mdx} (99%) diff --git a/docs/build-your-first-basic-workflow/python.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx similarity index 91% rename from docs/build-your-first-basic-workflow/python.mdx rename to docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 3db68706bc..e381b81186 100644 --- a/docs/build-your-first-basic-workflow/python.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -1,7 +1,7 @@ --- -id: python +id: build-your-first-workflow title: Run your first Temporal application with the Python SDK -sidebar_label: "Part 1: Python" +sidebar_label: "Part 1: Build Your First Workflow" description: Learn Temporal's core concepts by building a money transfer Workflow with the Python SDK. Experience reliability, failure handling, and live debugging in a 10-minute tutorial. keywords: - temporal @@ -11,10 +11,8 @@ keywords: - money transfer - reliability tags: - - Python - - SDK - Getting Started -hide_table_of_contents: false + - Tutorial --- import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; @@ -28,8 +26,7 @@ import SdkTabs from "@site/src/components/elements/SdkTabs"; You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process.
- ⭐ Temporal beginner - 🐍 Python SDK + Temporal beginner
@@ -86,7 +83,6 @@ The application you'll use in this tutorial is available in a GitHub repository. Open a new terminal window and use `git` to clone the repository, then change to the project directory. - Now that you've downloaded the project, let's dive into the code. @@ -116,7 +112,7 @@ The repository for this tutorial is a GitHub Template repository, which means yo The Temporal Application will consist of the following pieces: 1. **A Workflow** written in Python using the Python SDK. A Workflow defines the overall flow of the application. -2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. +2. **An Activity** is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. They handle individual tasks like withdraw(), deposit(), and refund(). 3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. @@ -132,13 +128,6 @@ None of your application code runs on the Temporal Server. Your Worker, Workflow -In the money transfer application, you have: -- **Workflow**: Orchestrates the overall money transfer process -- **Activities**: Handle individual tasks like `withdraw()`, `deposit()`, and `refund()` -- **Worker**: Executes your Workflow and Activities - -![High-level project design](/img/moneytransfer/money-withdrawal.png) -

Workflow Definition

A Workflow Definition in Python uses the `@workflow.defn` decorator on the Workflow class to identify a Workflow. @@ -237,13 +226,6 @@ class BankingActivities: -In the money transfer application, you have: -- **Workflow**: Orchestrates the overall money transfer process using C# -- **Activities**: Handle individual tasks like `WithdrawAsync()`, `DepositAsync()`, and `RefundAsync()` -- **Worker**: Executes your Workflow and Activities - -![High-level project design](/img/moneytransfer/money-withdrawal.png) -

Workflow Definition

In the Temporal .NET SDK, a Workflow Definition is marked by the `[Workflow]` attribute placed above the class. @@ -421,27 +403,18 @@ The `run_workflow.py` script starts a Workflow Execution. Each time you run this - - -
- -
-

Python Environment Setup

-

Run through the Quickstart to get your set up complete.

-
-
- -
- - ## View the state of the Workflow with the Temporal Web UI + +}> + The Temporal Web UI lets you see details about the Workflow you just ran. **What you'll see in the UI:** @@ -453,12 +426,14 @@ The Temporal Web UI lets you see details about the Workflow you just ran. **Try This:** Click on a Workflow in the list to see all the details of the Workflow Execution. + + + +
Money Transfer Web UI
- - ## Ready for Part 2? Date: Thu, 13 Nov 2025 14:00:01 -0600 Subject: [PATCH 06/40] text edits --- .../build-your-first-workflow.mdx | 10 ++++------ .../failure-simulation.mdx | 13 +++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index e381b81186..1a9444fec3 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -123,7 +123,7 @@ The Temporal Application will consist of the following pieces: None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. ::: -## Explore the Application's Workflow and Activity Definitions +## Build your Workflow and Activities @@ -388,7 +388,7 @@ Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. R
}> -Now that your Worker is running and polling for tasks, you can start a Workflow execution. +Now that your Worker is running and polling for tasks, you can start a Workflow Execution. **In Terminal 3, start the Workflow:** @@ -403,9 +403,7 @@ The `run_workflow.py` script starts a Workflow Execution. Each time you run this - - -## View the state of the Workflow with the Temporal Web UI +## Check the Temporal Web UI Continue to Part 2: Simulate Failures diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 7a21784fe0..f006786441 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -32,16 +32,15 @@ import { CodeComparison } from "@site/src/components/CodeComparison"; { id: 'part1', label: 'Part 1: Basic Workflow', status: 'completed' }, { id: 'part2', label: 'Part 2: Failure Simulation', status: 'active' } ]} /> -**Your mission**: Break things on purpose and watch Temporal work its magic. -You're about to discover why developers sleep better at night knowing Temporal is handling their critical processes. +In this part, you'll simulate failures to see how Temporal handles them. +This demonstrates why Temporal is particularly useful for building reliable systems. - -**Durable execution** means your workflow's progress is saved after every step. When failures happen - server crashes, network issues, bugs in your code - Temporal resumes your workflow exactly where it stopped. No lost work, no restarting from the beginning. +The key concept here is **durable execution**: your workflow's progress is saved after every step. +When failures and crashes happen (network issues, bugs in your code, server restarts), Temporal resumes your workflow exactly where it stopped. No lost work, no restarting from the beginning. **What you'll accomplish**: - Crash a server mid-transaction and see zero data loss -- Inject bugs into production code and fix them live -- Experience reliability that makes traditional error handling look primitive +- Inject bugs into code and fix them live **Difficulty**: Intermediate @@ -49,8 +48,6 @@ Ready to break some stuff? Let's go. ## Experiment 1 of 2: Crash Recovery Test - - Unlike other solutions, Temporal is designed with failure in mind. You're about to simulate a server crash mid-transaction and watch Temporal handle it flawlessly. From 435aff15feb6c9532db7d2453b6a3e2f48207f55 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 14:25:55 -0600 Subject: [PATCH 07/40] component clean up --- .../build-your-first-workflow.mdx | 1 - .../TutorialNavigation/TutorialNavigation.js | 24 ----- .../TutorialNavigation.module.css | 87 ------------------- src/components/TutorialNavigation/index.js | 2 +- src/components/icons/SearchIcon.js | 16 ---- src/components/icons/index.js | 1 - 6 files changed, 1 insertion(+), 130 deletions(-) delete mode 100644 src/components/icons/SearchIcon.js diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 1a9444fec3..fa2228f418 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -19,7 +19,6 @@ import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/element import { CallToAction } from "@site/src/components/elements/CallToAction"; import { TemporalProgress } from "@site/src/components/TemporalProgress"; import { StatusIndicators } from "@site/src/components/StatusIndicators"; -import { SearchIcon } from "@site/src/components/icons"; import { NextButton } from "@site/src/components/TutorialNavigation"; import SdkTabs from "@site/src/components/elements/SdkTabs"; diff --git a/src/components/TutorialNavigation/TutorialNavigation.js b/src/components/TutorialNavigation/TutorialNavigation.js index 4f1628503c..2d8eaa3629 100644 --- a/src/components/TutorialNavigation/TutorialNavigation.js +++ b/src/components/TutorialNavigation/TutorialNavigation.js @@ -1,15 +1,6 @@ import React from 'react'; import styles from './TutorialNavigation.module.css'; -export const BackButton = ({ href, children }) => ( - - - - - {children} - -); - export const NextButton = ({ href, children, description }) => (
@@ -21,18 +12,3 @@ export const NextButton = ({ href, children, description }) => ( ); - -export const TutorialProgress = ({ currentStep, totalSteps, stepTitle }) => ( -
-
- Part {currentStep} of {totalSteps} - {stepTitle} -
-
-
-
-
-); diff --git a/src/components/TutorialNavigation/TutorialNavigation.module.css b/src/components/TutorialNavigation/TutorialNavigation.module.css index 2ce020246f..4ee3738c18 100644 --- a/src/components/TutorialNavigation/TutorialNavigation.module.css +++ b/src/components/TutorialNavigation/TutorialNavigation.module.css @@ -1,32 +1,3 @@ -.backButton { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: transparent; - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 8px; - color: var(--ifm-color-content); - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - transition: all 0.2s ease; - margin-bottom: 2rem; -} - -.backButton:hover { - background: var(--ifm-color-emphasis-100); - border-color: var(--ifm-color-primary); - color: var(--ifm-color-primary); - text-decoration: none; - transform: translateX(-2px); -} - -.backIcon { - width: 16px; - height: 16px; -} - .nextButton { display: flex; align-items: center; @@ -72,58 +43,6 @@ flex-shrink: 0; } -.tutorialProgress { - background: var(--ifm-card-background-color); - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 8px; - padding: 1rem; - margin: 2rem 0; -} - -.progressHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; -} - -.stepIndicator { - font-size: 0.8rem; - color: var(--ifm-color-content-secondary); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.stepTitle { - font-size: 1rem; - font-weight: 500; - color: var(--ifm-color-content); -} - -.progressBar { - width: 100%; - height: 4px; - background: var(--ifm-color-emphasis-200); - border-radius: 2px; - overflow: hidden; -} - -.progressFill { - height: 100%; - background: linear-gradient(135deg, #444CE7 0%, #7C3AED 100%); - border-radius: 2px; - transition: width 0.5s ease; -} - -/* Dark mode adjustments */ -:global([data-theme='dark']) .backButton { - border-color: var(--ifm-color-emphasis-400); -} - -:global([data-theme='dark']) .backButton:hover { - background: var(--ifm-color-emphasis-200); -} /* Mobile responsive */ @media (max-width: 768px) { @@ -138,10 +57,4 @@ .nextDescription { font-size: 0.8rem; } - - .progressHeader { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } } \ No newline at end of file diff --git a/src/components/TutorialNavigation/index.js b/src/components/TutorialNavigation/index.js index 159feb283b..0f0a02f89b 100644 --- a/src/components/TutorialNavigation/index.js +++ b/src/components/TutorialNavigation/index.js @@ -1 +1 @@ -export { BackButton, NextButton, TutorialProgress } from './TutorialNavigation'; +export { NextButton } from './TutorialNavigation'; diff --git a/src/components/icons/SearchIcon.js b/src/components/icons/SearchIcon.js deleted file mode 100644 index 5c82a5a2e9..0000000000 --- a/src/components/icons/SearchIcon.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -export const SearchIcon = ({ size = 16, color = '#065f46' }) => ( - - - -); diff --git a/src/components/icons/index.js b/src/components/icons/index.js index 8a29069645..cc785cd90e 100644 --- a/src/components/icons/index.js +++ b/src/components/icons/index.js @@ -1,2 +1 @@ export { CheckIcon } from './CheckIcon'; -export { SearchIcon } from './SearchIcon'; From f78d6893759d0484b86c2bcbe117199b8802874f Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 14:34:09 -0600 Subject: [PATCH 08/40] adding custom language order for language tabs --- .../build-your-first-workflow.mdx | 187 +++++++++--------- src/components/elements/SdkTabs/index.js | 6 +- 2 files changed, 96 insertions(+), 97 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index fa2228f418..9e9dd21a97 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -21,6 +21,18 @@ import { TemporalProgress } from "@site/src/components/TemporalProgress"; import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { NextButton } from "@site/src/components/TutorialNavigation"; import SdkTabs from "@site/src/components/elements/SdkTabs"; +import { FaPython, FaJava } from 'react-icons/fa'; +import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; + +export const TUTORIAL_LANGUAGE_ORDER = [ + { key: 'py', label: 'Python', icon: FaPython }, + { key: 'go', label: 'Go', icon: SiGo }, + { key: 'java', label: 'Java', icon: FaJava }, + { key: 'ts', label: 'TypeScript', icon: SiTypescript }, + { key: 'php', label: 'PHP', icon: SiPhp }, + { key: 'dotnet', label: '.NET', icon: SiDotnet }, + { key: 'rb', label: 'Ruby', icon: SiRuby }, +]; You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process. @@ -124,7 +136,7 @@ None of your application code runs on the Temporal Server. Your Worker, Workflow ## Build your Workflow and Activities - +

Workflow Definition

@@ -223,115 +235,100 @@ class BankingActivities:
- +

Workflow Definition

-In the Temporal .NET SDK, a Workflow Definition is marked by the `[Workflow]` attribute placed above the class. +In the Temporal Go SDK, a Workflow Definition is a Go function that follows the Workflow function signature. This is what the Workflow Definition looks like for this process: -**MoneyTransferWorker/Workflow.cs** - -```csharp -namespace Temporalio.MoneyTransferProject.MoneyTransferWorker; -using Temporalio.MoneyTransferProject.BankingService.Exceptions; -using Temporalio.Workflows; -using Temporalio.Common; -using Temporalio.Exceptions; - -[Workflow] -public class MoneyTransferWorkflow -{ - [WorkflowRun] - public async Task RunAsync(PaymentDetails details) - { - // Retry policy - var retryPolicy = new RetryPolicy - { - InitialInterval = TimeSpan.FromSeconds(1), - MaximumInterval = TimeSpan.FromSeconds(100), - BackoffCoefficient = 2, - MaximumAttempts = 3, - NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } - }; - - string withdrawResult; - try - { - withdrawResult = await Workflow.ExecuteActivityAsync( - () => BankingActivities.WithdrawAsync(details), - new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } - ); - } - catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException") - { - throw new ApplicationFailureException("Withdrawal failed due to insufficient funds.", ex); - } - - string depositResult; - try - { - depositResult = await Workflow.ExecuteActivityAsync( - () => BankingActivities.DepositAsync(details), - new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } - ); - // If everything succeeds, return transfer complete - return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})"; - } - catch (Exception depositEx) - { - try - { - // if the deposit fails, attempt to refund the withdrawal - string refundResult = await Workflow.ExecuteActivityAsync( - () => BankingActivities.RefundAsync(details), - new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } - ); - // If refund is successful, but deposit failed - throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money returned to {details.SourceAccount}.", depositEx); - } - catch (Exception refundEx) - { - // If both deposit and refund fail - throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money could not be returned to {details.SourceAccount}. Cause: {refundEx.Message}", refundEx); - } - } - } +**workflows.go** + +```go +package workflows + +import ( + "context" + "time" + + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +func MoneyTransferWorkflow(ctx workflow.Context, input PaymentDetails) (string, error) { + // Retry policy + retryPolicy := &temporal.RetryPolicy{ + InitialInterval: time.Second, + BackoffCoefficient: 2.0, + MaximumInterval: time.Second * 100, + MaximumAttempts: 3, + NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, + } + + options := workflow.ActivityOptions{ + StartToCloseTimeout: 5 * time.Minute, + RetryPolicy: retryPolicy, + } + ctx = workflow.WithActivityOptions(ctx, options) + + // Withdraw money + var withdrawResult string + err := workflow.ExecuteActivity(ctx, WithdrawActivity, input).Get(ctx, &withdrawResult) + if err != nil { + return "", err + } + + // Deposit money + var depositResult string + err = workflow.ExecuteActivity(ctx, DepositActivity, input).Get(ctx, &depositResult) + if err != nil { + // If deposit fails, attempt to refund + var refundResult string + refundErr := workflow.ExecuteActivity(ctx, RefundActivity, input).Get(ctx, &refundResult) + if refundErr != nil { + return "", temporal.NewApplicationError("Failed to deposit and refund", "DepositRefundFailed", err) + } + return "", temporal.NewApplicationError("Deposit failed, money refunded", "DepositFailed", err) + } + + return "Transfer complete (transaction IDs: " + withdrawResult + ", " + depositResult + ")", nil } ```

Activity Definition

-Activities handle the business logic. Each activity method calls an external banking service: +Activities handle the business logic. Each activity function calls an external banking service: + +**activities.go** + +```go +package activities + +import ( + "context" + "fmt" + + "go.temporal.io/sdk/activity" +) + +func WithdrawActivity(ctx context.Context, input PaymentDetails) (string, error) { + logger := activity.GetLogger(ctx) + logger.Info("Withdrawing money", "Amount", input.Amount, "Account", input.SourceAccount) + + bankService := NewBankingService("bank1.example.com") + referenceID := input.ReferenceID + "-withdrawal" + + result, err := bankService.Withdraw(input.SourceAccount, input.Amount, referenceID) + if err != nil { + return "", err + } -**MoneyTransferWorker/Activities.cs** - -```csharp -namespace Temporalio.MoneyTransferProject.MoneyTransferWorker; -using Temporalio.Activities; -using Temporalio.Exceptions; - -public class BankingActivities -{ - [Activity] - public static async Task WithdrawAsync(PaymentDetails details) - { - var bankService = new BankingService("bank1.example.com"); - Console.WriteLine($"Withdrawing ${details.Amount} from account {details.SourceAccount}."); - try - { - return await bankService.WithdrawAsync(details.SourceAccount, details.Amount, details.ReferenceId).ConfigureAwait(false); - } - catch (Exception ex) - { - throw new ApplicationFailureException("Withdrawal failed", ex); - } - } + return result, nil } ``` -
+
diff --git a/src/components/elements/SdkTabs/index.js b/src/components/elements/SdkTabs/index.js index f00069b048..58acb74fc1 100644 --- a/src/components/elements/SdkTabs/index.js +++ b/src/components/elements/SdkTabs/index.js @@ -26,7 +26,7 @@ export const Ruby = ({ children }) => children; Ruby.displayName = 'rb'; // Main wrapper -const SdkTabs = ({ children }) => { +const SdkTabs = ({ children, languageOrder }) => { const contentMap = {}; React.Children.forEach(children, (child) => { @@ -36,9 +36,11 @@ const SdkTabs = ({ children }) => { } }); + const languages = languageOrder || SDK_LANGUAGES; + return ( - {SDK_LANGUAGES.map(({ key, icon: Icon, label }) => ( + {languages.map(({ key, icon: Icon, label }) => ( }> {contentMap[key] || (
From e592d93e94a35948b3e86858e84f5f5135ecc2bf Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 14:43:39 -0600 Subject: [PATCH 09/40] title and description change --- .../build-your-first-workflow.mdx | 5 ++--- docs/build-your-first-basic-workflow/failure-simulation.mdx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 9e9dd21a97..974c4d5a01 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -1,11 +1,10 @@ --- id: build-your-first-workflow -title: Run your first Temporal application with the Python SDK +title: Build Your First Workflow sidebar_label: "Part 1: Build Your First Workflow" -description: Learn Temporal's core concepts by building a money transfer Workflow with the Python SDK. Experience reliability, failure handling, and live debugging in a 10-minute tutorial. +description: Learn Temporal's core concepts by building a money transfer Workflow. Experience reliability, failure handling, and live debugging in a short tutorial. keywords: - temporal - - python - workflow - tutorial - money transfer diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index f006786441..98de2af498 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -2,7 +2,7 @@ id: failure-simulation title: Simulate Failures with Temporal sidebar_label: "Part 2: Failure Simulation" -description: Learn how Temporal handles failures, recovers from crashes, and enables live debugging of your Python workflows. +description: Learn how Temporal handles failures, recovers from crashes, and enables live debugging of your Workflows. hide_table_of_contents: true keywords: - temporal From c684dea6ebc26c392e267cfbe11a430a1dd4da61 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Thu, 13 Nov 2025 16:45:18 -0600 Subject: [PATCH 10/40] getting started section --- .../failure-simulation.mdx | 16 +++--- sidebars.js | 51 +++++++++++-------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index 98de2af498..d182c11804 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -374,23 +374,23 @@ Test your understanding of what you just experienced:
diff --git a/sidebars.js b/sidebars.js index 1a6e77085e..2c5a540ad0 100644 --- a/sidebars.js +++ b/sidebars.js @@ -3,32 +3,39 @@ module.exports = { "index", { type: "category", - label: "Quickstarts", + label: "Getting Started", collapsed: false, - link: { - type: "doc", - id: "quickstarts", - }, - items: [], - }, - { - type: "category", - label: "Build your First Basic Workflow", - collapsed: false, - link: { - type: "doc", - id: "build-your-first-basic-workflow/index", - }, items: [ - "build-your-first-basic-workflow/build-your-first-workflow", - "build-your-first-basic-workflow/failure-simulation", + { + type: "category", + label: "Quickstarts", + collapsed: false, + link: { + type: "doc", + id: "quickstarts", + }, + items: [], + }, + { + type: "category", + label: "Build your First Basic Workflow", + collapsed: true, + link: { + type: "doc", + id: "build-your-first-basic-workflow/index", + }, + items: [ + "build-your-first-basic-workflow/build-your-first-workflow", + "build-your-first-basic-workflow/failure-simulation", + ], + }, + { + type: "link", + label: "Courses and Tutorials", + href: "https://learn.temporal.io/", + }, ], }, - { - type: "link", - label: "Courses and Tutorials", - href: "https://learn.temporal.io/", - }, { type: "category", label: "Evaluate", From 302eb073823c56c1f2d4a68f40c9f2240e938bca Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 17 Nov 2025 13:45:40 -0600 Subject: [PATCH 11/40] adding in workflow and activity definition base --- .../build-your-first-workflow.mdx | 665 +++++++++++++++--- 1 file changed, 572 insertions(+), 93 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 974c4d5a01..2b6de05277 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -238,96 +238,601 @@ class BankingActivities:

Workflow Definition

-In the Temporal Go SDK, a Workflow Definition is a Go function that follows the Workflow function signature. +In the Temporal Go SDK, a Workflow Definition is a Go function that accepts a Workflow Context and input parameters. -This is what the Workflow Definition looks like for this process: +This is what the Workflow Definition looks like for the money transfer process: -**workflows.go** +**workflow.go** ```go -package workflows +func MoneyTransfer(ctx workflow.Context, input PaymentDetails) (string, error) { + // RetryPolicy specifies how to automatically handle retries if an Activity fails. + retrypolicy := &temporal.RetryPolicy{ + InitialInterval: time.Second, + BackoffCoefficient: 2.0, + MaximumInterval: 100 * time.Second, + MaximumAttempts: 500, // 0 is unlimited retries + NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, + } + + options := workflow.ActivityOptions{ + // Timeout options specify when to automatically timeout Activity functions. + StartToCloseTimeout: time.Minute, + // Optionally provide a customized RetryPolicy. + // Temporal retries failed Activities by default. + RetryPolicy: retrypolicy, + } + + // Apply the options. + ctx = workflow.WithActivityOptions(ctx, options) + + // Withdraw money. + var withdrawOutput string + withdrawErr := workflow.ExecuteActivity(ctx, Withdraw, input).Get(ctx, &withdrawOutput) + if withdrawErr != nil { + return "", withdrawErr + } + + // Deposit money. + var depositOutput string + depositErr := workflow.ExecuteActivity(ctx, Deposit, input).Get(ctx, &depositOutput) + if depositErr != nil { + // The deposit failed; put money back in original account. + var result string + refundErr := workflow.ExecuteActivity(ctx, Refund, input).Get(ctx, &result) + if refundErr != nil { + return "", + fmt.Errorf("Deposit: failed to deposit money into %v: %v. Money could not be returned to %v: %w", + input.TargetAccount, depositErr, input.SourceAccount, refundErr) + } + return "", fmt.Errorf("Deposit: failed to deposit money into %v: Money returned to %v: %w", + input.TargetAccount, input.SourceAccount, depositErr) + } + + result := fmt.Sprintf("Transfer complete (transaction IDs: %s, %s)", withdrawOutput, depositOutput) + return result, nil +} +``` -import ( - "context" - "time" +The `MoneyTransfer` function takes in the details about the transaction, executes Activities to withdraw and deposit the money, and returns the results of the process. The `PaymentDetails` type is defined in `shared.go`: - "go.temporal.io/sdk/temporal" - "go.temporal.io/sdk/workflow" -) +**shared.go** -func MoneyTransferWorkflow(ctx workflow.Context, input PaymentDetails) (string, error) { - // Retry policy - retryPolicy := &temporal.RetryPolicy{ - InitialInterval: time.Second, - BackoffCoefficient: 2.0, - MaximumInterval: time.Second * 100, - MaximumAttempts: 3, - NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, - } - - options := workflow.ActivityOptions{ - StartToCloseTimeout: 5 * time.Minute, - RetryPolicy: retryPolicy, - } - ctx = workflow.WithActivityOptions(ctx, options) - - // Withdraw money - var withdrawResult string - err := workflow.ExecuteActivity(ctx, WithdrawActivity, input).Get(ctx, &withdrawResult) - if err != nil { - return "", err - } - - // Deposit money - var depositResult string - err = workflow.ExecuteActivity(ctx, DepositActivity, input).Get(ctx, &depositResult) - if err != nil { - // If deposit fails, attempt to refund - var refundResult string - refundErr := workflow.ExecuteActivity(ctx, RefundActivity, input).Get(ctx, &refundResult) - if refundErr != nil { - return "", temporal.NewApplicationError("Failed to deposit and refund", "DepositRefundFailed", err) - } - return "", temporal.NewApplicationError("Deposit failed, money refunded", "DepositFailed", err) - } - - return "Transfer complete (transaction IDs: " + withdrawResult + ", " + depositResult + ")", nil +```go +type PaymentDetails struct { + SourceAccount string + TargetAccount string + Amount int + ReferenceID string } ```

Activity Definition

-Activities handle the business logic. Each activity function calls an external banking service: +Activities handle the business logic. Each Activity function calls an external banking service: -**activities.go** +**activity.go** ```go -package activities +func Withdraw(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Withdrawing $%d from account %s.\n\n", + data.Amount, + data.SourceAccount, + ) + + referenceID := fmt.Sprintf("%s-withdrawal", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + confirmation, err := bank.Withdraw(data.SourceAccount, data.Amount, referenceID) + return confirmation, err +} -import ( - "context" - "fmt" +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Depositing $%d into account %s.\n\n", + data.Amount, + data.TargetAccount, + ) - "go.temporal.io/sdk/activity" -) + referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + return confirmation, err +} +``` + + + + + +

Workflow Definition

-func WithdrawActivity(ctx context.Context, input PaymentDetails) (string, error) { - logger := activity.GetLogger(ctx) - logger.Info("Withdrawing money", "Amount", input.Amount, "Account", input.SourceAccount) +In the Temporal Java SDK, a Workflow Definition is marked by the `@WorkflowInterface` attribute placed above the class interface. - bankService := NewBankingService("bank1.example.com") - referenceID := input.ReferenceID + "-withdrawal" - - result, err := bankService.Withdraw(input.SourceAccount, input.Amount, referenceID) - if err != nil { - return "", err - } +**MoneyTransferWorkflow.java** - return result, nil +```java +@WorkflowInterface +public interface MoneyTransferWorkflow { + @WorkflowMethod + void transfer(TransactionDetails transaction); } ``` - +**MoneyTransferWorkflowImpl.java** + +```java +public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) + .setMaximumInterval(Duration.ofSeconds(20)) + .setBackoffCoefficient(2) + .setMaximumAttempts(5000) + .build(); + + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) + .setStartToCloseTimeout(Duration.ofSeconds(2)) + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) + .build(); + + private final AccountActivity accountActivityStub = + Workflow.newActivityStub(AccountActivity.class, defaultActivityOptions); + + @Override + public void transfer(TransactionDetails transaction) { + String sourceAccountId = transaction.getSourceAccountId(); + String destinationAccountId = transaction.getDestinationAccountId(); + String transactionReferenceId = transaction.getTransactionReferenceId(); + int amountToTransfer = transaction.getAmountToTransfer(); + + // Stage 1: Withdraw funds from source + try { + accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); + } catch (Exception e) { + System.out.printf("[%s] Withdrawal of $%d from account %s failed", + transactionReferenceId, amountToTransfer, sourceAccountId); + return; + } + + // Stage 2: Deposit funds to destination + try { + accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); + System.out.printf("[%s] Transaction succeeded.\n", transactionReferenceId); + return; + } catch (Exception e) { + System.out.printf("[%s] Deposit of $%d to account %s failed.\n", + transactionReferenceId, amountToTransfer, destinationAccountId); + } + + // Compensate with a refund + try { + System.out.printf("[%s] Refunding $%d to account %s.\n", + transactionReferenceId, amountToTransfer, sourceAccountId); + accountActivityStub.refund(sourceAccountId, transactionReferenceId, amountToTransfer); + System.out.printf("[%s] Refund to originating account was successful.\n", transactionReferenceId); + } catch (Exception e) { + System.out.printf("[%s] Workflow failed.", transactionReferenceId); + throw(e); + } + } +} +``` + +The `TransactionDetails` interface: + +**TransactionDetails.java** + +```java +public interface TransactionDetails { + String getSourceAccountId(); + String getDestinationAccountId(); + String getTransactionReferenceId(); + int getAmountToTransfer(); +} +``` + +

Activity Definition

+ +Activities handle the business logic. Each Activity method calls an external banking service: + +**AccountActivity.java** + +```java +@ActivityInterface +public interface AccountActivity { + @ActivityMethod + void withdraw(String accountId, String referenceId, int amount); + + @ActivityMethod + void deposit(String accountId, String referenceId, int amount); + + @ActivityMethod + void refund(String accountId, String referenceId, int amount); +} +``` + +**AccountActivityImpl.java** + +```java +public class AccountActivityImpl implements AccountActivity { + @Override + public void withdraw(String accountId, String referenceId, int amount) { + System.out.printf("\nWithdrawing $%d from account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } + + @Override + public void deposit(String accountId, String referenceId, int amount) { + System.out.printf("\nDepositing $%d into account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } + + @Override + public void refund(String accountId, String referenceId, int amount) { + System.out.printf("\nRefunding $%d to account %s.\n[ReferenceId: %s]\n", + amount, accountId, referenceId); + } +} +``` + +
+ + + +

Workflow Definition

+ +In the Temporal TypeScript SDK, a Workflow Definition is a regular TypeScript function that accepts some input values. + +This is what the Workflow Definition looks like for the money transfer process: + +**workflows.ts** + +```typescript +import { proxyActivities } from '@temporalio/workflow'; +import { ApplicationFailure } from '@temporalio/common'; + +import type * as activities from './activities'; +import type { PaymentDetails } from './shared'; + +export async function moneyTransfer(details: PaymentDetails): Promise { + // Get the Activities for the Workflow and set up the Activity Options. + const { withdraw, deposit, refund } = proxyActivities({ + // RetryPolicy specifies how to automatically handle retries if an Activity fails. + retry: { + initialInterval: '1 second', + maximumInterval: '1 minute', + backoffCoefficient: 2, + maximumAttempts: 500, + nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'], + }, + startToCloseTimeout: '1 minute', + }); + + // Execute the withdraw Activity + let withdrawResult: string; + try { + withdrawResult = await withdraw(details); + } catch (withdrawErr) { + throw new ApplicationFailure(`Withdrawal failed. Error: ${withdrawErr}`); + } + + //Execute the deposit Activity + let depositResult: string; + try { + depositResult = await deposit(details); + } catch (depositErr) { + // The deposit failed; try to refund the money. + let refundResult; + try { + refundResult = await refund(details); + throw ApplicationFailure.create({ + message: `Failed to deposit money into account ${details.targetAccount}. Money returned to ${details.sourceAccount}. Cause: ${depositErr}.`, + }); + } catch (refundErr) { + throw ApplicationFailure.create({ + message: `Failed to deposit money into account ${details.targetAccount}. Money could not be returned to ${details.sourceAccount}. Cause: ${refundErr}.`, + }); + } + } + return `Transfer complete (transaction IDs: ${withdrawResult}, ${depositResult})`; +} +``` + +The `PaymentDetails` type is defined in `shared.ts`: + +**shared.ts** + +```typescript +export type PaymentDetails = { + amount: number; + sourceAccount: string; + targetAccount: string; + referenceId: string; +}; +``` + +

Activity Definition

+ +Activities handle the business logic. Each Activity function calls an external banking service: + +**activities.ts** + +```typescript +import type { PaymentDetails } from './shared'; +import { BankingService } from './banking-client'; + +export async function withdraw(details: PaymentDetails): Promise { + console.log( + `Withdrawing $${details.amount} from account ${details.sourceAccount}.\n\n` + ); + const bank1 = new BankingService('bank1.example.com'); + return await bank1.withdraw( + details.sourceAccount, + details.amount, + details.referenceId + ); +} + +export async function deposit(details: PaymentDetails): Promise { + console.log( + `Depositing $${details.amount} into account ${details.targetAccount}.\n\n` + ); + const bank2 = new BankingService('bank2.example.com'); + return await bank2.deposit( + details.targetAccount, + details.amount, + details.referenceId + ); +} + +export async function refund(details: PaymentDetails): Promise { + console.log( + `Refunding $${details.amount} to account ${details.sourceAccount}.\n\n` + ); + const bank1 = new BankingService('bank1.example.com'); + return await bank1.deposit( + details.sourceAccount, + details.amount, + details.referenceId + ); +} +``` + +
+ + + +

Workflow Definition

+ +In the Temporal Ruby SDK, a Workflow Definition is a class that extends `Temporalio::Workflow::Definition`. + +This is what the Workflow Definition looks like for the money transfer process: + +**workflow.rb** + +```ruby +class MoneyTransferWorkflow < Temporalio::Workflow::Definition + def execute(details) + retry_policy = Temporalio::RetryPolicy.new( + max_interval: 10, + non_retryable_error_types: [ + 'InvalidAccountError', + 'InsufficientFundsError' + ] + ) + + Temporalio::Workflow.logger.info("Starting workflow (#{details})") + + withdraw_result = Temporalio::Workflow.execute_activity( + BankActivities::Withdraw, + details, + start_to_close_timeout: 5, + retry_policy: retry_policy + ) + + begin + deposit_result = Temporalio::Workflow.execute_activity( + BankActivities::Deposit, + details, + start_to_close_timeout: 5, + retry_policy: retry_policy + ) + + "Transfer complete (transaction IDs: #{withdraw_result}, #{deposit_result})" + rescue Temporalio::Error::ActivityError => e + # Since the deposit failed, attempt to recover by refunding + begin + refund_result = Temporalio::Workflow.execute_activity( + BankActivities::Refund, + details, + start_to_close_timeout: 5, + retry_policy: retry_policy + ) + + "Transfer complete (transaction IDs: #{withdraw_result}, #{refund_result})" + rescue Temporalio::Error::ActivityError => refund_error + Temporalio::Workflow.logger.error("Refund failed: #{refund_error}") + end + end + end +end +``` + +The `TransferDetails` struct is defined in `shared.rb`: + +**shared.rb** + +```ruby +TransferDetails = Struct.new(:source_account, :target_account, :amount, :reference_id) do + def to_s + "TransferDetails { #{source_account}, #{target_account}, #{amount}, #{reference_id} }" + end +end +``` + +

Activity Definition

+ +Activities handle the business logic. Each Activity class calls an external banking service: + +**activities.rb** + +```ruby +module BankActivities + class Withdraw < Temporalio::Activity::Definition + def execute(details) + puts("Doing a withdrawal from #{details.source_account} for #{details.amount}") + raise InsufficientFundsError, 'Transfer amount too large' if details.amount > 1000 + + "OKW-#{details.amount}-#{details.source_account}" + end + end + + class Deposit < Temporalio::Activity::Definition + def execute(details) + puts("Doing a deposit into #{details.target_account} for #{details.amount}") + raise InvalidAccountError, 'Invalid account number' if details.target_account == 'B5555' + + "OKD-#{details.amount}-#{details.target_account}" + end + end + + class Refund < Temporalio::Activity::Definition + def execute(details) + puts("Refunding #{details.amount} back to account #{details.source_account}") + + "OKR-#{details.amount}-#{details.source_account}" + end + end +end +``` + +
+ + + +

Workflow Definition

+ +In the Temporal .NET SDK, a Workflow Definition is marked by the `[Workflow]` attribute placed above the class. + +This is what the Workflow Definition looks like for this process: + +**MoneyTransferWorker/Workflow.cs** + +```csharp +[Workflow] +public class MoneyTransferWorkflow +{ + [WorkflowRun] + public async Task RunAsync(PaymentDetails details) + { + // Retry policy + var retryPolicy = new RetryPolicy + { + InitialInterval = TimeSpan.FromSeconds(1), + MaximumInterval = TimeSpan.FromSeconds(100), + BackoffCoefficient = 2, + MaximumAttempts = 3, + NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } + }; + + string withdrawResult; + try + { + withdrawResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.WithdrawAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + } + catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException") + { + throw new ApplicationFailureException("Withdrawal failed due to insufficient funds.", ex); + } + + string depositResult; + try + { + depositResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.DepositAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})"; + } + catch (Exception depositEx) + { + try + { + // if the deposit fails, attempt to refund the withdrawal + string refundResult = await Workflow.ExecuteActivityAsync( + () => BankingActivities.RefundAsync(details), + new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy } + ); + throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money returned to {details.SourceAccount}.", depositEx); + } + catch (Exception refundEx) + { + throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money could not be returned to {details.SourceAccount}. Cause: {refundEx.Message}", refundEx); + } + } + } +} +``` + +The `PaymentDetails` record is defined in `PaymentDetails.cs`: + +**MoneyTransferWorker/PaymentDetails.cs** + +```csharp +public record PaymentDetails( + string SourceAccount, + string TargetAccount, + int Amount, + string ReferenceId); +``` + +

Activity Definition

+ +Activities handle the business logic. Each Activity method calls an external banking service: + +**MoneyTransferWorker/Activities.cs** + +```csharp +public class BankingActivities +{ + [Activity] + public static async Task WithdrawAsync(PaymentDetails details) + { + var bankService = new BankingService("bank1.example.com"); + Console.WriteLine($"Withdrawing ${details.Amount} from account {details.SourceAccount}."); + try + { + return await bankService.WithdrawAsync(details.SourceAccount, details.Amount, details.ReferenceId).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Withdrawal failed", ex); + } + } + + [Activity] + public static async Task DepositAsync(PaymentDetails details) + { + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } + } +} +``` + +
@@ -435,29 +940,3 @@ The Temporal Web UI lets you see details about the Workflow you just ran. > Continue to Part 2: Simulate Failures - -## Continue Your Learning - -
-
- -
-
- -
-
- -
-
\ No newline at end of file From 0fa18684aa1109cb1198fbc6ffe3862f73388100 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 17 Nov 2025 14:12:12 -0600 Subject: [PATCH 12/40] refining retry policy section and adding each sdk tab --- .../build-your-first-workflow.mdx | 138 +++++++++++++++++- .../RetryPolicyComparison.js | 34 +++++ .../RetryPolicyComparison.module.css | 87 +++++++++++ src/components/RetryPolicyComparison/index.js | 1 + 4 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 src/components/RetryPolicyComparison/RetryPolicyComparison.js create mode 100644 src/components/RetryPolicyComparison/RetryPolicyComparison.module.css create mode 100644 src/components/RetryPolicyComparison/index.js diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 2b6de05277..350b71313d 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -18,6 +18,7 @@ import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/element import { CallToAction } from "@site/src/components/elements/CallToAction"; import { TemporalProgress } from "@site/src/components/TemporalProgress"; import { StatusIndicators } from "@site/src/components/StatusIndicators"; +import { RetryPolicyComparison } from "@site/src/components/RetryPolicyComparison"; import { NextButton } from "@site/src/components/TutorialNavigation"; import SdkTabs from "@site/src/components/elements/SdkTabs"; import { FaPython, FaJava } from 'react-icons/fa'; @@ -840,16 +841,145 @@ public class BankingActivities Temporal makes your software durable and fault tolerant by default. If an Activity fails, Temporal automatically retries it, but you can customize this behavior through a Retry Policy. -In the `MoneyTransfer` Workflow, you'll see a Retry Policy that retries failed Activities up to 3 times, with specific errors that shouldn't be retried: +### Retry Policy Configuration + +In the `MoneyTransfer` Workflow, you'll see a Retry Policy that controls this behavior: + + + + +**workflows.py** ```python +# ... retry_policy = RetryPolicy( - maximum_attempts=3, - maximum_interval=timedelta(seconds=2), - non_retryable_error_types=["InvalidAccountError", "InsufficientFundsError"], + maximum_attempts=3, # Stop after 3 tries + maximum_interval=timedelta(seconds=2), # Don't wait longer than 2s + non_retryable_error_types=[ # Never retry these errors + "InvalidAccountError", + "InsufficientFundsError" + ], ) ``` + + + + +**workflow.go** + +```go +// ... +// RetryPolicy specifies how to automatically handle retries if an Activity fails. +retrypolicy := &temporal.RetryPolicy{ + InitialInterval: time.Second, + BackoffCoefficient: 2.0, + MaximumInterval: 100 * time.Second, + MaximumAttempts: 500, // 0 is unlimited retries + NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, +} + +options := workflow.ActivityOptions{ + // Timeout options specify when to automatically timeout Activity functions. + StartToCloseTimeout: time.Minute, + // Optionally provide a customized RetryPolicy. + // Temporal retries failed Activities by default. + RetryPolicy: retrypolicy, +} + +// Apply the options. +``` + + + + + +**src/main/java/moneytransfer/MoneyTransferWorkflowImpl.java** + +```java +// ... +private static final String WITHDRAW = "Withdraw"; + +// RetryOptions specify how to automatically handle retries when Activities fail +private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry + .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries + .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc + .setMaximumAttempts(5000) // Fail after 5000 attempts + .build(); +``` + + + + + +**src/workflows.ts** + +```typescript +// ... +const { withdraw, deposit, refund } = proxyActivities({ + // RetryPolicy specifies how to automatically handle retries if an Activity fails. + retry: { + initialInterval: '1 second', + maximumInterval: '1 minute', + backoffCoefficient: 2, + maximumAttempts: 500, + nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'], + }, + startToCloseTimeout: '1 minute', +}); +``` + + + + + +**MoneyTransferWorker/Workflow.cs** + +```csharp +// ... +// Retry policy +var retryPolicy = new RetryPolicy +{ + InitialInterval = TimeSpan.FromSeconds(1), + MaximumInterval = TimeSpan.FromSeconds(100), + BackoffCoefficient = 2, + MaximumAttempts = 3, + NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } +}; +``` + + + + + +**workflow.rb** + +```ruby +# Temporal Workflow that withdraws the specified amount from the source +# account and deposits it into the target account, refunding the source +# account if the deposit cannot be completed. +class MoneyTransferWorkflow < Temporalio::Workflow::Definition + def execute(details) + retry_policy = Temporalio::RetryPolicy.new( + max_interval: 10, + non_retryable_error_types: [ + 'InvalidAccountError', + 'InsufficientFundsError' + ] + ) +``` + + + + + +### What Makes Errors Non-Retryable? +Without retry policies, a temporary network glitch could cause your entire money transfer to fail. With Temporal's intelligent retries, your workflow becomes resilient to these common infrastructure issues. + + + + :::important This is a Simplified Example This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling. ::: diff --git a/src/components/RetryPolicyComparison/RetryPolicyComparison.js b/src/components/RetryPolicyComparison/RetryPolicyComparison.js new file mode 100644 index 0000000000..268ab414b7 --- /dev/null +++ b/src/components/RetryPolicyComparison/RetryPolicyComparison.js @@ -0,0 +1,34 @@ +import React from 'react'; +import styles from './RetryPolicyComparison.module.css'; + +export const RetryPolicyComparison = () => { + return ( +
+
+
+

Don't Retry

+
    +
  • InvalidAccountError - Wrong account number
  • +
  • InsufficientFundsError - Not enough money
  • +
+

+ These are business logic errors that won't be fixed by retrying. +

+
+
+
+
+

Retry Automatically

+
    +
  • Network timeouts - Temporary connectivity
  • +
  • Service unavailable - External API down
  • +
  • Rate limiting - Too many requests
  • +
+

+ These are temporary issues that often resolve themselves. +

+
+
+
+ ); +}; diff --git a/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css b/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css new file mode 100644 index 0000000000..1e0ca56cf9 --- /dev/null +++ b/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css @@ -0,0 +1,87 @@ +.comparisonContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-top: 1.5rem; + margin-bottom: 2rem; +} + +.errorType { + display: flex; +} + +.errorBox { + padding: 1rem; + border-radius: 8px; + border: 2px solid; + flex: 1; +} + +.noRetry { + border-color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} + +.doRetry { + border-color: #22c55e; + background: rgba(34, 197, 94, 0.1); +} + +.errorTitle { + margin: 0 0 0.5rem 0; + font-size: 1.1rem; +} + +.noRetry .errorTitle { + color: #ef4444; +} + +.doRetry .errorTitle { + color: #22c55e; +} + +.errorList { + margin: 0; + padding-left: 1.2rem; +} + +.errorList li { + margin-bottom: 0.25rem; +} + +.errorDescription { + font-size: 0.9rem; + margin: 0.5rem 0 0; + opacity: 0.8; +} + +/* Dark mode adjustments */ +:global([data-theme='dark']) .noRetry { + background: rgba(239, 68, 68, 0.15); + border-color: #f87171; +} + +:global([data-theme='dark']) .doRetry { + background: rgba(34, 197, 94, 0.15); + border-color: #4ade80; +} + +:global([data-theme='dark']) .noRetry .errorTitle { + color: #f87171; +} + +:global([data-theme='dark']) .doRetry .errorTitle { + color: #4ade80; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .comparisonContainer { + grid-template-columns: 1fr; + gap: 1rem; + } + + .errorBox { + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/src/components/RetryPolicyComparison/index.js b/src/components/RetryPolicyComparison/index.js new file mode 100644 index 0000000000..6eb9a9d405 --- /dev/null +++ b/src/components/RetryPolicyComparison/index.js @@ -0,0 +1 @@ +export { RetryPolicyComparison } from './RetryPolicyComparison'; From 0186c4bc703a8b328709e60a779e49a3b9358987 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 17 Nov 2025 14:23:04 -0600 Subject: [PATCH 13/40] add comments to retry policy code --- .../build-your-first-workflow.mdx | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 350b71313d..0d83fa972e 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -872,19 +872,16 @@ retry_policy = RetryPolicy( // ... // RetryPolicy specifies how to automatically handle retries if an Activity fails. retrypolicy := &temporal.RetryPolicy{ - InitialInterval: time.Second, - BackoffCoefficient: 2.0, - MaximumInterval: 100 * time.Second, - MaximumAttempts: 500, // 0 is unlimited retries - NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, + InitialInterval: time.Second, // Start with 1 second wait + BackoffCoefficient: 2.0, // Double the wait each time + MaximumInterval: 100 * time.Second, // Don't wait longer than 100s + MaximumAttempts: 500, // Stop after 500 tries (0 = unlimited) + NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, // Never retry these errors } options := workflow.ActivityOptions{ - // Timeout options specify when to automatically timeout Activity functions. - StartToCloseTimeout: time.Minute, - // Optionally provide a customized RetryPolicy. - // Temporal retries failed Activities by default. - RetryPolicy: retrypolicy, + StartToCloseTimeout: time.Minute, + RetryPolicy: retrypolicy, } // Apply the options. @@ -902,11 +899,11 @@ private static final String WITHDRAW = "Withdraw"; // RetryOptions specify how to automatically handle retries when Activities fail private final RetryOptions retryoptions = RetryOptions.newBuilder() - .setInitialInterval(Duration.ofSeconds(1)) // Wait 1 second before first retry - .setMaximumInterval(Duration.ofSeconds(20)) // Do not exceed 20 seconds between retries - .setBackoffCoefficient(2) // Wait 1 second, then 2, then 4, etc - .setMaximumAttempts(5000) // Fail after 5000 attempts - .build(); + .setInitialInterval(Duration.ofSeconds(1)) // Start with 1 second wait + .setMaximumInterval(Duration.ofSeconds(20)) // Don't wait longer than 20s + .setBackoffCoefficient(2) // Double the wait each time (1s, 2s, 4s, etc) + .setMaximumAttempts(5000) // Stop after 5000 tries + .build(); ``` @@ -920,13 +917,13 @@ private final RetryOptions retryoptions = RetryOptions.newBuilder() const { withdraw, deposit, refund } = proxyActivities({ // RetryPolicy specifies how to automatically handle retries if an Activity fails. retry: { - initialInterval: '1 second', - maximumInterval: '1 minute', - backoffCoefficient: 2, - maximumAttempts: 500, - nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'], + initialInterval: '1 second', // Start with 1 second wait + maximumInterval: '1 minute', // Don't wait longer than 1 minute + backoffCoefficient: 2, // Double the wait each time + maximumAttempts: 500, // Stop after 500 tries + nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'], // Never retry these errors }, - startToCloseTimeout: '1 minute', + startToCloseTimeout: '1 minute', // Activity must complete within 1 minute }); ``` @@ -941,11 +938,11 @@ const { withdraw, deposit, refund } = proxyActivities({ // Retry policy var retryPolicy = new RetryPolicy { - InitialInterval = TimeSpan.FromSeconds(1), - MaximumInterval = TimeSpan.FromSeconds(100), - BackoffCoefficient = 2, - MaximumAttempts = 3, - NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } + InitialInterval = TimeSpan.FromSeconds(1), // Start with 1 second wait + MaximumInterval = TimeSpan.FromSeconds(100), // Don't wait longer than 100s + BackoffCoefficient = 2, // Double the wait each time + MaximumAttempts = 3, // Stop after 3 tries + NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } // Never retry these errors }; ``` @@ -962,8 +959,8 @@ var retryPolicy = new RetryPolicy class MoneyTransferWorkflow < Temporalio::Workflow::Definition def execute(details) retry_policy = Temporalio::RetryPolicy.new( - max_interval: 10, - non_retryable_error_types: [ + max_interval: 10, # Don't wait longer than 10s + non_retryable_error_types: [ # Never retry these errors 'InvalidAccountError', 'InsufficientFundsError' ] From 15533aa3a0ba93c2e301b8437bebc1b40a47f430 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Mon, 17 Nov 2025 14:44:01 -0600 Subject: [PATCH 14/40] add base of worker step --- .../build-your-first-workflow.mdx | 244 +++++++++++++++++- 1 file changed, 242 insertions(+), 2 deletions(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 0d83fa972e..21c550ee6e 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -134,7 +134,7 @@ The Temporal Application will consist of the following pieces: None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. ::: -## Build your Workflow and Activities +## Step 1: Build your Workflow and Activities @@ -837,7 +837,7 @@ public class BankingActivities -## Set the Retry Policy +## Step 2: Set the Retry Policy Temporal makes your software durable and fault tolerant by default. If an Activity fails, Temporal automatically retries it, but you can customize this behavior through a Retry Policy. @@ -981,6 +981,246 @@ Without retry policies, a temporary network glitch could cause your entire money This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling. ::: +## Step 3: Create a Worker file + +A Worker is responsible for executing your Workflow and Activity code. It: + +- Can only execute Workflows and Activities registered to it +- Knows which piece of code to execute based on Tasks from the Task Queue +- Only listens to the Task Queue that it's registered to +- Returns execution results back to the Temporal Server + + + + +**run_worker.py** + +```python +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import BankingActivities +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME +from workflows import MoneyTransfer + + +async def main() -> None: + client: Client = await Client.connect("localhost:7233", namespace="default") + # Run the worker + activities = BankingActivities() + worker: Worker = Worker( + client, + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + workflows=[MoneyTransfer], + activities=[activities.withdraw, activities.deposit, activities.refund], + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +**worker/main.go** + +```go +func main() { + c, err := client.Dial(client.Options{}) + if err != nil { + log.Fatalln("Unable to create Temporal client.", err) + } + defer c.Close() + + w := worker.New(c, app.MoneyTransferTaskQueueName, worker.Options{}) + + // This worker hosts both Workflow and Activity functions. + w.RegisterWorkflow(app.MoneyTransfer) + w.RegisterActivity(app.Withdraw) + w.RegisterActivity(app.Deposit) + w.RegisterActivity(app.Refund) + + // Start listening to the Task Queue. + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("unable to start Worker", err) + } +} +``` + + + + + +**src/main/java/moneytransfer/MoneyTransferWorker.java** + +```java +package moneytransferapp; + +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; + +public class MoneyTransferWorker { + public static void main(String[] args) { + // Create a stub that accesses a Temporal Service on the local development machine + WorkflowServiceStubs serviceStub = WorkflowServiceStubs.newLocalServiceStubs(); + + // The Worker uses the Client to communicate with the Temporal Service + WorkflowClient client = WorkflowClient.newInstance(serviceStub); + + // A WorkerFactory creates Workers + WorkerFactory factory = WorkerFactory.newInstance(client); + + // A Worker listens to one Task Queue. + // This Worker processes both Workflows and Activities + Worker worker = factory.newWorker(Shared.MONEY_TRANSFER_TASK_QUEUE); + + // Register a Workflow implementation with this Worker + // The implementation must be known at runtime to dispatch Workflow tasks + // Workflows are stateful so a type is needed to create instances. + worker.registerWorkflowImplementationTypes(MoneyTransferWorkflowImpl.class); + + // Register Activity implementation(s) with this Worker. + // The implementation must be known at runtime to dispatch Activity tasks + // Activities are stateless and thread safe so a shared instance is used. + worker.registerActivitiesImplementations(new AccountActivityImpl()); + + System.out.println("Worker is running and actively polling the Task Queue."); + System.out.println("To quit, use ^C to interrupt."); + + // Start all registered Workers. The Workers will start polling the Task Queue. + factory.start(); + } +} +``` + + + + + +**src/worker.ts** + +```typescript +import { Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { namespace, taskQueueName } from './shared'; + +async function run() { + // Register Workflows and Activities with the Worker and connect to + // the Temporal server. + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + namespace, + taskQueue: taskQueueName, + }); + + // Start accepting tasks from the Task Queue. + await worker.run(); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + + +**MoneyTransferWorker/Program.cs** + +```csharp +// This file is designated to run the worker +using Temporalio.Client; +using Temporalio.Worker; +using Temporalio.MoneyTransferProject.MoneyTransferWorker; + +// Create a client to connect to localhost on "default" namespace +var client = await TemporalClient.ConnectAsync(new("localhost:7233")); + +// Cancellation token to shutdown worker on ctrl+c +using var tokenSource = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + tokenSource.Cancel(); + eventArgs.Cancel = true; +}; + +// Create an instance of the activities since we have instance activities. +// If we had all static activities, we could just reference those directly. +var activities = new BankingActivities(); + +// Create a worker with the activity and workflow registered +using var worker = new TemporalWorker( + client, // client + new TemporalWorkerOptions(taskQueue: "MONEY_TRANSFER_TASK_QUEUE") + .AddAllActivities(activities) // Register activities + .AddWorkflow() // Register workflow +); + +// Run the worker until it's cancelled +Console.WriteLine("Running worker..."); +try +{ + await worker.ExecuteAsync(tokenSource.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Worker cancelled"); +} +``` + + + + + +**worker.rb** + +```ruby +require_relative 'activities' +require_relative 'shared' +require_relative 'workflow' +require 'logger' +require 'temporalio/client' +require 'temporalio/worker' + +# Create a Temporal Client that connects to a local Temporal Service, uses +# a Namespace called 'default', and displays log messages to standard output +client = Temporalio::Client.connect( + 'localhost:7233', + 'default', + logger: Logger.new($stdout, level: Logger::INFO) +) + +# Create a Worker that polls the specified Task Queue and can +# fulfill requests for the specified Workflow and Activities +worker = Temporalio::Worker.new( + client:, + task_queue: MoneyTransfer::TASK_QUEUE_NAME, + workflows: [MoneyTransfer::MoneyTransferWorkflow], + activities: [MoneyTransfer::BankActivities::Withdraw, + MoneyTransfer::BankActivities::Deposit, + MoneyTransfer::BankActivities::Refund] +) + +# Start the Worker, which will poll the Task Queue until stopped +puts 'Starting Worker (press Ctrl+C to exit)' +worker.run(shutdown_signals: ['SIGINT']) +``` + + + + + ## Run Your Money Transfer Date: Mon, 17 Nov 2025 15:14:31 -0600 Subject: [PATCH 15/40] task queue base by language --- .../build-your-first-workflow.mdx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index 21c550ee6e..affc043a3f 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -1221,6 +1221,105 @@ worker.run(shutdown_signals: ['SIGINT']) +## Step 4: Define the Task Queue + +A Task Queue is where Temporal Workers look for Tasks about Workflows and Activities to execute. +Each Task Queue is identified by a name, which you will specify when you configure the Worker and again in the code that starts the Workflow Execution. +To ensure that the same name is used in both places, this project follows the recommended practice of specifying the Task Queue name in a constant referenced from both places. + +### Set Your Task Queue Name + +To ensure your Worker and Workflow starter use the same queue, define the Task Queue name as a constant: + + + + +**shared.py** + +```python +# Task Queue name - used by both Worker and Workflow starter +MONEY_TRANSFER_TASK_QUEUE_NAME = "MONEY_TRANSFER_TASK_QUEUE" +``` + + + + + +**app/shared.go** + +```go +package app + +// MoneyTransferTaskQueueName is the task queue name used by both +// the Worker and the Workflow starter +const MoneyTransferTaskQueueName = "MONEY_TRANSFER_TASK_QUEUE" +``` + + + + + +**src/main/java/moneytransfer/Shared.java** + +```java +package moneytransferapp; + +public class Shared { + // Task Queue name used by both Worker and Workflow starter + public static final String MONEY_TRANSFER_TASK_QUEUE = "MONEY_TRANSFER_TASK_QUEUE"; +} +``` + + + + + +**src/shared.ts** + +```typescript +// Task Queue name - used by both Worker and Workflow starter +export const taskQueueName = 'MONEY_TRANSFER_TASK_QUEUE'; +export const namespace = 'default'; +``` + + + + + +**Shared/Constants.cs** + +```csharp +namespace MoneyTransferProject.Shared +{ + public static class Constants + { + // Task Queue name used by both Worker and Workflow starter + public const string MONEY_TRANSFER_TASK_QUEUE = "MONEY_TRANSFER_TASK_QUEUE"; + } +} +``` + + + + + +**shared.rb** + +```ruby +module MoneyTransfer + # Task Queue name used by both Worker and Workflow starter + TASK_QUEUE_NAME = "MONEY_TRANSFER_TASK_QUEUE".freeze +end +``` + + + + + +:::tip Why Use Constants? +Using a shared constant prevents typos that would cause your Worker to listen to a different Task Queue than where your Workflow tasks are being sent. It's a common source of "Why isn't my Workflow running?" issues. +::: + ## Run Your Money Transfer Date: Mon, 17 Nov 2025 15:36:44 -0600 Subject: [PATCH 16/40] add execute the workflow base --- .../build-your-first-workflow.mdx | 265 +++++++++++++++++- 1 file changed, 264 insertions(+), 1 deletion(-) diff --git a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx index affc043a3f..d2853beebf 100644 --- a/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -2,6 +2,7 @@ id: build-your-first-workflow title: Build Your First Workflow sidebar_label: "Part 1: Build Your First Workflow" +hide_table_of_contents: true description: Learn Temporal's core concepts by building a money transfer Workflow. Experience reliability, failure handling, and live debugging in a short tutorial. keywords: - temporal @@ -109,7 +110,7 @@ The repository for this tutorial is a GitHub Template repository, which means yo Temporal Application Components
- Your Temporal Application + Your Temporal Application
@@ -1320,6 +1321,268 @@ end Using a shared constant prevents typos that would cause your Worker to listen to a different Task Queue than where your Workflow tasks are being sent. It's a common source of "Why isn't my Workflow running?" issues. ::: +## Step 5: Execute the Workflow + +Now you'll create a client program that starts a Workflow execution. This code connects to the Temporal Service and submits a Workflow execution request: + + + + +**start_workflow.py** + +```python +import asyncio +from temporalio.client import Client +from workflows import MoneyTransfer +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME + +async def main(): + # Create the Temporal Client to connect to the Temporal Service + client = await Client.connect("localhost:7233", namespace="default") + + # Define the money transfer details + details = { + "source_account": "A1001", + "target_account": "B2002", + "amount": 100, + "reference_id": "12345" + } + + # Start the Workflow execution + handle = await client.start_workflow( + MoneyTransfer.run, + details, + id=f"money-transfer-{details['reference_id']}", + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + ) + + print(f"Started Workflow {handle.id}") + print(f"Transferring ${details['amount']} from {details['source_account']} to {details['target_account']}") + + # Wait for the result + result = await handle.result() + print(f"Workflow result: {result}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +**start/main.go** + +```go +func main() { + // Create the client object just once per process + c, err := client.Dial(client.Options{}) + if err != nil { + log.Fatalln("Unable to create Temporal client:", err) + } + defer c.Close() + + input := app.PaymentDetails{ + SourceAccount: "85-150", + TargetAccount: "43-812", + Amount: 250, + ReferenceID: "12345", + } + + options := client.StartWorkflowOptions{ + ID: "pay-invoice-701", + TaskQueue: app.MoneyTransferTaskQueueName, + } + + log.Printf("Starting transfer from account %s to account %s for %d", + input.SourceAccount, input.TargetAccount, input.Amount) + + we, err := c.ExecuteWorkflow(context.Background(), options, app.MoneyTransfer, input) + if err != nil { + log.Fatalln("Unable to start the Workflow:", err) + } + + log.Printf("WorkflowID: %s RunID: %s\n", we.GetID(), we.GetRunID()) + + var result string + err = we.Get(context.Background(), &result) + if err != nil { + log.Fatalln("Unable to get Workflow result:", err) + } + + log.Println(result) +} +``` + + + + + +**src/main/java/moneytransfer/TransferApp.java** + +```java +public class TransferApp { + public static void main(String[] args) throws Exception { + // A WorkflowServiceStubs communicates with the Temporal front-end service. + WorkflowServiceStubs serviceStub = WorkflowServiceStubs.newLocalServiceStubs(); + + // A WorkflowClient wraps the stub and can be used to start, signal, query, cancel, and terminate Workflows. + WorkflowClient client = WorkflowClient.newInstance(serviceStub); + + // Workflow options configure Workflow stubs. + // A WorkflowId prevents duplicate instances. + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue(Shared.MONEY_TRANSFER_TASK_QUEUE) + .setWorkflowId("money-transfer-workflow") + .build(); + + // WorkflowStubs enable calls to methods as if the Workflow object is local + // but actually perform a gRPC call to the Temporal Service. + MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); + + // Configure the details for this money transfer request + String referenceId = UUID.randomUUID().toString().substring(0, 18); + String fromAccount = "A1001"; + String toAccount = "B2002"; + int amountToTransfer = 100; + TransactionDetails transaction = new CoreTransactionDetails(fromAccount, toAccount, referenceId, amountToTransfer); + + // Perform asynchronous execution + WorkflowExecution we = WorkflowClient.start(workflow::transfer, transaction); + + System.out.printf("Initiating transfer of $%d from [Account %s] to [Account %s].\n", + amountToTransfer, fromAccount, toAccount); + System.out.printf("[WorkflowID: %s] [RunID: %s] [Reference: %s]\n", + we.getWorkflowId(), we.getRunId(), referenceId); + } +} +``` + + + + + +**src/client.ts** + +```typescript +import { Connection, Client } from '@temporalio/client'; +import { moneyTransfer } from './workflows'; +import type { PaymentDetails } from './shared'; +import { namespace, taskQueueName } from './shared'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, namespace }); + + const details: PaymentDetails = { + amount: 400, + sourceAccount: '85-150', + targetAccount: '43-812', + referenceId: '12345', + }; + + console.log( + `Starting transfer from account ${details.sourceAccount} to account ${details.targetAccount} for $${details.amount}` + ); + + const handle = await client.workflow.start(moneyTransfer, { + args: [details], + taskQueue: taskQueueName, + workflowId: 'pay-invoice-801', + }); + + console.log( + `Started Workflow ${handle.workflowId} with RunID ${handle.firstExecutionRunId}` + ); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + + +**MoneyTransferStarter/Program.cs** + +```csharp +using Temporalio.Client; +using Temporalio.MoneyTransferProject.MoneyTransferWorker; + +// Create a client to connect to localhost on "default" namespace +var client = await TemporalClient.ConnectAsync(new("localhost:7233")); + +// Configure the money transfer details +var transferDetails = new TransferDetails +{ + SourceAccount = "A1001", + TargetAccount = "B2002", + Amount = 100, + ReferenceId = "12345" +}; + +Console.WriteLine($"Starting transfer of ${transferDetails.Amount} from {transferDetails.SourceAccount} to {transferDetails.TargetAccount}"); + +// Start the workflow +var handle = await client.StartWorkflowAsync( + (MoneyTransferWorkflow wf) => wf.RunAsync(transferDetails), + new(id: $"money-transfer-{transferDetails.ReferenceId}", taskQueue: "MONEY_TRANSFER_TASK_QUEUE")); + +Console.WriteLine($"Started workflow {handle.Id}"); + +// Wait for result +var result = await handle.GetResultAsync(); +Console.WriteLine($"Workflow result: {result}"); +``` + + + + + +**starter.rb** + +```ruby +require_relative 'shared' +require_relative 'workflow' +require 'securerandom' +require 'temporalio/client' + +# Create the Temporal Client that connects to the Temporal Service +client = Temporalio::Client.connect('localhost:7233', 'default') + +# Default values for the payment details +details = MoneyTransfer::TransferDetails.new('A1001', 'B2002', 100, SecureRandom.uuid) + +# Use the Temporal Client to submit a Workflow Execution request +handle = client.start_workflow( + MoneyTransfer::MoneyTransferWorkflow, + details, + id: "moneytransfer-#{details.reference_id}", + task_queue: MoneyTransfer::TASK_QUEUE_NAME +) + +puts "Initiated transfer of $#{details.amount} from #{details.source_account} to #{details.target_account}" +puts "Workflow ID: #{handle.id}" + +# Keep running (and retry) if the Temporal Service becomes unavailable +begin + puts "Workflow result: #{handle.result}" +rescue Temporalio::Error::RPCError + puts 'Temporal Service unavailable while awaiting result' + retry +end +``` + + + + + +This code uses a Temporal Client to connect to the Temporal Service, calling its workflow start method to request execution. This returns a handle, and calling result on that handle will block until execution is complete, at which point it provides the result. + ## Run Your Money Transfer Date: Wed, 19 Nov 2025 12:28:22 -0600 Subject: [PATCH 17/40] adding langauge tabs for crash test --- .../failure-simulation.mdx | 199 +++++++++++++++++- 1 file changed, 189 insertions(+), 10 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index d182c11804..a2e1ff8f94 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -18,11 +18,23 @@ tags: import { CallToAction } from "@site/src/components/elements/CallToAction"; import { TemporalProgress } from "@site/src/components/TemporalProgress"; - import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { WorkflowDiagram } from "@site/src/components/WorkflowDiagram"; import { RetryCounter } from "@site/src/components/RetryCounter"; import { TemporalCheckbox } from "@site/src/components/TemporalCheckbox"; +import SdkTabs from "@site/src/components/elements/SdkTabs"; +import { FaPython, FaJava } from 'react-icons/fa'; +import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; + +export const TUTORIAL_LANGUAGE_ORDER = [ + { key: 'py', label: 'Python', icon: FaPython }, + { key: 'go', label: 'Go', icon: SiGo }, + { key: 'java', label: 'Java', icon: FaJava }, + { key: 'ts', label: 'TypeScript', icon: SiTypescript }, + { key: 'php', label: 'PHP', icon: SiPhp }, + { key: 'dotnet', label: '.NET', icon: SiDotnet }, + { key: 'rb', label: 'Ruby', icon: SiRuby }, +]; import { CodeComparison } from "@site/src/components/CodeComparison"; # Part 2: Simulate Failures @@ -90,33 +102,200 @@ This is fundamentally different from traditional applications where process cras ### Instructions -1. **Make sure your Worker is stopped** before proceeding. -If the Worker is running, press `Ctrl+C` to stop it. + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. 2. **Start the Worker in Terminal 2:** ```bash python run_worker.py ``` -3. **In Terminal 3, start the Workflow:** +3. **Now in Terminal 3, start the Workflow:** ```bash python run_workflow.py ``` -4. **Inspect the Workflow Execution** using the Web UI. You can see the Worker is executing the Workflow and its Activities: +4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. -5. **Return to Terminal 2** and stop the Worker by pressing `Ctrl+C`. +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. -6. **Switch back to the Web UI** and refresh the page. Your Workflow is still listed as "Running". +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". - The Workflow is still in progress because the Temporal Server maintains the state of the Workflow, even when the Worker crashes. + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. -7. **Restart your Worker** by switching back to Terminal 2 and running the Worker command: +7. **Bring your Worker back to life** by going to Terminal 2 and running: ```bash python run_worker.py ``` -8. **Switch back to Terminal 3** where you ran `python run_workflow.py`. You'll see the program complete and you'll see the result message. +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + go run worker/main.go + ``` + +3. **Now in Terminal 3, start the Workflow:** + ```bash + go run start/main.go + ``` + +4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. + +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". + + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + +7. **Bring your Worker back to life** by going to Terminal 2 and running: + ```bash + go run worker/main.go + ``` + +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + ``` + +3. **Now in Terminal 3, start the Workflow:** + ```bash + mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" + ``` + +4. **Inspect the Workflow Execution** using the Web UI at `localhost:8080`. You can see the Worker is executing the Workflow and its Activities. + +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. + +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". + + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + +7. **Bring your Worker back to life** by going to Terminal 2 and running: + ```bash + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + ``` + +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + npm run worker + ``` + +3. **Now in Terminal 3, start the Workflow:** + ```bash + npm run client + ``` + +4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. + +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". + + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + +7. **Bring your Worker back to life** by going to Terminal 2 and running: + ```bash + npm run worker + ``` + +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + dotnet run --project MoneyTransferWorker + ``` + +3. **Now in Terminal 3, start the Workflow:** + ```bash + dotnet run --project MoneyTransferClient + ``` + +4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. + +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". + + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + +7. **Bring your Worker back to life** by going to Terminal 2 and running: + ```bash + dotnet run --project MoneyTransferWorker + ``` + +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + + +1. **Stop your Worker if it's running.** + Press `Ctrl+C` in the terminal to stop it. + +2. **Start the Worker in Terminal 2:** + ```bash + bundle exec ruby worker.rb + ``` + +3. **Now in Terminal 3, start the Workflow:** + ```bash + bundle exec ruby starter.rb + ``` + +4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + +5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. + +6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". + + That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + +7. **Bring your Worker back to life** by going to Terminal 2 and running: + ```bash + bundle exec ruby worker.rb + ``` + +8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! + + + + Date: Wed, 19 Nov 2025 12:45:26 -0600 Subject: [PATCH 18/40] different layout for crash test and friendlier language --- .../failure-simulation.mdx | 302 +++++++----------- 1 file changed, 124 insertions(+), 178 deletions(-) diff --git a/docs/build-your-first-basic-workflow/failure-simulation.mdx b/docs/build-your-first-basic-workflow/failure-simulation.mdx index a2e1ff8f94..df8a7b260e 100644 --- a/docs/build-your-first-basic-workflow/failure-simulation.mdx +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -22,6 +22,7 @@ import { StatusIndicators } from "@site/src/components/StatusIndicators"; import { WorkflowDiagram } from "@site/src/components/WorkflowDiagram"; import { RetryCounter } from "@site/src/components/RetryCounter"; import { TemporalCheckbox } from "@site/src/components/TemporalCheckbox"; +import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; import SdkTabs from "@site/src/components/elements/SdkTabs"; import { FaPython, FaJava } from 'react-icons/fa'; import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; @@ -102,206 +103,151 @@ This is fundamentally different from traditional applications where process cras ### Instructions - - - -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. - -2. **Start the Worker in Terminal 2:** - ```bash - python run_worker.py - ``` - -3. **Now in Terminal 3, start the Workflow:** - ```bash - python run_workflow.py - ``` - -4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. - -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. - -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". - - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. - -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - python run_worker.py - ``` - -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! - - - - - -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. - -2. **Start the Worker in Terminal 2:** - ```bash - go run worker/main.go - ``` - -3. **Now in Terminal 3, start the Workflow:** - ```bash - go run start/main.go - ``` - -4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. - -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. - -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". - - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. - -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - go run worker/main.go - ``` - -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! - - - - - -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. - -2. **Start the Worker in Terminal 2:** - ```bash - mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" - ``` - -3. **Now in Terminal 3, start the Workflow:** - ```bash - mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" - ``` - -4. **Inspect the Workflow Execution** using the Web UI at `localhost:8080`. You can see the Worker is executing the Workflow and its Activities. - -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. - -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". - - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. - -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" - ``` - -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! - - - - - -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. - -2. **Start the Worker in Terminal 2:** - ```bash - npm run worker - ``` - -3. **Now in Terminal 3, start the Workflow:** - ```bash - npm run client - ``` - -4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. - -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. - -6. **Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running". - - That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. - -7. **Bring your Worker back to life** by going to Terminal 2 and running: - ```bash - npm run worker - ``` - -8. **Head back to Terminal 3** where you started the Workflow. You'll see it finish up and show the result! - - - - - -1. **Stop your Worker if it's running.** - Press `Ctrl+C` in the terminal to stop it. + + + +
+ Terminal 2 - Worker +
+ + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + +
+}> -2. **Start the Worker in Terminal 2:** - ```bash - dotnet run --project MoneyTransferWorker - ``` +### Step 1: Start Your Worker -3. **Now in Terminal 3, start the Workflow:** - ```bash - dotnet run --project MoneyTransferClient - ``` +First, stop any running Worker (`Ctrl+C`) and start a fresh one in Terminal 2. -4. **Check out the Workflow** in the Web UI. You'll see your Worker busy executing the Workflow and its Activities. + -5. **Go back to Terminal 2** and kill the Worker with `Ctrl+C`. +
+ + +
+ Terminal 3 - Workflow +
+ + + python run_workflow.py + + + go run start/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" + + + npm run client + + + dotnet run --project MoneyTransferClient + + + bundle exec ruby starter.rb + + +