Skip to main content

Run your first Temporal application with the Go SDK

Tutorial information
  • Level: ⭐ Temporal beginner
  • Time: ⏱️ ~20 minutes
  • Goals: 🙌
    • Complete several runs of a Temporal Workflow application using the Temporal server and the Go SDK.
    • Practice reviewing the state of the Workflow.
    • Understand the inherent reliability of Workflow functions.
    • Learn many of Temporal's core terminology and concepts.

Introduction

The Temporal server and a language specific SDK, in this case the Go SDK, provide a comprehensive solution to the complexities which arise from modern application development. 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. Temporal provides reliability primitives right out of the box, such as seamless and fault tolerant application state tracking, automatic retries, timeouts, rollbacks due to process failures, and more.

In this tutorial you'll run your first Temporal Workflow application and forever change the way you approach application development.

Prerequisites

Before starting this tutorial:

Project setup

This tutorial uses a fully working template application which can be downloaded as a zip or converted to a new repository in your own Github account and cloned. Github's "Creating a Repository from a Template" guide will walk you through the steps.

  • To use the Github project, execute these commands in a new Terminal window:

    git clone https://github.com/temporalio/money-transfer-project-template-go
    cd money-transfer-project-template-go
  • Zip download

If you convert the template to a new repository, make sure you use the same repository name in your own Github account so that you don't have to make changes to the Go module name when you clone it. Once you have it, open your terminal in the project's root directory and you are ready to go.

Application overview

This project template mimics a "money transfer" application that has a single Workflow function which orchestrates the execution of Withdraw() and Deposit() functions, representing a transfer of money from one account to another. Temporal calls these particular functions Activity functions.

To run the application you will do the following:

  1. Send a signal to the Temporal server to start the money transfer. The Temporal server will track the progress of your Workflow function execution.
  2. Run a Worker. A Worker is a wrapper around your compiled Workflow and Activity code. A Worker's only job is to execute the Activity and Workflow functions and communicate the results back to the Temporal server.

Here's a high-level illustration of what's happening:

High level project design

The Workflow function

The Workflow function is the application entry point. This is what our money transfer Workflow looks like:

workflow.go

func TransferMoney(ctx workflow.Context, transferDetails TransferDetails) error {
// RetryPolicy specifies how to automatically handle retries if an Activity fails.
retrypolicy := &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute,
MaximumAttempts: 500,
}
options := workflow.ActivityOptions{
// Timeout options specify when to automatically timeout Activity functions.
StartToCloseTimeout: time.Minute,
// Optionally provide a customized RetryPolicy.
// Temporal retries failures by default, this is just an example.
RetryPolicy: retrypolicy,
}
ctx = workflow.WithActivityOptions(ctx, options)
err := workflow.ExecuteActivity(ctx, Withdraw, transferDetails).Get(ctx, nil)
if err != nil {
return err
}
err = workflow.ExecuteActivity(ctx, Deposit, transferDetails).Get(ctx, nil)
if err != nil {
return err
}
return nil
}

When you "start" a Workflow you are basically telling the Temporal server, "track the state of the Workflow with this function signature". Workers will execute the Workflow code below, piece by piece, relaying the execution events and results back to the server.

Initiate transfer

There are two ways to start a Workflow with Temporal, either via the SDK or via the CLI. For this tutorial we used the SDK to start the Workflow, which is how most Workflows get started in a live environment. The call to the Temporal server can be done synchronously or asynchronously. Here we do it asynchronously, so you will see the program run, tell you the transaction is processing, and exit.

start/main.go

func main() {
// Create the client object just once per process
c, err := client.NewClient(client.Options{})
if err != nil {
log.Fatalln("unable to create Temporal client", err)
}
defer c.Close()
options := client.StartWorkflowOptions{
ID: "transfer-money-workflow",
TaskQueue: app.TransferMoneyTaskQueue,
}
transferDetails := app.TransferDetails{
Amount: 54.99,
FromAccount: "001-001",
ToAccount: "002-002",
ReferenceID: uuid.New().String(),
}
we, err := c.ExecuteWorkflow(context.Background(), options, app.TransferMoney, transferDetails)
if err != nil {
log.Fatalln("error starting TransferMoney workflow", err)
}
printResults(transferDetails, we.GetID(), we.GetRunID())
}

Run the Workflow

Make sure the Temporal server is running in a terminal, and then run start/main.go from the project root using the following command:

go run start/main.go

If this is your first time running this application, Go may download some dependencies initially, but eventually you will get some feedback that looks like this:

2022/05/18 12:06:33 INFO  No logger configured for temporal client. Created default one.
2022/05/18 12:06:33
Transfer of $54.990002 from account 001-001 to account 002-002 is processing. ReferenceID: 3dc8a0fc-2101-4cb9-bd7f-9943f9a97ebd
2022/05/18 12:06:33
WorkflowID: transfer-money-workflow RunID: 345e9d58-1779-4694-a405-dfe370f0e437

