Run your first Temporal application with the Python SDK
You can think of Temporal as a sort of "cure-all" for the pains you experience as a developer when trying to build reliable applications. Whether you're writing a complex transaction-based Workflow or working with remote APIs, you know that creating reliable applications is a complex process.
Introduction
In this tutorial, you'll build and run your first Temporal Application. You'll learn how to construct Workflows and Activities, understand the core building blocks, and see you can get full visibility into its execution.
Prerequisites
Before starting this tutorial:
- Set up a local development environment for developing Temporal Applications using the Python programming language
- Ensure you have Git installed to clone the project
Quickstart Guide
Run through the Quickstart to get your set up complete.
What You'll Build
You'll construct a money transfer application from the ground up, learning to design and implement essential transactions such as withdrawals, deposits, and refunds using Temporal's powerful building blocks.
Why This Application? It demonstrates real-world complexity while teaching you how to build reliable, fault-tolerant systems that handle money safely.

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.
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.
git clone https://github.com/temporalio/money-transfer-project-template-python cd money-transfer-project-template-python
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.
Let's Recap: Temporal's Application Structure
The Temporal Application will consist of the following pieces:
- A Workflow written in Python using the Python SDK. A Workflow defines the overall flow of the application.
- An Activity is a method that encapsulates business logic prone to failure (e.g., calling a service that may go down). These Activities can be automatically retried upon some failure. They handle individual tasks like withdraw(), deposit(), and refund().
- A Worker, provided by the Temporal SDK, which runs your Workflow and Activities reliably and consistently.

None of your application code runs on the Temporal Server. Your Worker, Workflow, and Activity run on your infrastructure, along with the rest of your applications.
Build your Workflow and Activities
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
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
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 .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
namespace Temporalio.MoneyTransferProject.MoneyTransferWorker;
using Temporalio.MoneyTransferProject.BankingService.Exceptions;
using Temporalio.Workflows;
using Temporalio.Common;
using Temporalio.Exceptions;
[Workflow]
public class MoneyTransferWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(PaymentDetails details)
{
// Retry policy
var retryPolicy = new RetryPolicy
{
InitialInterval = TimeSpan.FromSeconds(1),
MaximumInterval = TimeSpan.FromSeconds(100),
BackoffCoefficient = 2,
MaximumAttempts = 3,
NonRetryableErrorTypes = new[] { "InvalidAccountException", "InsufficientFundsException" }
};
string withdrawResult;
try
{
withdrawResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.WithdrawAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
}
catch (ApplicationFailureException ex) when (ex.ErrorType == "InsufficientFundsException")
{
throw new ApplicationFailureException("Withdrawal failed due to insufficient funds.", ex);
}
string depositResult;
try
{
depositResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.DepositAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
// If everything succeeds, return transfer complete
return $"Transfer complete (transaction IDs: {withdrawResult}, {depositResult})";
}
catch (Exception depositEx)
{
try
{
// if the deposit fails, attempt to refund the withdrawal
string refundResult = await Workflow.ExecuteActivityAsync(
() => BankingActivities.RefundAsync(details),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5), RetryPolicy = retryPolicy }
);
// If refund is successful, but deposit failed
throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money returned to {details.SourceAccount}.", depositEx);
}
catch (Exception refundEx)
{
// If both deposit and refund fail
throw new ApplicationFailureException($"Failed to deposit money into account {details.TargetAccount}. Money could not be returned to {details.SourceAccount}. Cause: {refundEx.Message}", refundEx);
}
}
}
}
Activity Definition
Activities handle the business logic. Each activity method calls an external banking service:
MoneyTransferWorker/Activities.cs
namespace Temporalio.MoneyTransferProject.MoneyTransferWorker;
using Temporalio.Activities;
using Temporalio.Exceptions;
public class BankingActivities
{
[Activity]
public static async Task<string> 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);
}
}
}
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.
In the MoneyTransfer Workflow, you'll see a Retry Policy that retries failed Activities up to 3 times, with specific errors that shouldn't be retried:
retry_policy = RetryPolicy(
maximum_attempts=3,
maximum_interval=timedelta(seconds=2),
non_retryable_error_types=["InvalidAccountError", "InsufficientFundsError"],
)
This tutorial shows core Temporal features and is not intended for production use. A real money transfer system would need additional logic for edge cases, cancellations, and error handling.
Run Your Money Transfer
Now that your Worker is running and polling for tasks, you can start a Workflow Execution.
In Terminal 3, start the Workflow:
The run_workflow.py script starts a Workflow Execution. Each time you run this file, the Temporal Server starts a new Workflow Execution.
temporal server start-dev
python run_worker.py
python run_workflow.py
Result: Transfer complete (transaction IDs: Withdrew $250 from account 85-150. ReferenceId: 12345, Deposited $250 into account 43-812. ReferenceId: 12345)
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.

Ready for Part 2?
Continue to Part 2: Simulate Failures