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 new file mode 100644 index 0000000000..9f9eb27114 --- /dev/null +++ b/docs/build-your-first-basic-workflow/build-your-first-workflow.mdx @@ -0,0 +1,2101 @@ +--- +id: build-your-first-workflow +title: Build Your First Workflow +sidebar_label: "Part 1: Build Your First Workflow" +hide_table_of_contents: true +description: Learn Temporal's core concepts by building a money transfer Workflow. Experience reliability, failure handling, and live debugging in a short tutorial. +keywords: + - temporal + - workflow + - tutorial + - money transfer + - reliability +tags: + - Getting Started + - Tutorial +--- + +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 { StatusIndicators } from "@site/src/components/StatusIndicators"; +import { RetryPolicyComparison } from "@site/src/components/RetryPolicyComparison"; +import { NextButton } from "@site/src/components/TutorialNavigation"; +import SdkTabs from "@site/src/components/elements/SdkTabs"; +import { FaPython, FaJava } from 'react-icons/fa'; +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 }, +]; + +In this tutorial, you'll build and run your first Temporal application. +You'll understand the core building blocks of Temporal and learn how Temporal helps you build crash proof applications through durable execution. + + +
+ Temporal beginner +
+ + + + +## Introduction + + +### Prerequisites + +Before starting this tutorial: + +- **Set up a local development environment** for developing Temporal applications +- **Ensure you have Git installed** to clone the project + + + +### What You'll Build + +You’ll build a basic money transfer app from the ground up, learning how to handle essential transactions like deposits, withdrawals, and refunds using Temporal. + +**Why This Application?:** +Most applications require multiple coordinated steps - processing payments, sending emails, updating databases. +This tutorial uses money transfers to demonstrate how Temporal ensures these multi-step processes complete reliably, resuming exactly where they left off even after any failure. +
+ Money Transfer Application Flow +
+ + +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 + + + + + git clone https://github.com/temporalio/money-transfer-project-template-go + + + cd money-transfer-project-template-go + + + + + git clone https://github.com/temporalio/money-transfer-project-java + + + cd money-transfer-project-java + + + + + git clone https://github.com/temporalio/money-transfer-project-template-ts + + + cd money-transfer-project-template-ts + + + + + git clone https://github.com/temporalio/money-transfer-project-template-dotnet + + + cd money-transfer-temporal-template-dotnet + + + + + git clone https://github.com/temporalio/money-transfer-project-template-ruby + + + cd money-transfer-project-template-ruby + + + + + git clone https://github.com/temporalio/money-transfer-project-template-php + + + cd money-transfer-project-template-php + + + composer install + + + +}> + +### Download the example application + +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. + + + + +:::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 +
+
+ Your Temporal Application +
+ + + +}> + + + +### Let's Recap: Temporal's Application Structure + +The Temporal Application will consist of the following pieces: + +1. **A Workflow** written in your programming language of choice and your installed Temporal SDK in that language. A Workflow defines the overall flow of the application. +2. **An Activity** is a function or method that does specific operation - like withdrawing money, sending an email, or calling an API. Since these operations often depend on external services that can be unreliable, Temporal automatically retries Activities when they fail. +In this application, you'll write Activities for withdraw, deposit, and refund operations. +3. **A Worker**, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently. + +
+ +
+ +
+What You'll Build and Run + +The project in this tutorial mimics a "money transfer" application. +It is implemented with a single Workflow, which orchestrates the execution of three Activities (Withdraw, Deposit, and Refund) that move money between the accounts. + +To perform a money transfer, you will do the following: + +1. **Launch a Worker**: Since a Worker is responsible for executing the Workflow and Activity code, at least one Worker must be running for the money transfer to make progress. + +2. **Submit a Workflow Execution request** to the Temporal Service: After the Worker communicates with the Temporal Service, the Worker will begin executing the Workflow and Activity code. It reports the results to the Temporal Service, which tracks the progress of the Workflow Execution. + +
+ +:::important +None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications. +::: + +## Step 1: Build your Workflow and Activities + + + + +

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 +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) -> str: + 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 = f"Transfer complete (transaction IDs: {withdraw_output}, {deposit_output})" + return result + except ActivityError as deposit_err: + # Handle deposit error + workflow.logger.error(f"Deposit failed: {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( + f"Refund successful. Confirmation ID: {refund_output}" + ) + raise deposit_err + except ActivityError as refund_error: + workflow.logger.error(f"Refund failed: {refund_error}") + raise refund_error +``` + +

Activity Definition

+ +Activities handle the business logic. Each activity method calls an external banking service: + +**activities.py** + +```python +import asyncio +from temporalio import activity +from shared import PaymentDetails + +class BankingActivities: + @activity.defn + 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 +``` + +
+ + + +

Workflow Definition

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

Activity Definition

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

Workflow Definition

+ +In the Temporal Java SDK, a Workflow Definition is marked by the `@WorkflowInterface` attribute placed above the class interface. + +**MoneyTransferWorkflow.java** + +```java +@WorkflowInterface +public interface MoneyTransferWorkflow { + @WorkflowMethod + void transfer(TransactionDetails transaction); +} +``` + +**MoneyTransferWorkflowImpl.java** + +```java +public class MoneyTransferWorkflowImpl implements MoneyTransferWorkflow { + + // RetryOptions specify how to automatically handle retries when Activities fail + private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) + .setMaximumInterval(Duration.ofSeconds(20)) + .setBackoffCoefficient(2) + .setMaximumAttempts(5000) + .build(); + + private final ActivityOptions defaultActivityOptions = ActivityOptions.newBuilder() + .setRetryOptions(retryoptions) + .setStartToCloseTimeout(Duration.ofSeconds(2)) + .setScheduleToCloseTimeout(Duration.ofSeconds(5000)) + .build(); + + private final AccountActivity accountActivityStub = + Workflow.newActivityStub(AccountActivity.class, defaultActivityOptions); + + @Override + public void transfer(TransactionDetails transaction) { + String sourceAccountId = transaction.getSourceAccountId(); + String destinationAccountId = transaction.getDestinationAccountId(); + String transactionReferenceId = transaction.getTransactionReferenceId(); + int amountToTransfer = transaction.getAmountToTransfer(); + + // Stage 1: Withdraw funds from source + try { + accountActivityStub.withdraw(sourceAccountId, transactionReferenceId, amountToTransfer); + } catch (Exception e) { + System.out.printf("[%s] Withdrawal of $%d from account %s failed", + transactionReferenceId, amountToTransfer, sourceAccountId); + return; + } + + // Stage 2: Deposit funds to destination + try { + accountActivityStub.deposit(destinationAccountId, transactionReferenceId, amountToTransfer); + System.out.printf("[%s] Transaction succeeded.\n", transactionReferenceId); + return; + } catch (Exception e) { + System.out.printf("[%s] Deposit of $%d to account %s failed.\n", + transactionReferenceId, amountToTransfer, destinationAccountId); + } + + // Compensate with a refund + try { + System.out.printf("[%s] Refunding $%d to account %s.\n", + transactionReferenceId, amountToTransfer, sourceAccountId); + accountActivityStub.refund(sourceAccountId, transactionReferenceId, amountToTransfer); + System.out.printf("[%s] Refund to originating account was successful.\n", transactionReferenceId); + } catch (Exception e) { + System.out.printf("[%s] Workflow failed.", transactionReferenceId); + throw(e); + } + } +} +``` + +The `TransactionDetails` interface: + +**TransactionDetails.java** + +```java +public interface TransactionDetails { + String getSourceAccountId(); + String getDestinationAccountId(); + String getTransactionReferenceId(); + int getAmountToTransfer(); +} +``` + +

Activity Definition

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

Workflow Definition

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

Activity Definition

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

Workflow Definition

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

Activity Definition

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

Workflow Definition

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

Activity Definition

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

Workflow Definition

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

Activity Definition

