Skip to main content

Build a Temporal "Hello World!" app from scratch in Go

Tutorial information
  • Level: ⭐ Temporal beginner
  • Time: ⏱️ ~10 minutes
  • Goals: 🙌
    • Set up, build, and test a Temporal application project from scratch using the Go SDK.
    • Become more familiar with core concepts and the application structure.

Introduction

Creating reliable applications is a difficult task. Temporal lets you create fault-tolerant resiliant applications using programming languages you already know, so you can build complex applications that execute successfully and recover from failures.

In this tutorial, you will build your first Temporal Application from scratch using the Temporal Go SDK. The app will consist of four pieces:

  1. A Workflow: Workflows are functions that define the overall flow of the application and represent the orchestration aspect of the business logic.
  2. An Activity: Activities are functions called during Workflow Execution and represent the execution aspect of your business logic. The Workflow you'll create executes a single Activity, which takes a string from the Workflow as input and returns a formatted version of this string to the Workflow.
  3. A Worker: Workers host the Activity and Workflow code and execute the code piece by piece.
  4. An initiator: To start a Workflow, you need to send a signal to the Temporal server to tell it to track the state of the Workflow. You'll write a separate program to do this.

You'll also write a unit test to ensure your Workflow executes successfully.

When you're done, you'll have a basic application and a clear understanding of how to build out the components you'll need in future Temporal applications.

All of the code in this tutorial is available in the hello-world Go template repository.

Prerequisites

Before starting this tutorial:

Create a new Go project

To create an app with the Temporal Go SDK, you'll create a new Go project and initialize it as a module, just like any other Go program you're creating. Then you'll add the Temporal SDK package to your project.

In a terminal, create a new project directory called hello-world-temporal:

mkdir hello-world-temporal

Switch to the new directory:

cd hello-world-temporal

From the root of your new project directory, initialize a new Go module. Make sure the module path (for example, hello-world-temporal) matches that of the directory in which you are creating the module.

go mod init hello-world-temporal/app

Then add the Temporal Go SDK as a project dependency:

go get go.temporal.io/sdk

You'll see the following output, indicating that the SDK is now a project dependency:

go: added go.temporal.io/sdk v1.17.0

With your project workspace configured, you're ready to create your first Temporal Activity and Workflow. You'll start with the Workflow.

Create a Workflow

Workflows are where you configure and organize the execution of Activities. You define a Workflow by writing a Workflow Definition using one of the Temporal SDKs.

You write a Workflow using one of the programming languages supported by a Temporal SDK. This code is known as a Workflow Definition.

In the Temporal Go SDK, a Workflow Definition is an exported function with two additional requirements: it must accept workflow.Context as the first input parameter, and it must return error. Your Workflow function can optionally return another value, which you'll use to return the result of the Workflow Execution. You can learn more in the Workflow parameters section of the Temporal documentation.

Create the file workflow.go in the root of your project and add the following code to create a GreetingWorkflow function to define the Workflow:

workflow.go

package app

import (
"time"

"go.temporal.io/sdk/workflow"
)

func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
options := workflow.ActivityOptions{
StartToCloseTimeout: time.Second * 5,
}

ctx = workflow.WithActivityOptions(ctx, options)

var result string
err := workflow.ExecuteActivity(ctx, ComposeGreeting, name).Get(ctx, &result)

return result, err
}

The GreetingWorkflow function accepts a workflow.Context and a string value that holds the name. It returns a string value and an error, which follows the conventions you'll find in other Go programs. You can learn more in the Workflow parameters section of the Temporal documentation.

tip

You can pass multiple inputs to a Workflow, but it's a good practice to send a single input. If you have several values you want to send, you should define a Struct and pass that into the Workflow instead.

The function defines the options to execute an Activity, and then executes an Activity called ComposeGreeting, which you'll define next. The function returns the result of the Activity.

Create an Activity

You use Activities in your Temporal Applications to execute non-deterministic code or perform operations that may fail.

For this tutorial, your Activity won't be complex; you'll create an Activity that takes a string as input and uses it to create a new string as output, which is then returned to the Workflow. This will let you see how Workflows and Activities work together without building something complicated.

With the Temporal Go SDK, you define Activities similarly to how you define Workflows: using a regular exportable Go function.

Create the file activity.go in the project root and add the following code to define a ComposeGreeting function:

activity.go

package app

import (
"context"
"fmt"
)