State visibility

OK, now it's time to check out one of the really cool value propositions offered by Temporal: application state visibility. Visit the Temporal Web UI where you will see your Workflow listed.

The workflow running

Next, click the ID for your Workflow. Now you can see everything you want to know about the execution of the Workflow code we told the server to track, such as what parameter values it was given, timeout configurations, scheduled retries, number of attempts, stack traceable errors, and more.

The details of the run.

It seems that our Workflow is "running", but why hasn't the Workflow and Activity code executed yet? Scroll down to the Stack Trace section and you'll see that there are no Workers polling the Task Queue:

There are no workers.

You need at least one worker running in order to execute your workflows.

The Worker

It's time to start the Worker. A Worker is responsible for executing pieces of Workflow and Activity code.

  • It can only execute code that has been registered to it.
  • It knows which piece of code to execute from Tasks that it gets from the Task Queue.
  • It only listens to the Task Queue that it is registered to.

After The Worker executes code, it returns the results back to the Temporal Server. Note that the Worker listens to the same Task Queue that the Workflow and Activity Tasks are sent to. This is called "Task routing", and is a built-in mechanism for load balancing.

worker/main.go

func main() {
// Create the client object just once per process
c, err := client.NewClient(client.Options{})
if err != nil {
log.Fatalln("unable to create Temporal client", err)
}
defer c.Close()
// This worker hosts both Workflow and Activity functions
w := worker.New(c, app.TransferMoneyTaskQueue, worker.Options{})
w.RegisterWorkflow(app.TransferMoney)
w.RegisterActivity(app.Withdraw)
w.RegisterActivity(app.Deposit)
// Start listening to the Task Queue
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("unable to start Worker", err)
}
}

Task Queues are defined by a simple string name:

shared.go

const TransferMoneyTaskQueue = "TRANSFER_MONEY_TASK_QUEUE"

Run the Worker

Run worker/main.go from the project root using the following command:

go run worker/main.go

When you start the Worker it begins polling the Task Queue.

The terminal output will look like this:

2022/05/18 12:10:07 INFO  No logger configured for temporal client. Created default one.
2022/05/18 12:10:07 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 87816@temporal.local@
2022/05/18 12:10:07 DEBUG ExecuteActivity Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 87816@temporal.local@ WorkflowType TransferMoney WorkflowID transfer-money-workflow RunID 345e9d58-1779-4694-a405-dfe370f0e437 Attempt 1 ActivityID 5 ActivityType Withdraw

Withdrawing $54.990002 from account 001-001. ReferenceId: 3dc8a0fc-2101-4cb9-bd7f-9943f9a97ebd
2022/05/18 12:10:07 DEBUG ExecuteActivity Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 87816@temporal.local@ WorkflowType TransferMoney WorkflowID transfer-money-workflow RunID 345e9d58-1779-4694-a405-dfe370f0e437 Attempt 1 ActivityID 11 ActivityType Deposit

Depositing $54.990002 into account 002-002. ReferenceId: 3dc8a0fc-2101-4cb9-bd7f-9943f9a97ebd

If you check the Temporal Web UI again, you will see one Worker registered where previously there was none, and the Workflow status will show that its completed:

There is now one worker and the Workflow is complete

What actually happens under the hood
  • The first Task the Worker finds is the one that tells it to execute the Workflow function.
  • The Worker communicates the event back to the server.
  • This causes the server to send Activity Tasks to the Task Queue.
  • The Worker then grabs each of the Activity Tasks in their respective order from the Task Queue and executes each of the corresponding Activities.

Each of these are History Events that can be audited in Temporal Web (under the History tab next to Summary). Once a workflow is completed and closed, the full history will persist for a set retention period (typically 7-30 days) before being deleted. You can set up the Archival feature to send them to long term storage for compliance/audit needs.

Celebratory confetti

You've just ran a Temporal Workflow application!

Failure simulation

You just got a taste of one of Temporal's amazing value propositions: visibility into the Workflow and the status of the Workers executing the code. Let's explore another key value proposition, maintaining the state of a Workflow, even in the face of failures. To demonstrate this you will simulate some failures for your Workflow. Make sure your Worker is stopped before proceeding by pressing CTRL+C.