+ +Activities handle the business logic. Each Activity method calls an external banking service: + +**MoneyTransferWorker/Activities.cs** + +```csharp +public class BankingActivities +{ + [Activity] + public static async Task WithdrawAsync(PaymentDetails details) + { + var bankService = new BankingService("bank1.example.com"); + Console.WriteLine($"Withdrawing ${details.Amount} from account {details.SourceAccount}."); + try + { + return await bankService.WithdrawAsync(details.SourceAccount, details.Amount, details.ReferenceId).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Withdrawal failed", ex); + } + } + + [Activity] + public static async Task DepositAsync(PaymentDetails details) + { + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } + } +} +``` + +
+ +
+ +## Step 2: Set the Retry Policy + +Temporal makes your software durable and fault tolerant by default. If an Activity fails, Temporal automatically retries it, but you can customize this behavior through a Retry Policy. + +### Retry Policy Configuration + +In the `MoneyTransfer` Workflow, you'll see a Retry Policy that controls this behavior: + + + + +**workflows.py** + +```python +# ... +retry_policy = RetryPolicy( + maximum_attempts=3, # Stop after 3 tries + maximum_interval=timedelta(seconds=2), # Don't wait longer than 2s + non_retryable_error_types=[ # Never retry these errors + "InvalidAccountError", + "InsufficientFundsError" + ], +) +``` + + + + + +**workflow.go** + +```go +// ... +// RetryPolicy specifies how to automatically handle retries if an Activity fails. +retrypolicy := &temporal.RetryPolicy{ + InitialInterval: time.Second, // Start with 1 second wait + BackoffCoefficient: 2.0, // Double the wait each time + MaximumInterval: 100 * time.Second, // Don't wait longer than 100s + MaximumAttempts: 500, // Stop after 500 tries (0 = unlimited) + NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"}, // Never retry these errors +} + +options := workflow.ActivityOptions{ + StartToCloseTimeout: time.Minute, + RetryPolicy: retrypolicy, +} + +// Apply the options. +``` + + + + + +**src/main/java/moneytransfer/MoneyTransferWorkflowImpl.java** + +```java +// ... +private static final String WITHDRAW = "Withdraw"; + +// RetryOptions specify how to automatically handle retries when Activities fail +private final RetryOptions retryoptions = RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) // Start with 1 second wait + .setMaximumInterval(Duration.ofSeconds(20)) // Don't wait longer than 20s + .setBackoffCoefficient(2) // Double the wait each time (1s, 2s, 4s, etc) + .setMaximumAttempts(5000) // Stop after 5000 tries + .build(); +``` + + + + + +**src/workflows.ts** + +```typescript +// ... +const { withdraw, deposit, refund } = proxyActivities({ + // RetryPolicy specifies how to automatically handle retries if an Activity fails. + retry: { + initialInterval: '1 second', // Start with 1 second wait + maximumInterval: '1 minute', // Don't wait longer than 1 minute + backoffCoefficient: 2, // Double the wait each time + maximumAttempts: 500, // Stop after 500 tries + nonRetryableErrorTypes: ['InvalidAccountError', 'InsufficientFundsError'], // Never retry these errors + }, + startToCloseTimeout: '1 minute', // Activity must complete within 1 minute +}); +``` + + + + + +**MoneyTransferWorker/Workflow.cs** + +```csharp +// ... +// Retry policy +var retryPolicy = new RetryPolicy +{ + InitialInterval = TimeSpan.FromSeconds(1), // Start with 1 second wait + MaximumInterval = TimeSpan.FromSeconds(100), // Don't wait longer than 100s + BackoffCoefficient = 2, // Double the wait each time + MaximumAttempts = 3, // Stop after 3 tries + NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" } // Never retry these errors +}; +``` + + + + + +**workflow.rb** + +```ruby +# Temporal Workflow that withdraws the specified amount from the source +# account and deposits it into the target account, refunding the source +# account if the deposit cannot be completed. +class MoneyTransferWorkflow < Temporalio::Workflow::Definition + def execute(details) + retry_policy = Temporalio::RetryPolicy.new( + max_interval: 10, # Don't wait longer than 10s + non_retryable_error_types: [ # Never retry these errors + 'InvalidAccountError', + 'InsufficientFundsError' + ] + ) +``` + + + + + +**MoneyTransfer.php** + +```php +// ... +$this->bankingActivity = Workflow::newActivityStub( + Banking::class, + ActivityOptions::new() + ->withStartToCloseTimeout('5 seconds') + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(3) // Stop after 3 tries + ->withMaximumInterval('2 seconds') // Don't wait longer than 2s + ->withNonRetryableExceptions([ // Never retry these errors + InvalidAccount::class, + InsufficientFunds::class + ]), + ), +); +``` + + + + + +### What Makes Errors Non-Retryable? +Without retry policies, a temporary network glitch could cause your entire money transfer to fail. With Temporal's intelligent retries, your workflow becomes resilient to these common infrastructure issues. + + + + +:::important This is a Simplified Example +This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling. +::: + +## Step 3: Create a Worker file + +A Worker is responsible for executing your Workflow and Activity code. It: + +- Can only execute Workflows and Activities registered to it +- Knows which piece of code to execute based on Tasks from the Task Queue +- Only listens to the Task Queue that it's registered to +- Returns execution results back to the Temporal Server + + + + +**run_worker.py** + +```python +import asyncio + +from temporalio.client import Client +from temporalio.worker import Worker + +from activities import BankingActivities +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME +from workflows import MoneyTransfer + + +async def main() -> None: + client: Client = await Client.connect("localhost:7233", namespace="default") + # Run the worker + activities = BankingActivities() + worker: Worker = Worker( + client, + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + workflows=[MoneyTransfer], + activities=[activities.withdraw, activities.deposit, activities.refund], + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +**worker/main.go** + +```go +func main() { + c, err := client.Dial(client.Options{}) + if err != nil { + log.Fatalln("Unable to create Temporal client.", err) + } + defer c.Close() + + w := worker.New(c, app.MoneyTransferTaskQueueName, worker.Options{}) + + // This worker hosts both Workflow and Activity functions. + w.RegisterWorkflow(app.MoneyTransfer) + w.RegisterActivity(app.Withdraw) + w.RegisterActivity(app.Deposit) + w.RegisterActivity(app.Refund) + + // Start listening to the Task Queue. + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("unable to start Worker", err) + } +} +``` + + + + + +**src/main/java/moneytransfer/MoneyTransferWorker.java** + +```java +package moneytransferapp; + +import io.temporal.client.WorkflowClient; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; + +public class MoneyTransferWorker { + public static void main(String[] args) { + // Create a stub that accesses a Temporal Service on the local development machine + WorkflowServiceStubs serviceStub = WorkflowServiceStubs.newLocalServiceStubs(); + + // The Worker uses the Client to communicate with the Temporal Service + WorkflowClient client = WorkflowClient.newInstance(serviceStub); + + // A WorkerFactory creates Workers + WorkerFactory factory = WorkerFactory.newInstance(client); + + // A Worker listens to one Task Queue. + // This Worker processes both Workflows and Activities + Worker worker = factory.newWorker(Shared.MONEY_TRANSFER_TASK_QUEUE); + + // Register a Workflow implementation with this Worker + // The implementation must be known at runtime to dispatch Workflow tasks + // Workflows are stateful so a type is needed to create instances. + worker.registerWorkflowImplementationTypes(MoneyTransferWorkflowImpl.class); + + // Register Activity implementation(s) with this Worker. + // The implementation must be known at runtime to dispatch Activity tasks + // Activities are stateless and thread safe so a shared instance is used. + worker.registerActivitiesImplementations(new AccountActivityImpl()); + + System.out.println("Worker is running and actively polling the Task Queue."); + System.out.println("To quit, use ^C to interrupt."); + + // Start all registered Workers. The Workers will start polling the Task Queue. + factory.start(); + } +} +``` + + + + + +**src/worker.ts** + +```typescript +import { Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { namespace, taskQueueName } from './shared'; + +async function run() { + // Register Workflows and Activities with the Worker and connect to + // the Temporal server. + const worker = await Worker.create({ + workflowsPath: require.resolve('./workflows'), + activities, + namespace, + taskQueue: taskQueueName, + }); + + // Start accepting tasks from the Task Queue. + await worker.run(); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + + +**MoneyTransferWorker/Program.cs** + +```csharp +// This file is designated to run the worker +using Temporalio.Client; +using Temporalio.Worker; +using Temporalio.MoneyTransferProject.MoneyTransferWorker; + +// Create a client to connect to localhost on "default" namespace +var client = await TemporalClient.ConnectAsync(new("localhost:7233")); + +// Cancellation token to shutdown worker on ctrl+c +using var tokenSource = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + tokenSource.Cancel(); + eventArgs.Cancel = true; +}; + +// Create an instance of the activities since we have instance activities. +// If we had all static activities, we could just reference those directly. +var activities = new BankingActivities(); + +// Create a worker with the activity and workflow registered +using var worker = new TemporalWorker( + client, // client + new TemporalWorkerOptions(taskQueue: "MONEY_TRANSFER_TASK_QUEUE") + .AddAllActivities(activities) // Register activities + .AddWorkflow() // Register workflow +); + +// Run the worker until it's cancelled +Console.WriteLine("Running worker..."); +try +{ + await worker.ExecuteAsync(tokenSource.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Worker cancelled"); +} +``` + + + + + +**worker.rb** + +```ruby +require_relative 'activities' +require_relative 'shared' +require_relative 'workflow' +require 'logger' +require 'temporalio/client' +require 'temporalio/worker' + +# Create a Temporal Client that connects to a local Temporal Service, uses +# a Namespace called 'default', and displays log messages to standard output +client = Temporalio::Client.connect( + 'localhost:7233', + 'default', + logger: Logger.new($stdout, level: Logger::INFO) +) + +# Create a Worker that polls the specified Task Queue and can +# fulfill requests for the specified Workflow and Activities +worker = Temporalio::Worker.new( + client:, + task_queue: MoneyTransfer::TASK_QUEUE_NAME, + workflows: [MoneyTransfer::MoneyTransferWorkflow], + activities: [MoneyTransfer::BankActivities::Withdraw, + MoneyTransfer::BankActivities::Deposit, + MoneyTransfer::BankActivities::Refund] +) + +# Start the Worker, which will poll the Task Queue until stopped +puts 'Starting Worker (press Ctrl+C to exit)' +worker.run(shutdown_signals: ['SIGINT']) +``` + + + + + +**Worker.php** + +```php +newWorker('default', logger: $logger); + +# Register Workflows +$worker->registerWorkflowTypes(\App\Workflow\MoneyTransfer::class); + +# Register Activities +$worker->registerActivity(BankingActivity::class, static fn(): BankingActivity => new BankingActivity( + $logger, + new Service('bank-api.example.com'), +)); + +$factory->run(); +``` + + + + + +## Step 4: Define the Task Queue + +A Task Queue is where Temporal Workers look for Tasks about Workflows and Activities to execute. +Each Task Queue is identified by a name, which you will specify when you configure the Worker and again in the code that starts the Workflow Execution. +To ensure that the same name is used in both places, this project follows the recommended practice of specifying the Task Queue name in a constant referenced from both places. + +### Set Your Task Queue Name + +To ensure your Worker and Workflow starter use the same queue, define the Task Queue name as a constant: + + + + +**shared.py** + +```python +# Task Queue name - used by both Worker and Workflow starter +MONEY_TRANSFER_TASK_QUEUE_NAME = "MONEY_TRANSFER_TASK_QUEUE" +``` + + + + + +**app/shared.go** + +```go +package app + +// MoneyTransferTaskQueueName is the task queue name used by both +// the Worker and the Workflow starter +const MoneyTransferTaskQueueName = "MONEY_TRANSFER_TASK_QUEUE" +``` + + + + + +**src/main/java/moneytransfer/Shared.java** + +```java +package moneytransferapp; + +public class Shared { + // Task Queue name used by both Worker and Workflow starter + public static final String MONEY_TRANSFER_TASK_QUEUE = "MONEY_TRANSFER_TASK_QUEUE"; +} +``` + + + + + +**src/shared.ts** + +```typescript +// Task Queue name - used by both Worker and Workflow starter +export const taskQueueName = 'MONEY_TRANSFER_TASK_QUEUE'; +export const namespace = 'default'; +``` + + + + + +**Shared/Constants.cs** + +```csharp +namespace MoneyTransferProject.Shared +{ + public static class Constants + { + // Task Queue name used by both Worker and Workflow starter + public const string MONEY_TRANSFER_TASK_QUEUE = "MONEY_TRANSFER_TASK_QUEUE"; + } +} +``` + + + + + +**shared.rb** + +```ruby +module MoneyTransfer + # Task Queue name used by both Worker and Workflow starter + TASK_QUEUE_NAME = "MONEY_TRANSFER_TASK_QUEUE".freeze +end +``` + + + + + +**shared.php** or in your Worker.php + +```php + + + + +:::tip Why Use Constants? +Using a shared constant prevents typos that would cause your Worker to listen to a different Task Queue than where your Workflow tasks are being sent. It's a common source of "Why isn't my Workflow running?" issues. +::: + +## Step 5: Execute the Workflow + +Now you'll create a client program that starts a Workflow execution. This code connects to the Temporal Service and submits a Workflow execution request: + + + + +**start_workflow.py** + +```python +import asyncio +from temporalio.client import Client +from workflows import MoneyTransfer +from shared import MONEY_TRANSFER_TASK_QUEUE_NAME + +async def main(): + # Create the Temporal Client to connect to the Temporal Service + client = await Client.connect("localhost:7233", namespace="default") + + # Define the money transfer details + details = { + "source_account": "A1001", + "target_account": "B2002", + "amount": 100, + "reference_id": "12345" + } + + # Start the Workflow execution + handle = await client.start_workflow( + MoneyTransfer.run, + details, + id=f"money-transfer-{details['reference_id']}", + task_queue=MONEY_TRANSFER_TASK_QUEUE_NAME, + ) + + print(f"Started Workflow {handle.id}") + print(f"Transferring ${details['amount']} from {details['source_account']} to {details['target_account']}") + + # Wait for the result + result = await handle.result() + print(f"Workflow result: {result}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +**start/main.go** + +```go +func main() { + // Create the client object just once per process + c, err := client.Dial(client.Options{}) + if err != nil { + log.Fatalln("Unable to create Temporal client:", err) + } + defer c.Close() + + input := app.PaymentDetails{ + SourceAccount: "85-150", + TargetAccount: "43-812", + Amount: 250, + ReferenceID: "12345", + } + + options := client.StartWorkflowOptions{ + ID: "pay-invoice-701", + TaskQueue: app.MoneyTransferTaskQueueName, + } + + log.Printf("Starting transfer from account %s to account %s for %d", + input.SourceAccount, input.TargetAccount, input.Amount) + + we, err := c.ExecuteWorkflow(context.Background(), options, app.MoneyTransfer, input) + if err != nil { + log.Fatalln("Unable to start the Workflow:", err) + } + + log.Printf("WorkflowID: %s RunID: %s\n", we.GetID(), we.GetRunID()) + + var result string + err = we.Get(context.Background(), &result) + if err != nil { + log.Fatalln("Unable to get Workflow result:", err) + } + + log.Println(result) +} +``` + + + + + +**src/main/java/moneytransfer/TransferApp.java** + +```java +public class TransferApp { + public static void main(String[] args) throws Exception { + // A WorkflowServiceStubs communicates with the Temporal front-end service. + WorkflowServiceStubs serviceStub = WorkflowServiceStubs.newLocalServiceStubs(); + + // A WorkflowClient wraps the stub and can be used to start, signal, query, cancel, and terminate Workflows. + WorkflowClient client = WorkflowClient.newInstance(serviceStub); + + // Workflow options configure Workflow stubs. + // A WorkflowId prevents duplicate instances. + WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue(Shared.MONEY_TRANSFER_TASK_QUEUE) + .setWorkflowId("money-transfer-workflow") + .build(); + + // WorkflowStubs enable calls to methods as if the Workflow object is local + // but actually perform a gRPC call to the Temporal Service. + MoneyTransferWorkflow workflow = client.newWorkflowStub(MoneyTransferWorkflow.class, options); + + // Configure the details for this money transfer request + String referenceId = UUID.randomUUID().toString().substring(0, 18); + String fromAccount = "A1001"; + String toAccount = "B2002"; + int amountToTransfer = 100; + TransactionDetails transaction = new CoreTransactionDetails(fromAccount, toAccount, referenceId, amountToTransfer); + + // Perform asynchronous execution + WorkflowExecution we = WorkflowClient.start(workflow::transfer, transaction); + + System.out.printf("Initiating transfer of $%d from [Account %s] to [Account %s].\n", + amountToTransfer, fromAccount, toAccount); + System.out.printf("[WorkflowID: %s] [RunID: %s] [Reference: %s]\n", + we.getWorkflowId(), we.getRunId(), referenceId); + } +} +``` + + + + + +**src/client.ts** + +```typescript +import { Connection, Client } from '@temporalio/client'; +import { moneyTransfer } from './workflows'; +import type { PaymentDetails } from './shared'; +import { namespace, taskQueueName } from './shared'; + +async function run() { + const connection = await Connection.connect(); + const client = new Client({ connection, namespace }); + + const details: PaymentDetails = { + amount: 400, + sourceAccount: '85-150', + targetAccount: '43-812', + referenceId: '12345', + }; + + console.log( + `Starting transfer from account ${details.sourceAccount} to account ${details.targetAccount} for $${details.amount}` + ); + + const handle = await client.workflow.start(moneyTransfer, { + args: [details], + taskQueue: taskQueueName, + workflowId: 'pay-invoice-801', + }); + + console.log( + `Started Workflow ${handle.workflowId} with RunID ${handle.firstExecutionRunId}` + ); + console.log(await handle.result()); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + + + + + +**MoneyTransferStarter/Program.cs** + +```csharp +using Temporalio.Client; +using Temporalio.MoneyTransferProject.MoneyTransferWorker; + +// Create a client to connect to localhost on "default" namespace +var client = await TemporalClient.ConnectAsync(new("localhost:7233")); + +// Configure the money transfer details +var transferDetails = new TransferDetails +{ + SourceAccount = "A1001", + TargetAccount = "B2002", + Amount = 100, + ReferenceId = "12345" +}; + +Console.WriteLine($"Starting transfer of ${transferDetails.Amount} from {transferDetails.SourceAccount} to {transferDetails.TargetAccount}"); + +// Start the workflow +var handle = await client.StartWorkflowAsync( + (MoneyTransferWorkflow wf) => wf.RunAsync(transferDetails), + new(id: $"money-transfer-{transferDetails.ReferenceId}", taskQueue: "MONEY_TRANSFER_TASK_QUEUE")); + +Console.WriteLine($"Started workflow {handle.Id}"); + +// Wait for result +var result = await handle.GetResultAsync(); +Console.WriteLine($"Workflow result: {result}"); +``` + + + + + +**starter.rb** + +```ruby +require_relative 'shared' +require_relative 'workflow' +require 'securerandom' +require 'temporalio/client' + +# Create the Temporal Client that connects to the Temporal Service +client = Temporalio::Client.connect('localhost:7233', 'default') + +# Default values for the payment details +details = MoneyTransfer::TransferDetails.new('A1001', 'B2002', 100, SecureRandom.uuid) + +# Use the Temporal Client to submit a Workflow Execution request +handle = client.start_workflow( + MoneyTransfer::MoneyTransferWorkflow, + details, + id: "moneytransfer-#{details.reference_id}", + task_queue: MoneyTransfer::TASK_QUEUE_NAME +) + +puts "Initiated transfer of $#{details.amount} from #{details.source_account} to #{details.target_account}" +puts "Workflow ID: #{handle.id}" + +# Keep running (and retry) if the Temporal Service becomes unavailable +begin + puts "Workflow result: #{handle.result}" +rescue Temporalio::Error::RPCError + puts 'Temporal Service unavailable while awaiting result' + retry +end +``` + + + + + +**transfer.php** + +```php +newWorkflowStub( + MoneyTransfer::class, + WorkflowOptions::new() + ->withWorkflowIdReusePolicy(IdReusePolicy::AllowDuplicate) + ->withWorkflowRunTimeout(20) + ->withWorkflowExecutionTimeout(30), +); + +try { + $result = $workflow->handle($paymentDetails); + echo "\e[32mResult: $result\e[0m\n"; +} catch (WorkflowFailedException $e) { + echo "\e[31mWorkflow failed: {$e->getMessage()}\e[0m\n"; +} catch (\Throwable $e) { + echo "\e[31mError: {$e->getMessage()}\e[0m\n"; +} +``` + + + + + +This code uses a Temporal Client to connect to the Temporal Service, calling its workflow start method to request execution. This returns a handle, and calling result on that handle will block until execution is complete, at which point it provides the result. + +## Run Your Money Transfer + + +
+ Terminal 1 - Start the Temporal server: + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + + temporal server start-dev + + +
+
+ Terminal 2 - Start the Worker: + + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + + ./rr serve + + +
+
+ Terminal 3 - Start the Workflow: + + + python run_workflow.py + + + go run start/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" + + + npm run client + + + dotnet run --project MoneyTransferClient + + + bundle exec ruby starter.rb + + + php src/transfer.php + + +
+
+ Expected Success Output: + + + Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345) + + + Starting transfer from account 85-150 to account 43-812 for 250 +2022/11/14 10:52:20 WorkflowID: pay-invoice-701 RunID: 3312715c-9fea-4dc3-8040-cf8f270eb53c +Transfer complete (transaction IDs: W1779185060, D1779185060) + + + Worker is running and actively polling the Task Queue. +To quit, use ^C to interrupt. + +Withdrawing $62 from account 249946050. +[ReferenceId: 1480a22d-d0fc-4361] +Depositing $62 into account 591856595. +[ReferenceId: 1480a22d-d0fc-4361] +[1480a22d-d0fc-4361] Transaction succeeded. + + + Transfer complete (transaction IDs: W3436600150, D9270097234) + + + Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) + + + Initiated transfer of $100 from A1001 to B2002 +Workflow ID: moneytransfer-2926a650-1aaf-49d9-bf87-0e3a09ef7b32 +Workflow result: Transfer complete (transaction IDs: OKW-100-A1001, OKD-100-B2002) + + + Result: Transfer complete (transaction IDs: W12345, D12345) + + +
+ +}> + +Now that your Worker is running and polling for tasks, you can start a Workflow Execution. + +**In Terminal 3, start the Workflow:** + +The workflow starter script starts a Workflow Execution. Each time you run it, the Temporal Server starts a new Workflow Execution. + + + +
+ +## Check 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. + + + + + +
+ Money Transfer Web UI +
+ +## Ready for Part 2? + + + 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 new file mode 100644 index 0000000000..5d8791357f --- /dev/null +++ b/docs/build-your-first-basic-workflow/failure-simulation.mdx @@ -0,0 +1,1126 @@ +--- +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 Workflows. +hide_table_of_contents: true +keywords: + - temporal + - python + - failure simulation + - crash recovery + - live debugging + - reliability +tags: + - Getting Started + - Tutorial +--- + +import { CallToAction } from "@site/src/components/elements/CallToAction"; +import { TemporalProgress } from "@site/src/components/TemporalProgress"; +import { StatusIndicators } from "@site/src/components/StatusIndicators"; +import { WorkflowDiagram } from "@site/src/components/WorkflowDiagram"; +import { RetryCounter } from "@site/src/components/RetryCounter"; +import { TemporalCheckbox } from "@site/src/components/TemporalCheckbox"; +import SdkTabs from "@site/src/components/elements/SdkTabs"; +import { FaPython, FaJava } from 'react-icons/fa'; +import { SiGo, SiTypescript, SiPhp, SiDotnet, SiRuby } from 'react-icons/si'; + +export const TUTORIAL_LANGUAGE_ORDER = [ + { key: 'py', label: 'Python', icon: FaPython }, + { key: 'go', label: 'Go', icon: SiGo }, + { key: 'java', label: 'Java', icon: FaJava }, + { key: 'ts', label: 'TypeScript', icon: SiTypescript }, + { key: 'php', label: 'PHP', icon: SiPhp }, + { key: 'dotnet', label: '.NET', icon: SiDotnet }, + { key: 'rb', label: 'Ruby', icon: SiRuby }, +]; +import { SetupSteps, SetupStep, CodeSnippet } from "@site/src/components/elements/SetupSteps"; +import { CodeComparison } from "@site/src/components/CodeComparison"; +import { AnimatedTerminal } from "@site/src/components/AnimatedTerminal"; + +export const getTodayDate = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; +}; + +export const getTodayDateISO = () => { + return new Date().toISOString(); +}; + +# Part 2: Simulate Failures +In this part, you'll simulate failures to see how Temporal handles them. +This demonstrates why Temporal is particularly useful for building reliable systems. + + + +Systems fail in unpredictable ways. A seemingly harmless deployment can bring down production, a database connection can time out during peak traffic, or a third-party service can decide to have an outage. +Despite our best efforts with comprehensive testing and monitoring, systems are inherently unpredictable and complex. +Networks fail, servers restart unexpectedly, and dependencies we trust can become unavailable without warning. + +Traditional systems aren't equipped to handle these realities. +When something fails halfway through a multi-step process, you're left with partial state, inconsistent data, and the complex task of figuring out where things went wrong and how to recover. +Most applications either lose progress entirely or require you to build extensive checkpointing and recovery logic. + +In this tutorial, you'll see Temporal's durable execution in action by running two tests: crashing a server while it's working and fixing code problems on the fly without stopping your application. + +## Recover from a server crash + +Unlike other solutions, Temporal is designed with failure in mind. +In this part of the tutorial, you'll simulate a server crash mid-transaction and watch Temporal helps you recover from it. + +**Here's the challenge:** Kill your Worker process while money is being transferred. +In traditional systems, this would corrupt the transaction or lose data entirely. + + + +### Before You Start + + + Worker is currently stopped + + + + You have terminals ready (Terminal 2 for Worker, Terminal 3 for Workflow) + + + + Web UI is open at `http://localhost:8233` + + +
+What's happening behind the scenes? + +Unlike many modern applications that require complex leader election processes and external databases to handle failure, Temporal automatically preserves the state of your Workflow even if the server is down. +You can test this by stopping the Temporal Service while a Workflow Execution is in progress. + +No data is lost once the Temporal Service went offline. +When it comes back online, the work picked up where it left off before the outage. +Keep in mind that this example uses a single instance of the service running on a single machine. +In a production deployment, the Temporal Service can be deployed as a cluster, spread across several machines for higher availability and increased throughput. + +
+ +### Instructions + + + + +
+ Terminal 2 - Worker +
+ + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + + ./rr serve + + + +}> + +### Step 1: Start Your Worker + +First, stop any running Worker (`Ctrl+C`) and start a fresh one in Terminal 2. + + + +
+ + +
+ Terminal 3 - Workflow +
+ + + python run_workflow.py + + + go run start/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.TransferApp" + + + npm run client + + + dotnet run --project MoneyTransferClient + + + bundle exec ruby starter.rb + + + php src/transfer.php + + + +}> + +### Step 2: Start the Workflow + +Now in Terminal 3, start the Workflow. Check the Web UI - you'll see your Worker busy executing the Workflow and its Activities. + + + +
+ + + The Crash Test +

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

