Skip to main content

Build Your First Workflow

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.

Introduction

Prerequisites

Before you begin, set up your local development environment:

Quickstart Guide

Run through the Quickstart to get your set up complete.

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 FlowMoney 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.

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-go
cd money-transfer-project-template-go
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.

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.
Temporal Application Components
Your Temporal Application
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. Start a Workflow Execution through the Temporal Service: After the Worker communicates with the Temporal Service, the Worker will begin executing the Workflow and Activity code. It reports the results to the Temporal Service, which tracks the progress of the Workflow Execution.

info

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

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

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

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

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
}

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:

workflow.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.

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.

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.

This is a Simplified Example

This tutorial shows core Temporal features and is not intended for production use.

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

worker/main.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)
}
}

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:

app/shared.go

package app

// MoneyTransferTaskQueueName is the task queue name used by both
// the Worker and the Workflow starter
const MoneyTransferTaskQueueName = "MONEY_TRANSFER_TASK_QUEUE"
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/main.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)
}

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

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.

Workflow Status: EXECUTING
Withdraw Activity: RUNNING
Deposit Activity: RUNNING
Transaction: COMPLETED
Terminal 1 - Start the Temporal server:
temporal server start-dev
Terminal 2 - Start the Worker:
go run worker/main.go
Terminal 3 - Start the Workflow:
go run start/main.go
Expected Success Output:
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)

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

Simulate crashes and fix bugs in running Workflows