From 49216edc0f5b4c16f4a2c3ad545e1b8aba5f95dd Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Fri, 24 Oct 2025 12:35:15 -0500 Subject: [PATCH 01/10] 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/10] 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 z%NGYlFc!9G!H6S9Qq%=iFP3JP=+@XSPzelY_KJ%2i#jU}b&Q~U=ZG;yQ^P8h5zna0 zT;XkKquySZ(dq9@i(XQ;(-8Sgxl0tS$@DaIE;eD-z?MapEtn?F-1}vG$3T) zB-E7opW)(vnhJ}*u<}3Q-YvIbXB(EEN{3T4S4j6V{D%DoOB8i#*^QBP5pA|;E^fv3 zAZ#~|uc!Ti?)Iow9QmO>otsmOd|g3;-jaLy05xP8m^@_IaFP zo)Qgv&yqiSOrz$~0$eZ}@TL4K_1AFLUnzG^N z|H#%M=63=8-RtSe7DI8}04OPactK3EK(<1wyp8ieKp_yG=C z79Dv=zZ!!*aqWgD$KwLFKg$ZX;~g*1PkAZYho0cA+DYH^nV0J zZSdGpo@`$Y0qOdJ0#Z;NgTkYZ1n~I#N+{ z7kzD%I1(otA@moN$8y1niiDj#rZx=L^jsFUz6wI4!BAMwl-QC&WQ@Xq&FG)gg`Gs6 zWQPnSbX6(<5z1QRb=oeMpw4iR$sdmD^_<~ulzo>&XexZu8FJilX{!n!>6!kb?JC~m z-jHG92=aR5uUwjXfizW1A9YYG8PweWwaxI~UnT%3$x9wz{P`H&cJNUbY=sP5&_N*FwLFu!k}&}XrZ^WT(j-wQJ{b4A z=;B(8Koq(NZY~6LRmcV|e8|Zfo9$|aipRI*XJT?@X%&qL6XZvn>B)6lTLpa;dIB_6 zD}sxy|8~p%tEuuLgmvMr;}A}V9JmUCzFcSxQ))Yzze;`25+GB;`CxlqwAjtC6e8(}DvkH!Adt=on=(JPF@-mC}}z2Qw+s-YTc3#;Rj z{PQXqYjEgu2u)|kU%lVh@}BKinPU)#h6+XMXEn?R{F`JsxAHs-KzB-$8apyqU$v2% zI0*AIh*kh$=Zl#5Zx+qtMfsK@3|3g=8w~b^;>y(5t>6mE9#QNqmtrPdO}}UN|7zvK z5)PB22GKnjA^ig+8V+Oq7Wm{Ted1~N)gH}P>EuA=#pQg8EV9}DUk~uln%IEW>$KK% zX>ck5+qeT_3RxgBO_gl#@AyJBHn{er>?D@w8_Q3p_hrCHIocX$N1?SusR^k_s-v1#Gz2U;TN7N zPpOS!@@Bv`g-PgFvbcDTA2vV|KB=nRUaQgoVPdNumL^lUHU zObc~H=P~L3s(@q38zsZgvKVX@G~kUuWawzy8$*~o-^OE@RjqcP)T;XOc{JCyE<*g`+94o(L%zC`&7XyyWAsHTxbk42x2H#uSn>xf*w4 zx>pP8tf^7~W`(_qGT=l8z~P4i?A3=v>8qM|S`yMa(4)_5z>NrhX(cCK&-@d`@_1Xz^-VP?vzC$|)(oR~TQ5|hF| z#@Eg_gL%q$fze;NH6`^l!~chXKnU{{7iL)sY$xXt0-_a`2KN$?%QBW#iDK{WC)bT7 z`ulZ`iXk)yDTvmI(^maFW`Lw1TdL(VW64@0n|de!)sh_oJw!k#9$h7=lcZWSCf`=1 z`Fa4m@u2s_BACu@O_EfyA!&j-BvrDkt`DSfaLpeIl52i!%6Eelmz4iJq=9yn`A&B& zi55Q0o%9=bDNXGE6@&l%&x;pg3Z!fM_B;sJis7dtPhE8&9e?z3MiTDvP_0rkE?cgz zf&juPhJ`hfN@7stl*-6MVpdh9w36x@D{t#3qotQxmVoAf825%~a z#x*t|?+~=XlCl{m0jg+|tV2LlAtwX>!=?N0R3I=X`lk|Q)rJyA26U7w7&masH! zHn0;Q_u+^u6l9crGJ&~B45J?lenEFB@$Hz}S3}9OtA4Z7yS{Y*uGjegV5ROHFLQUB zt^WG&EB3$Re;tsr?n4L&Z@@=>no!f!<4TllaJZj|^3JP0u8#kBf>|D1qA?bR2=Ox< z09LL17BbqMRgpz8?nrRFS}+d5!_yooGN3x}Bu_D^psCEzYSu4^@dmA;hgBM)RvL(c zA%-Jz^HpM~K$@y3!#(Bx@2*NA0`&EzC%IFa-~HNZ*zxKVI~ifXP+f4rZ=3j9*j_fP zXqxEkS0m{w+L3k!e=1sXPFa$Ji`zSEJ%jB?{`5@ztSb5+KFSa=CFrL+j%H#T06QHM zQc6IWAfFiMj~1^UVmjsO+~KkXt_Mrg_Rg2xFp;-8bahhht6#_h12Lguyt>M9^C54n7+OnQO{pvc(@mL}aW3a}ZFg60%c zV$leMd`kz+3saa+Y-Mk4469d&fY$KO=gD<9w!*R6@+h+J=bD4_Ps8(?`I8_v>>s@= z5ysn5)Q`jNW7_{_QU8DAqai(Yoyk8-USS%g4pBHZOO>?*^#K&Ybjc9MNdP&%)~`6| zL=}KMIB_sPFsoQI!}H235sZpF)P?M@>AdOsRW~22;^j+5T_$2Er~*rqJk$7@wOz%P zJSvv}s7ni}E2#rs=#+0Kuhl75vZU}dyYg1YdRzByk4Mh=T*e;9>Mc;afGh@=waX{tDdD66*&eckP$%i93ivewPHLFY zbMkcw>yA=;gzygwTOdz zxfN!{NRrz3hdW`5Q?smtcunOaltoFfMn^{zZ*E-W`sq11)TiWPN{WkRA#}RibC6^CH~Z z-fk$->oh%Ft}Svs(vVBSnZ_uty!>_xPhsKjUgB}VC{H_O-dL6D19>}tM zEHPI~zOmj*Lf2NV9qyVX)}4GogsFwp4gt1i|HqLft~s#|A|t$^Xv^IX;z=OBBp^{z z$yZUG@>M~KVhLzDaV8DiW5>^%+YJoLeUGOP7-CR=_AMG_?}9#k6iTIxOoB}{L%xeN z^u)Qrp{a(rEkDHeKSfe@>)m~KOGurGgUp>hbB)$C&dZ3t@qad|#Oi!*@A#R;>OZ&-xbA}o-=of{AfxA

+ + ## 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
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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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",