func ComposeGreeting(ctx context.Context, name string) (string, error) {
greeting := fmt.Sprintf("Hello %s!", name)
return greeting, nil
}

The ComposeGreeting Activity Definition also accepts a Context . This parameter is optional for Activity Definitions, but it's recommended because you may need it for other Go SDK features as your application evolves.

Your Activity Definition can accept input parameters. Review the Activity parameters section of the Temporal documentation for more details, as there are some limitations you'll want to be aware of when running more complex applications.

The logic within the ComposeGreeting function creates the string and returns the greeting, along with nil for the error. In a more complex case, you can return an error from your Activity and then configure your Workflow to handle the error.

You've completed the logic for the application; you have a Workflow and an Activity defined. Before moving on, you'll write a unit test for your Workflow.

Add a unit test

The Temporal Go SDK includes functions that help you test your Workflow executions. Let's add a basic unit test to the application to make sure the Workflow works as expected.

You'll use the testify package to build your test cases and mock the Activity so you can test the Workflow in isolation.

Add the required testify packages to your project by running the following commands in your terminal:

go get github.com/stretchr/testify/mock
go get github.com/stretchr/testify/require
go mod tidy

Now create the file workflow_test.go and add the following code to the file to define the Workflow test:

workflow_test.go

package app

import (
"testing"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.temporal.io/sdk/testsuite"
)

func Test_Workflow(t *testing.T) {
// Set up the test suite and testing execution environment
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()

// Mock activity implementation
env.OnActivity(ComposeGreeting, mock.Anything, "World").Return("Hello World!", nil)

env.ExecuteWorkflow(GreetingWorkflow, "World")
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())

var greeting string
require.NoError(t, env.GetWorkflowResult(&greeting))
require.Equal(t, "Hello World!", greeting)
}

This test creates a test execution environment and then mocks the Activity implementation so it returns a successful execution. The test then executes the Workflow in the test environment and checks for a successful execution. Finally, the test ensures the Workflow's return value returns the expected value.

Run the following command from the project root to execute the unit tests:

go test

You'll see the following output from your test run indicating that the test was successful:

2022/09/28 16:16:32 DEBUG handleActivityResult: *workflowservice.RespondActivityTaskCompletedRequest. ActivityID 1 ActivityType ComposeGreeting
PASS
ok hello-world-temporal/app 0.197s

You have a working application and a test to ensure the Workflow executes as expected. Next, you'll configure a Worker to execute your Workflow.

Configure a Worker

A Worker hosts Workflow and Activity functions and executes them one at a time. The Temporal Server tells the Worker to execute a specific function from information it pulls from the Task Queue. After the Worker runs the code, it communicates the results back to the Temporal Server.

To configure a Worker process using the Go SDK, you create an instance of Worker and give it the name of the Task Queue to poll. When you start a Workflow, you tell the server which Task Queue the Workflow and Activities use.

Since you'll use the Task Queue name in multiple places in your project, create the file shared.go and define the Task Queue name there:

shared.go

package app

const GreetingTaskQueue = "GREETING_TASK_QUEUE"

Now you'll create the Worker process. In this tutorial you'll create a small standalone Worker program so you can see how all of the components work together.

Create a new directory called worker which will hold the program you'll create:

mkdir worker

Then create the file worker/main.go and add the following code to connect to the Temporal Server, instantiate the Worker, and register the Workflow and Activity:

worker/main.go

package main

import (
"hello-world-temporal/app"
"log"

"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)

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()

// This worker hosts both Workflow and Activity functions
w := worker.New(c, app.GreetingTaskQueue, worker.Options{})
w.RegisterWorkflow(app.GreetingWorkflow)
w.RegisterActivity(app.ComposeGreeting)

// Start listening to the Task Queue
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("unable to start Worker", err)
}
}

This program uses client.Dial to connect to the Temporal server, and then uses worker.New to instantiate the Worker. You register the Workflow and Activity with the Worker and then use Run to start the Worker.

You've created a program that instantiates a Worker to process the Workflow. Now you need to start the Workflow.

Write code to start a Workflow Execution

You can start a Workflow Execution by using the Temporal CLI or by writing code using the Temporal SDK. In this tutorial, you'll use the Temporal SDK to start the Workflow, which is how most real-world applications work.