+ +} style={{background: 'transparent'}}> + +### Step 3: Simulate the Crash + +**The moment of truth!** Kill your Worker while it's processing the transaction. + +**Jump back to the Web UI** and refresh. Your Workflow is still showing as "Running"! + +That's the magic! The Workflow keeps running because Temporal saved its state, even though we killed the Worker. + + + +
+ + +
+ Terminal 2 - Recovery +
+ + + python run_worker.py + + + go run worker/main.go + + + mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" + + + npm run worker + + + dotnet run --project MoneyTransferWorker + + + bundle exec ruby worker.rb + + + ./rr serve + + + +}> + +### Step 4: Bring Your Worker Back + +Restart your Worker in Terminal 2. Watch Terminal 3 - you'll see the Workflow finish up and show the result! + + + +
+ +
+ + +:::tip **Try This Challenge** + +Try killing the Worker at different points during execution. Start the Workflow, kill the Worker during the withdrawal, then restart it. Kill it during the deposit. Each time, notice how Temporal maintains perfect state consistency. + +Check the Web UI while the Worker is down and you'll see the Workflow is still "Running" even though no code is executing. +::: + +## Recover from an unknown error + +In this part of the tutorial, you will inject a bug into your production code, watch Temporal retry automatically, then fix the bug while the Workflow is still running. +This demo application makes a call to an external service in an Activity. +If that call fails due to a bug in your code, the Activity produces an error. + +To test this out and see how Temporal responds, you'll simulate a bug in the Deposit Activity function or method. + + + +### Before You Start + + + Worker is stopped + + + + Code editor open with the Activities file + + + + Ready to uncomment the failure line + + + + Web UI open to watch the retries + + + +## Instructions + +### Step 1: Stop Your Worker + +Before we can simulate a failure, we need to stop the current Worker process. This allows us to modify the Activity code safely. + +In Terminal 2 (where your Worker is running), stop it with `Ctrl+C`. + +**What's happening?** You're about to modify Activity code to introduce a deliberate failure. The Worker process needs to restart to pick up code changes, but the Workflow execution will continue running in Temporal's service - this separation between execution state and code is a core Temporal concept. + +### Step 2: Introduce the Bug + +Now we'll intentionally introduce a failure in the deposit Activity to simulate real-world scenarios like network timeouts, database connection issues, or external service failures. This demonstrates how Temporal handles partial failures in multi-step processes. + + + + +Find the `deposit()` method and **uncomment the failing line** while **commenting out the working line**: + +**activities.py** +```python +@activity.defn +async def deposit(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-deposit" + try: + # Comment out this working line: + # confirmation = await asyncio.to_thread( + # self.bank.deposit, data.target_account, data.amount, reference_id + # ) + + # Uncomment this failing line: + confirmation = await asyncio.to_thread( + self.bank.deposit_that_fails, + data.target_account, + data.amount, + reference_id, + ) + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Deposit failed") + raise +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `Deposit()` function and **uncomment the failing line** while **commenting out the working line**: + +**activity.go** +```go +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Depositing $%d into account %s.\n\n", + data.Amount, + data.TargetAccount, + ) + + referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + + // Uncomment this failing line: + confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + // Comment out this working line: + // confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + + return confirmation, err +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `deposit()` method and **change `activityShouldSucceed` to `false`**: + +**AccountActivityImpl.java** +```java +public String deposit(PaymentDetails details) { + // Change this to false to simulate failure: + boolean activityShouldSucceed = false; + + // ... rest of your method +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `deposit()` function and **uncomment the failing line** while **commenting out the working line**: + +**activities.ts** +```typescript +export async function deposit(details: PaymentDetails): Promise { + // Comment out this working line: + // return await bank.deposit(details.targetAccount, details.amount, details.referenceId); + + // Uncomment this failing line: + return await bank.depositThatFails(details.targetAccount, details.amount, details.referenceId); +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `DepositAsync()` method and **uncomment the failing line** while **commenting out the working block**: + +**MoneyTransferWorker/Activities.cs** +```csharp +[Activity] +public static async Task DepositAsync(PaymentDetails details) +{ + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + // Uncomment this failing line: + return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); + + // Comment out this working block: + /* + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } + */ +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `deposit` method and **uncomment the failing line** that causes a divide-by-zero error: + +**activities.rb** +```ruby +def deposit(details) + # Uncomment this line to introduce the bug: + result = 100 / 0 # This will cause a divide-by-zero error + + # Your existing deposit logic here... +end +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + + +Find the `deposit()` method in `BankingActivity.php` and **uncomment the failing line** while **commenting out the working line**: + +**BankingActivity.php** + +```php +#[\Override] +public function deposit(PaymentDetails $data): string +{ + $referenceId = $data->referenceId . "-deposit"; + try { + // Comment out this working line: + // $confirmation = $this->bank->deposit( + // $data->targetAccount, + // $data->amount, + // $referenceId, + // ); + + // Uncomment this failing line: + $confirmation = $this->bank->depositThatFails( + $data->targetAccount, + $data->amount, + $referenceId, + ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Deposit failed", ['exception' => $e]); + throw $e; + } +} +``` + +Save your changes. You've now created a deliberate failure point in your deposit Activity. This simulates a real-world scenario where external service calls might fail intermittently. + + + + +### Step 3: Start Worker & Observe Retry Behavior + +Now let's see how Temporal handles this failure. When you start your Worker, it will execute the withdraw Activity successfully, but hit the failing deposit Activity. Instead of the entire Workflow failing permanently, Temporal will retry the failed Activity according to your retry policy. + + + + +```bash +python run_worker.py +``` + +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically + + + + + + + +```bash +go run worker/main.go +``` + +**Here's what you'll see:** +- The `Withdraw()` Activity completes successfully +- The `Deposit()` Activity fails and retries automatically + + + + + + + +Make sure your Workflow is still running in the Web UI, then start your Worker: + +```bash +mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null +mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn +``` + +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically + + + + + + + +```bash +npm run worker +``` + +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically + + + + + + + +```bash +dotnet run --project MoneyTransferWorker +``` + +**Here's what you'll see:** +- The `WithdrawAsync()` Activity completes successfully +- The `DepositAsync()` Activity fails and retries automatically + + + + + + + +```bash +bundle exec ruby worker.rb +``` + +In another terminal, start a new Workflow: + +```bash +bundle exec ruby starter.rb +``` + +**Here's what you'll see:** +- The `withdraw` Activity completes successfully +- The `deposit` Activity fails and retries automatically + + + +Check the Web UI - click on your Workflow to see the failure details and retry attempts. + + + + + +```bash +./rr serve +``` + +**Here's what you'll see:** +- The `withdraw()` Activity completes successfully +- The `deposit()` Activity fails and retries automatically + +Check the Web UI - click on your Workflow to see the failure details and retry attempts. + + + + +**Key observation:** Your Workflow isn't stuck or terminated. Temporal automatically retries the failed Activity according to your configured retry policy, while maintaining the overall Workflow state. The successful withdraw Activity doesn't get re-executed - only the failed deposit Activity is retried. + +### Step 4: Fix the Bug + +Here's where Temporal really shines - you can fix bugs in production code while Workflows are still executing. The Workflow state is preserved in Temporal's durable storage, so you can deploy fixes and let the retry mechanism pick up your corrected code. + + + + +Go back to `activities.py` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activities.py** +```python +@activity.defn +async def deposit(self, data: PaymentDetails) -> str: + reference_id = f"{data.reference_id}-deposit" + try: + # Uncomment this working line: + confirmation = await asyncio.to_thread( + self.bank.deposit, data.target_account, data.amount, reference_id + ) + + # Comment out this failing line: + # confirmation = await asyncio.to_thread( + # self.bank.deposit_that_fails, + # data.target_account, + # data.amount, + # reference_id, + # ) + return confirmation + except InvalidAccountError: + raise + except Exception: + activity.logger.exception("Deposit failed") + raise +``` + + + + + +Go back to `activity.go` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activity.go** +```go +func Deposit(ctx context.Context, data PaymentDetails) (string, error) { + log.Printf("Depositing $%d into account %s.\n\n", + data.Amount, + data.TargetAccount, + ) + + referenceID := fmt.Sprintf("%s-deposit", data.ReferenceID) + bank := BankingService{"bank-api.example.com"} + + // Comment out this failing line: + // confirmation, err := bank.DepositThatFails(data.TargetAccount, data.Amount, referenceID) + // Uncomment this working line: + confirmation, err := bank.Deposit(data.TargetAccount, data.Amount, referenceID) + + return confirmation, err +} +``` + + + + + +Go back to `AccountActivityImpl.java` and **change `activityShouldSucceed` back to `true`**: + +**AccountActivityImpl.java** +```java +public String deposit(PaymentDetails details) { + // Change this back to true to fix the bug: + boolean activityShouldSucceed = true; + + // ... rest of your method +} +``` + + + + + +Go back to `activities.ts` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**activities.ts** +```typescript +export async function deposit(details: PaymentDetails): Promise { + // Uncomment this working line: + return await bank.deposit(details.targetAccount, details.amount, details.referenceId); + + // Comment out this failing line: + // return await bank.depositThatFails(details.targetAccount, details.amount, details.referenceId); +} +``` + + + + + +Go back to `Activities.cs` and **reverse the comments** - comment out the failing line and uncomment the working block: + +**MoneyTransferWorker/Activities.cs** +```csharp +[Activity] +public static async Task DepositAsync(PaymentDetails details) +{ + var bankService = new BankingService("bank2.example.com"); + Console.WriteLine($"Depositing ${details.Amount} into account {details.TargetAccount}."); + + // Comment out this failing line: + // return await bankService.DepositThatFailsAsync(details.TargetAccount, details.Amount, details.ReferenceId); + + // Uncomment this working block: + try + { + return await bankService.DepositAsync(details.TargetAccount, details.Amount, details.ReferenceId); + } + catch (Exception ex) + { + throw new ApplicationFailureException("Deposit failed", ex); + } +} +``` + + + + + +Go back to `activities.rb` and **comment out the failing line**: + +**activities.rb** +```ruby +def deposit(details) + # Comment out this problematic line: + # result = 100 / 0 # This will cause a divide-by-zero error + + # Your existing deposit logic here... +end +``` + + + + + +Go back to `BankingActivity.php` and **reverse the comments** - comment out the failing line and uncomment the working line: + +**BankingActivity.php** + +```php +#[\Override] +public function deposit(PaymentDetails $data): string +{ + $referenceId = $data->referenceId . "-deposit"; + try { + // Uncomment this working line: + $confirmation = $this->bank->deposit( + $data->targetAccount, + $data->amount, + $referenceId, + ); + + // Comment out this failing line: + // $confirmation = $this->bank->depositThatFails( + // $data->targetAccount, + // $data->amount, + // $referenceId, + // ); + return $confirmation; + } catch (InvalidAccount $e) { + throw $e; + } catch (\Throwable $e) { + $this->logger->error("Deposit failed", ['exception' => $e]); + throw $e; + } +} +``` + + + + +Save your changes. You've now restored the working implementation. The key insight here is that you can deploy fixes to Activities while Workflows are still executing - Temporal will pick up your changes on the next retry attempt. + +### Step 5: Restart Worker + +To apply your fix, you need to restart the Worker process so it picks up the code changes. Since the Workflow execution state is stored in Temporal's servers (not in your Worker process), restarting the Worker won't affect the running Workflow. + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +python run_worker.py +``` + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction in Terminal 3: + +``` +Transfer complete. +Withdraw: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} +Deposit: {'amount': 250, 'receiver': '43-812', 'reference_id': '1f35f7c6-4376-4fb8-881a-569dfd64d472', 'sender': '85-150'} +``` + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +go run worker/main.go +``` + +**On the next retry attempt,** your fixed `Deposit()` Activity will succeed, and you'll see the completed transaction in your starter terminal: + +``` +Transfer complete (transaction IDs: W1779185060, D1779185060) +``` + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +mvn clean install -Dorg.slf4j.simpleLogger.defaultLogLevel=info 2>/dev/null +mvn compile exec:java -Dexec.mainClass="moneytransferapp.MoneyTransferWorker" -Dorg.slf4j.simpleLogger.defaultLogLevel=warn +``` + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed: + +``` +Depositing $32 into account 872878204. +[ReferenceId: d3d9bcf0-a897-4326] +[d3d9bcf0-a897-4326] Transaction succeeded. +``` + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +npm run worker +``` + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction in your client terminal: + +``` +Transfer complete (transaction IDs: W3436600150, D9270097234) +``` + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +dotnet run --project MoneyTransferWorker +``` + +**On the next retry attempt,** your fixed `DepositAsync()` Activity will succeed, and you'll see the completed transaction in your client terminal: + +``` +Workflow result: Transfer complete (transaction IDs: W-caa90e06-3a48-406d-86ff-e3e958a280f8, D-1910468b-5951-4f1d-ab51-75da5bba230b) +``` + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +bundle exec ruby worker.rb +``` + +**On the next retry attempt,** your fixed `deposit` Activity will succeed, and you'll see the Workflow complete successfully. + + + + + +```bash +# Stop the current Worker +Ctrl+C + +# Start it again with the fix +./rr serve +``` + +**On the next retry attempt,** your fixed `deposit()` Activity will succeed, and you'll see the completed transaction: + +``` +Result: Transfer complete (transaction IDs: W12345, D12345) +``` + + + + +Check the Web UI - your Workflow shows as completed. You've just demonstrated Temporal's key differentiator: the ability to fix production bugs in running applications without losing transaction state or progress. This is possible because Temporal stores execution state separately from your application code. + +**Mission Accomplished.** You have just fixed a bug in a running application without losing the state of the Workflow or restarting the transaction. + + + + + + +:::tip Advanced Challenge + +Try this advanced scenario of compensating transactions. + + +1. **Modify the retry policy** in `workflows.py` to only retry 1 time +2. **Force the deposit to fail permanently** +3. **Watch the automatic refund** execute + +::: + + +## Knowledge Check + +Test your understanding of what you just experienced: + +
+Q: What are four of Temporal's value propositions that you learned about in this tutorial? + +**Answer**: +1. Temporal automatically maintains the state of your Workflow, despite crashes or even outages of the Temporal Service itself. +2. Temporal's built-in support for retries and timeouts enables your code to overcome transient and intermittent failures. +3. Temporal provides full visibility in the state of the Workflow Execution and its Web UI offers a convenient way to see the details of both current and past executions. +4. Temporal makes it possible to fix a bug in a Workflow Execution that you've already started. After updating the code and restarting the Worker, the failing Activity is retried using the code containing the bug fix, completes successfully, and execution continues with what comes next. +
+ +
+Q: Why do we use a shared constant for the Task Queue name? + +**Answer**: Because the Task Queue name is specified in two different parts of the code (the first starts the Workflow and the second configures the Worker). If their values differ, the Worker and Temporal Service would not share the same Task Queue, and the Workflow Execution would not progress. + + +
+ +
+Q: What do you have to do if you make changes to Activity code for a Workflow that is running? + +**Answer**: Restart the Worker. + + +
+ +## Continue Your Learning + +
+
+ +
+
+ +
+
+ +
+
diff --git a/docs/build-your-first-basic-workflow/index.mdx b/docs/build-your-first-basic-workflow/index.mdx new file mode 100644 index 0000000000..669db50ea4 --- /dev/null +++ b/docs/build-your-first-basic-workflow/index.mdx @@ -0,0 +1,37 @@ +--- +id: index +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. +hide_table_of_contents: true +keywords: + - temporal + - workflow + - getting started + - tutorial + - money transfer +tags: + - Getting Started + - Tutorial +--- + +# Build your First Basic Workflow + +Build a money transfer app that survives crashes and handles failures gracefully. + +import QuickstartCards from "@site/src/components/QuickstartCards"; + + diff --git a/sidebars.js b/sidebars.js index e44d82525a..2c5a540ad0 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1,11 +1,40 @@ module.exports = { documentation: [ "index", - "quickstarts", { - type: "link", - label: "Courses and Tutorials", - href: "https://learn.temporal.io/", + type: "category", + label: "Getting Started", + collapsed: false, + items: [ + { + 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: "category", @@ -598,7 +627,7 @@ module.exports = { "references/server-options", "references/web-ui-configuration", "references/web-ui-environment-variables", - + ], }, { @@ -613,7 +642,7 @@ module.exports = { "troubleshooting/blob-size-limit-error", "troubleshooting/deadline-exceeded-error", "troubleshooting/last-connection-error", - "troubleshooting/performance-bottlenecks" + "troubleshooting/performance-bottlenecks" ], }, { @@ -642,37 +671,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", diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.js b/src/components/AnimatedTerminal/AnimatedTerminal.js new file mode 100644 index 0000000000..c69ad7b30d --- /dev/null +++ b/src/components/AnimatedTerminal/AnimatedTerminal.js @@ -0,0 +1,123 @@ +import React, { useState, useEffect, useRef } from 'react'; +import styles from './AnimatedTerminal.module.css'; + +export const AnimatedTerminal = ({ + lines = [], + delay = 1000, + typingSpeed = 50, + prompt = "$", + autoStart = true, + startOnVisible = false, + loop = false, + restartDelay = 2000 +}) => { + const [displayedLines, setDisplayedLines] = useState([]); + const [currentLineIndex, setCurrentLineIndex] = useState(0); + const [isTyping, setIsTyping] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + const [shouldStart, setShouldStart] = useState(!startOnVisible); + const terminalRef = useRef(null); + + // Intersection Observer for scroll-triggered start + useEffect(() => { + if (!startOnVisible || shouldStart) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setShouldStart(true); + } + }); + }, + { + threshold: 0.3, // Trigger when 30% of the component is visible + rootMargin: '0px 0px -100px 0px' // Start slightly before it's fully visible + } + ); + + if (terminalRef.current) { + observer.observe(terminalRef.current); + } + + return () => { + observer.disconnect(); + }; + }, [startOnVisible, shouldStart]); + + useEffect(() => { + if (!shouldStart || !autoStart || lines.length === 0) return; + + const showNextLine = () => { + if (currentLineIndex < lines.length) { + setIsTyping(true); + + // Simulate typing effect for current line + const currentLine = lines[currentLineIndex]; + let charIndex = 0; + const typingInterval = setInterval(() => { + if (charIndex <= currentLine.length) { + setDisplayedLines(prev => { + const newLines = [...prev]; + newLines[currentLineIndex] = currentLine.slice(0, charIndex); + return newLines; + }); + charIndex++; + } else { + clearInterval(typingInterval); + setIsTyping(false); + setCurrentLineIndex(prev => prev + 1); + } + }, typingSpeed); + + return () => clearInterval(typingInterval); + } else if (loop && currentLineIndex === lines.length) { + // Animation complete, restart if looping + setIsRestarting(true); + const restartTimer = setTimeout(() => { + setDisplayedLines([]); + setCurrentLineIndex(0); + setIsRestarting(false); + }, restartDelay); + + return () => clearTimeout(restartTimer); + } + }; + + const timer = setTimeout(() => { + showNextLine(); + }, currentLineIndex === 0 ? 0 : delay); + + return () => clearTimeout(timer); + }, [currentLineIndex, lines, delay, typingSpeed, autoStart, loop, restartDelay, shouldStart]); + + return ( +
+
+
+ + + +
+
Terminal
+
+
+ {displayedLines.map((line, index) => ( +
+ {prompt} + {line} + {index === currentLineIndex - 1 && isTyping && ( + | + )} +
+ ))} + {(currentLineIndex < lines.length && !isTyping && !isRestarting) && ( +
+ {prompt} + | +
+ )} +
+
+ ); +}; diff --git a/src/components/AnimatedTerminal/AnimatedTerminal.module.css b/src/components/AnimatedTerminal/AnimatedTerminal.module.css new file mode 100644 index 0000000000..80683a5d63 --- /dev/null +++ b/src/components/AnimatedTerminal/AnimatedTerminal.module.css @@ -0,0 +1,112 @@ +.terminal { + background: #1e1e1e; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin: 1.5rem 0; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Liberation Mono', monospace; + font-size: 14px; + overflow: hidden; +} + +.terminalHeader { + background: #323232; + padding: 8px 16px; + display: flex; + align-items: center; + border-bottom: 1px solid #444; +} + +.terminalButtons { + display: flex; + gap: 8px; + margin-right: 16px; +} + +.terminalButtons span { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.close { + background: #ff5f57; +} + +.minimize { + background: #ffbd2e; +} + +.maximize { + background: #28ca42; +} + +.terminalTitle { + color: #ccc; + font-size: 12px; + font-weight: 500; +} + +.terminalBody { + padding: 16px; + min-height: 120px; + background: #1e1e1e; + color: #f8f8f2; +} + +.terminalLine { + display: flex; + align-items: center; + margin: 4px 0; + line-height: 1.4; +} + +.prompt { + color: #50fa7b; + margin-right: 8px; + user-select: none; +} + +.command { + color: #f8f8f2; + white-space: pre-wrap; +} + +.cursor { + color: #f8f8f2; + animation: blink 1s infinite; + margin-left: 2px; +} + +@keyframes blink { + + 0%, + 50% { + opacity: 1; + } + + 51%, + 100% { + opacity: 0; + } +} + +/* Different colors for different types of output */ +.terminalLine:has(.command:contains("ERROR")) .command, +.terminalLine .command:contains("ERROR") { + color: #ff5555; +} + +.terminalLine:has(.command:contains("Withdrawing")) .command, +.terminalLine .command:contains("Withdrawing") { + color: #8be9fd; +} + +.terminalLine:has(.command:contains("Depositing")) .command, +.terminalLine .command:contains("Depositing") { + color: #ffb86c; +} + +.terminalLine:has(.command:contains("Running")) .command, +.terminalLine .command:contains("Running") { + color: #50fa7b; +} \ No newline at end of file diff --git a/src/components/AnimatedTerminal/index.js b/src/components/AnimatedTerminal/index.js new file mode 100644 index 0000000000..6e8daa97de --- /dev/null +++ b/src/components/AnimatedTerminal/index.js @@ -0,0 +1 @@ +export { AnimatedTerminal } from './AnimatedTerminal'; diff --git a/src/components/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/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/RetryPolicyComparison/RetryPolicyComparison.js b/src/components/RetryPolicyComparison/RetryPolicyComparison.js new file mode 100644 index 0000000000..268ab414b7 --- /dev/null +++ b/src/components/RetryPolicyComparison/RetryPolicyComparison.js @@ -0,0 +1,34 @@ +import React from 'react'; +import styles from './RetryPolicyComparison.module.css'; + +export const RetryPolicyComparison = () => { + return ( +
+
+
+

Don't Retry

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

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

+
+
+
+
+

Retry Automatically

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

+ These are temporary issues that often resolve themselves. +

+
+
+
+ ); +}; diff --git a/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css b/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css new file mode 100644 index 0000000000..1e0ca56cf9 --- /dev/null +++ b/src/components/RetryPolicyComparison/RetryPolicyComparison.module.css @@ -0,0 +1,87 @@ +.comparisonContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-top: 1.5rem; + margin-bottom: 2rem; +} + +.errorType { + display: flex; +} + +.errorBox { + padding: 1rem; + border-radius: 8px; + border: 2px solid; + flex: 1; +} + +.noRetry { + border-color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} + +.doRetry { + border-color: #22c55e; + background: rgba(34, 197, 94, 0.1); +} + +.errorTitle { + margin: 0 0 0.5rem 0; + font-size: 1.1rem; +} + +.noRetry .errorTitle { + color: #ef4444; +} + +.doRetry .errorTitle { + color: #22c55e; +} + +.errorList { + margin: 0; + padding-left: 1.2rem; +} + +.errorList li { + margin-bottom: 0.25rem; +} + +.errorDescription { + font-size: 0.9rem; + margin: 0.5rem 0 0; + opacity: 0.8; +} + +/* Dark mode adjustments */ +:global([data-theme='dark']) .noRetry { + background: rgba(239, 68, 68, 0.15); + border-color: #f87171; +} + +:global([data-theme='dark']) .doRetry { + background: rgba(34, 197, 94, 0.15); + border-color: #4ade80; +} + +:global([data-theme='dark']) .noRetry .errorTitle { + color: #f87171; +} + +:global([data-theme='dark']) .doRetry .errorTitle { + color: #4ade80; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .comparisonContainer { + grid-template-columns: 1fr; + gap: 1rem; + } + + .errorBox { + padding: 0.75rem; + } +} \ No newline at end of file diff --git a/src/components/RetryPolicyComparison/index.js b/src/components/RetryPolicyComparison/index.js new file mode 100644 index 0000000000..6eb9a9d405 --- /dev/null +++ b/src/components/RetryPolicyComparison/index.js @@ -0,0 +1 @@ +export { RetryPolicyComparison } from './RetryPolicyComparison'; 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..0b01588b0e --- /dev/null +++ b/src/components/TemporalCheckbox/TemporalCheckbox.module.css @@ -0,0 +1,45 @@ +.temporalCheckbox { + 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; + position: relative; + cursor: pointer; + margin: 0; + flex-shrink: 0; + margin-top: 2px; + /* Align with first line of text */ +} + +.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; + line-height: 1.5; + margin: 0; +} \ No newline at end of file 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/TutorialNavigation/TutorialNavigation.js b/src/components/TutorialNavigation/TutorialNavigation.js new file mode 100644 index 0000000000..2d8eaa3629 --- /dev/null +++ b/src/components/TutorialNavigation/TutorialNavigation.js @@ -0,0 +1,14 @@ +import React from 'react'; +import styles from './TutorialNavigation.module.css'; + +export const NextButton = ({ href, children, description }) => ( + +
+
{children}
+ {description &&
{description}
} +
+ + + +
+); diff --git a/src/components/TutorialNavigation/TutorialNavigation.module.css b/src/components/TutorialNavigation/TutorialNavigation.module.css new file mode 100644 index 0000000000..4ee3738c18 --- /dev/null +++ b/src/components/TutorialNavigation/TutorialNavigation.module.css @@ -0,0 +1,60 @@ +.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; +} + + +/* Mobile responsive */ +@media (max-width: 768px) { + .nextButton { + padding: 1rem 1.5rem; + } + + .nextTitle { + font-size: 1rem; + } + + .nextDescription { + font-size: 0.8rem; + } +} \ 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..0f0a02f89b --- /dev/null +++ b/src/components/TutorialNavigation/index.js @@ -0,0 +1 @@ +export { NextButton } from './TutorialNavigation'; diff --git a/src/components/WorkflowDiagram/WorkflowDiagram.js b/src/components/WorkflowDiagram/WorkflowDiagram.js new file mode 100644 index 0000000000..ffe6871720 --- /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, style }) => { + 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/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/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] || (
diff --git a/src/components/elements/SetupSteps.js b/src/components/elements/SetupSteps.js index cfdf16d55d..7745afb4c6 100644 --- a/src/components/elements/SetupSteps.js +++ b/src/components/elements/SetupSteps.js @@ -1,32 +1,32 @@ import React from 'react'; - import styles from './setup-steps.module.css'; - import CodeBlock from '@theme/CodeBlock'; +import styles from './setup-steps.module.css'; +import CodeBlock from '@theme/CodeBlock'; - export const SetupStep = ({ children, code }) => { - return ( -
-
- {children} -
-
- {code} -
+export const SetupStep = ({ children, code, style }) => { + return ( +
+
+ {children} +
+
+ {code}
- ); - }; +
+ ); +}; - export const CodeSnippet = ({ language, children }) => { - return ( - - {children} - - ); - }; +export const CodeSnippet = ({ language, children }) => { + return ( + + {children} + + ); +}; - export const SetupSteps = ({ children }) => { - return ( -
- {children} -
- ); - }; \ No newline at end of file +export const SetupSteps = ({ children }) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file 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/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/index.js b/src/components/icons/index.js new file mode 100644 index 0000000000..cc785cd90e --- /dev/null +++ b/src/components/icons/index.js @@ -0,0 +1 @@ +export { CheckIcon } from './CheckIcon'; diff --git a/static/img/moneytransfer/money-withdrawal.png b/static/img/moneytransfer/money-withdrawal.png new file mode 100644 index 0000000000..07bfa08800 Binary files /dev/null and b/static/img/moneytransfer/money-withdrawal.png differ diff --git a/static/img/moneytransfer/webuisample.png b/static/img/moneytransfer/webuisample.png new file mode 100644 index 0000000000..1725ae0852 Binary files /dev/null and b/static/img/moneytransfer/webuisample.png differ diff --git a/static/img/moneytransfer/yourapplication.png b/static/img/moneytransfer/yourapplication.png new file mode 100644 index 0000000000..75f57ddec5 Binary files /dev/null and b/static/img/moneytransfer/yourapplication.png differ 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 0000000000..af1dc434d2 Binary files /dev/null and b/static/img/sdks/sdk-box-logos/ruby.png differ 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 @@ + + + +