Recover from a server crash

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 easily test this by following these steps (again, make sure your Worker is stopped so your Workflow doesn't finish):

  1. Start the Workflow again with go run starter/main.go.
  2. Verify the Workflow is running in the UI.
  3. Shut down the Temporal server by either using CTRL+C in the terminal window running the server or via the Docker dashboard.
  4. After the Temporal server has stopped, restart it and visit the UI.

Your Workflow is still listed:

The second workflow is listed

Recover from an Activity error

Next you'll simulate a bug in the Deposit() Activity function. Let your Workflow continue to run but don't start the Worker yet.

Open the activity.go file and switch out the comments on the return statements such that the Deposit() function returns an error.

activity.go

func Deposit(ctx context.Context, transferDetails TransferDetails) error {
fmt.Printf(
"\nDepositing $%f into account %s. ReferenceId: %s\n",
transferDetails.Amount,
transferDetails.ToAccount,
transferDetails.ReferenceID,
)
// Switch out comments on the return statements to simulate an error
//return fmt.Errorf("deposit did not occur due to an issue")
return nil
}

Save your changes and run the Worker.

go run worker/main.go

You will see the Worker complete the Withdraw() Activity function, but it errors when it attempts the Deposit() Activity function. The important thing to note here is that the Worker keeps retrying the Deposit() function.

2022/05/18 12:15:24 INFO  No logger configured for temporal client. Created default one.
2022/05/18 12:15:24 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@
2022/05/18 12:15:25 DEBUG ExecuteActivity Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowType TransferMoney WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 Attempt 1 ActivityID 5 ActivityType Withdraw

Withdrawing $54.990002 from account 001-001. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5
2022/05/18 12:15:25 DEBUG ExecuteActivity Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowType TransferMoney WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 Attempt 1 ActivityID 11 ActivityType Deposit

Depositing $54.990002 into account 002-002. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5
2022/05/18 12:15:25 ERROR Activity error. Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 ActivityType Deposit Attempt 1 Error deposit did not occur due to an issue

Depositing $54.990002 into account 002-002. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5
2022/05/18 12:15:26 ERROR Activity error. Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 ActivityType Deposit Attempt 2 Error deposit did not occur due to an issue

Depositing $54.990002 into account 002-002. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5
2022/05/18 12:15:28 ERROR Activity error. Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 ActivityType Deposit Attempt 3 Error deposit did not occur due to an issue

# it keeps retrying... with the RetryPolicy specified in workflow.go

You can view more information about what is happening in the UI. Click on the Workflow. You will see a stack trace showing you the errors, as well as details about the pending Activity:

The next activity

Select the Show Details link to see more details including its state, the number of times it has been attempted, and the next scheduled run time:

More details about the activity


Traditionally application developers are forced to implement timeout and retry logic within the service code itself. This is repetitive and error prone. With Temporal, one of the key value propositions is that timeout configurations (Schedule-To-Start Timeout, Schedule-To-Close Timeout, Start-To-Close Timeout, and Heartbeat Timeout) and Retry Policies are specified in the Workflow code as Activity options. In workflow.go, you can see that we have specified a StartToCloseTimeout for our Activities, and set a retry policy that tells the server to retry them up to 500 times. You can read more about Retries in our docs.

Your Workflow is running, but only the Withdraw() Activity function has succeeded. In any other application, the whole process would likely have to be abandoned and rolled back. So, here is the last value proposition of this tutorial: With Temporal, you can debug and fix the issue while the Workflow is running!

Pretend that you found a fix for the issue; Switch the comments back on the return statements of the Deposit() function in the activity.go file and save your changes.

How can you possibly update a Workflow that is already halfway complete? You restart the Worker!

First, cancel the worker with CTRL+C.

# continuing logs from previous retries...

Depositing $54.990002 into account 002-002. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5
2022/05/18 12:17:28 ERROR Activity error. Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ WorkflowID transfer-money-workflow RunID f023b1eb-9376-4bdf-8bdf-19d929c41f74 ActivityType Deposit Attempt 8 Error deposit did not occur due to an issue

^C

2022/05/18 12:17:36 INFO Worker has been stopped. Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@ Signal interrupt
2022/05/18 12:17:36 INFO Stopped Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88534@temporal.local@

Then restart the worker:

go run worker/main.go

On the next scheduled attempt, the Worker will pick up right where the Workflow was failing and successfully execute the newly compiled Deposit() Activity function, completing the Workflow.

2022/05/18 12:17:52 INFO  No logger configured for temporal client. Created default one.
2022/05/18 12:17:52 INFO Started Worker Namespace default TaskQueue TRANSFER_MONEY_TASK_QUEUE WorkerID 88987@temporal.local@

Depositing $54.990002 into account 002-002. ReferenceId: aacea6de-c461-4883-a57b-adeebfd8c1c5

Visit the UI again and you'll see the workflow has completed:

Both workflows completed successfully

You have just fixed a bug "on the fly" without losing the state of the Workflow.

Conclusion

Business person blasting off with a backpack rocket

You now know how to run a Temporal Workflow and understand some of the key values Temporal offers.

Review

Answer the following questions to see if you remember some of the more important concepts from this tutorial:

What are four of Temporal's value propositions that you learned about in this tutorial?

  1. Temporal gives you full visibility in the state of your Workflow and code execution.
  2. Temporal maintains the state of your Workflow, even through server outages and errors.
  3. Temporal makes it easy to timeout and retry Activity code using options that exist outside of your business logic.
  4. Temporal enables you to perform "live debugging" of your business logic while the Workflow is running.

How do you pair up Workflow initiation with a Worker that executes it?

Use the same Task Queue.

What do you have to do if you make changes to Activity code for a Workflow that is running?

Restart the Worker.