Starting a Workflow Execution using the Temporal SDK involves connecting to the Temporal Server, configuring the Task Queue the Workflow should use, and starting the Workflow with the input parameters it expects. In a real application, you may invoke this code when someone submits a form, presses a button, or visits a certain URL. In this tutorial, you'll create a small command-line program that starts the Workflow Execution.

Create a new directory called start to hold the program:

mkdir start

Then create the file start/main.go and add the following code to the file to connect to the server and start the Workflow:

start/main.go

package main

import (
"context"
"fmt"
"hello-world-temporal/app"
"log"

"go.temporal.io/sdk/client"
)

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()

options := client.StartWorkflowOptions{
ID: "greeting-workflow",
TaskQueue: app.GreetingTaskQueue,
}

// Start the Workflow
name := "World"
we, err := c.ExecuteWorkflow(context.Background(), options, app.GreetingWorkflow, name)
if err != nil {
log.Fatalln("unable to complete Workflow", err)
}

// Get the results
var greeting string
err = we.Get(context.Background(), &greeting)
if err != nil {
log.Fatalln("unable to get Workflow result", err)
}

printResults(greeting, we.GetID(), we.GetRunID())
}

func printResults(greeting string, workflowID, runID string) {
fmt.Printf("\nWorkflowID: %s RunID: %s\n", workflowID, runID)
fmt.Printf("\n%s\n\n", greeting)
}

Like the Worker you created, this program uses client.Dial to connect to the Temporal server. It then specifies a Workflow ID for the Workflow, as well as the Task Queue. The Worker you configured is looking for tasks on that Task Queue.

Specify a Workflow ID

You don't need to specify a Workflow ID, as Temporal will generate one for you, but defining the ID yourself makes it easier for you to find it later in logs or interact with a running Workflow in the future.

Using an ID that reflects some business process or entity is a good practice. For example, you might use a customer ID or email address as part of the Workflow ID if you ran one Workflow per customer. This would make it easier to find all of the Workflow Executions related to that customer later.

You can get the results from your Workflow right away, or you can get the results at a later time. This implementation attempts to get the results immediately by calling we.Get, which blocks the program's execution until the Workflow Execution completes.

You have a Workflow, an Activity, a Worker, and a way to start a Workflow Execution. It's time to run the Workflow.

Run the app

To run the app, you need to start the Workflow and the Worker. You can start these in any order, but you'll need to run each command from a separate terminal window, as the Worker needs to be constantly running to look for tasks to execute.

First, ensure that your local Temporal Cluster is running.

To start the Worker, run this command from the project root:

go run worker/main.go

You'll see output like the following in your terminal, indicating that the Worker has started and has connected to the Task Queue:

2022/09/30 13:57:56 INFO  No logger configured for temporal client. Created default one.
2022/09/30 13:57:56 INFO Started Worker Namespace default TaskQueue GREETING_TASK_QUEUE WorkerID 45122@temporal.local@

Leave this program running.

To start the Workflow, open a new terminal window and switch to your project root:

cd hello-world-temporal

Then your start/main.go from the project root to start the Workflow Execution:

go run start/main.go

The program runs and returns the result:

2022/09/30 14:00:07 INFO  No logger configured for temporal client. Created default one.

WorkflowID: greeting-workflow RunID: 0c189fd9-57aa-4155-8b1e-cd6c50cf1761

Hello World!

Switch to the terminal window that's running the Worker and you'll see that its output updated to show that it executed the Workflow and the Activity:

2022/09/30 14:00:07 DEBUG ExecuteActivity Namespace default TaskQueue GREETING_TASK_QUEUE WorkerID 46038@temporal.local@ WorkflowType GreetingWorkflow WorkflowID greeting-workflow RunID 0c189fd9-57aa-4155-8b1e-cd6c50cf1761 Attempt 1 ActivityID 5 ActivityType ComposeGreeting

You can stop the Worker with CTRL-C.

You have successfully built a Temporal application from scratch.

Conclusion

You now know how to build a Temporal Workflow application using the Go SDK.

Review

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

What are the minimum four pieces of a Temporal Workflow application?

  1. An Activity function.
  2. A Workflow function.
  3. A Worker to host the Activity and Workflow code.
  4. Some way to start the Workflow.

How does the Temporal server get information to the Worker?

It adds the information to a Task Queue.

True or false, with the Temporal Go SDK, you define Activities and Workflows by writing Go functions?

True. Workflows and Activities are Go functions that must be exportable.