Skip to main content

Temporal Go SDK developer guide - getting started

Add the Temporal Go SDK to your project:

go get go.temporal.io/sdk@latest

Where is the Go SDK technical reference?

The Temporal Go SDK API reference is published on pkg.go.dev

Are there executable code samples?

You can find a complete list of executable code samples in the samples library, and each of the Go SDK Tutorials is backed by a fully executable template application.

Run the Server locally#

Use docker-compose to quickly install and run the Temporal Server locally to test the system while developing Workflows.

Make sure both Docker and docker-compose are installed.

Then clone the temporalio/docker-compose repository and run docker-compose up from the root of that repo:

git clone https://github.com/temporalio/docker-compose.gitcd  docker-composedocker-compose up

When the Temporal Server is running, Workflow Executions can be invoked. The Temporal Server can be kept running in the background while applications are built. Workflow Execution details can be viewed in the Temporal Web UI via your browser: localhost:8088.

The preceding steps start the Temporal Server using a default configuration.

To try other configurations (different dependencies and databases), or to try a custom Docker image follow the temporalio/docker-compose README.

Develop a Workflow Definition#

In the Temporal Go SDK programming model, a Workflow Definition is an exportable function.

func YourWorkflowDefinition(ctx workflow.Context) error {  // ...  return nil}

Workflow parameters in Go

The first parameter of a Go-based Workflow Definition must be of the workflow.Context type, as it is used by the Temporal Go SDK to pass around Workflow Execution context, and virtually all the Go SDK APIs that are callable from the Workflow require it. It is acquired from the go.temporal.io/sdk/workflow package.

import (    "go.temporal.io/sdk/workflow")
func YourWorkflowDefinition(ctx workflow.Context, param string) error {  // ...}

The workflow.Context entity operates similarly to the standard context.Context entity provided by Go. The only difference between workflow.Context and context.Context is that the Done() function, provided by workflow.Context, returns workflow.Channel instead of the standard Go chan.

The second parameter, string, is a custom parameter that is passed to the Workflow when it is invoked. A Workflow Definition may support multiple custom parameters, or none. However, the best practice is to pass a single parameter that is of a struct type so there can be some backward compatibility if new parameters are added.

type YourWorkflowParam struct {  WorkflowParamFieldOne string  WorkflowParamFieldTwo int}
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error {  // ...}

All Workflow Definition parameters must be serializable, which means that parameters can’t be channels, functions, variadic, or unsafe pointers.

Workflow return values in Go

A Go-based Workflow Definition can return either just an error or a customValue, error combination. Again, the best practice here is to use a struct type to hold all custom values.

type YourWorkflowResponse struct{  WorkflowResultFieldOne string  WorkflowResultFieldTwo int}
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...  if err != nil {    return "", err  }  responseVar := YourWorkflowResponse {    FieldOne: "super",    FieldTwo: 1,  }  return responseVar, nil}

A Workflow Definition written in Go can return both a custom value and an error. However, it is not possible to receive both a custom value and an error in the calling process as is normal in Go. The caller will receive either one or the other. Returning a non-nil error from a Workflow indicates that an error was encountered during its execution and the Workflow Execution should be Terminated and any custom return values will be ignored by the system.

Workflow logic requirements in Go

In Go specifically, Workflow Definition code can not directly do the following:

  • Iterate over maps using range, because with range the order of the map's iteration is randomized. Instead you can collect the keys of the map, sort them, and then iterate over the sorted keys to access the map. This will give deterministic results. You can also use a Side Effect or an Activity to process the map instead.
  • Use the native go statement, select statement, or chan type. (Use the SDK Go API, SDK Select API, or SDK Channel API instead.)
  • Call an external API, conduct a file I/O operation, talk to another service, etc. (Use an Activity for these.)

Develop an Activity Definition#

In the Temporal Go SDK programming model, an Activity Definition is an exportable function or struct method.

Function:

// basic function signaturefunc YourActivityDefinition(ctx workflow.Context) error {  // ...  return nil}
// with parameters and return valuesfunc SimpleActivity(ctx context.Context, value string) (string, error)

Struct method:

type YourActivityStruct struct {  ActivityFieldOne string  ActivityFieldTwo int}
func(a *YourActivityStruct) YourActivityDefinition(ctx workflow.Context) error {  // ...}
func(a *YourActivityStruct) YourActivityDefinitionTwo(ctx workflow.Context) error {  // ...}

An "Activity struct" can have more than one method, with each method acting as a separate Activity Type. Activities written as struct methods can use shared struct variables such as:

  • an application level DB pool
  • client connection to another service
  • reusable utilities
  • any other expensive resources you only want to initialize once per process

Because this is such a common need, the rest of this guide shows Activities written as struct methods.

Activity parameters in Go

The first parameter of an Activity Definition is context.Context. This parameter is optional for an Activity Definition, though it is recommended especially if the Activity is expected to use other Go SDK APIs.

An Activity Definition can support as many other custom parameters as needed. However, all parameters must be serializable (parameters can’t be channels, functions, variadic, or unsafe pointers), and it is recommended to pass a single struct that can be updated later.

type YourActivityParam struct {  ActivityParamFieldOne string  ActivityParamFieldTwo int}
type YourActivityStruct struct {  // ...}
func (a *YourActivityStruct) YourActivityDefinition(ctx context.Context, param YourActivityParam) error {  // ...}

There is no explicit limit to the amount of parameter data that can be passed to an Activity, however all parameters are recorded in the Workflow Execution History and a large Workflow Execution History can adversely impact the performance of your Workflow Execution.

Activity return values in Go

A Go-based Activity Definition can return either just an error or a customValue, error combination (same as a Workflow Definition). You may wish to use a struct type to hold all custom values, just keep in mind they must all be serializable.

type YourActivityResult struct{  ActivityResultFieldOne string  ActivityResultFieldTwo int}
func (a *YourActivityStruct) YourActivityDefinition(ctx context.Context, param YourActivityParam) (YourActivityResult, error) {  // ...  result := YourActivityResult {    ActivityResultFieldOne: a.ActivityFieldOne,    ActivityResultFieldTwo: a.ActivityFieldTwo,  }  return result, nil}

Other notes for developing Activities

All native features of the Go programming language can be used within an Activity and there are no other limitations to Activity Definition logic:

  • Performance: Keep in mind that all parameters and return values are recorded in the Workflow Execution Event History. A large Workflow Execution Event History can adversely impact the performance of your Workflow Executions, as the entire Event History is transferred to Worker Processes with every Workflow Task.
  • Idiomatic usage: You are free to use:
    • your own loggers and metrics controllers
    • the standard Go concurrency constructs
    • make calls to other services across a network

Develop the call to spawn an Activity Execution#

To spawn an Activity Execution, use the ExecuteActivity() API call inside your Workflow Definition. The API is available from the go.temporal.io/sdk/workflow package.

The ExecuteActivity() API call requires an instance of workflow.Context, the Activity function name, and any variables to be passed to the Activity Execution.

import (  // ...
  "go.temporal.io/sdk/workflow")
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...  yourActivityParam := YourActivityParam{    // ...  }  var activities *YourActivityStruct  future := workflow.ExecuteActivity(ctx, activities.YourActivityDefinition, yourActivityParam)  // ...}
func (a *YourActivityStruct) YourActivityDefinition(ctx context.Context, param YourActivityParam) error {  // ...}

The Activity function name can be provided as a variable object (no quotations) or as a string.

// ...  future := workflow.ExecuteActivity(ctx, "YourActivityDefinition", yourActivityParam)// ...

The benefit of passing the actual function object is that the framework can validate the parameters against the Activity Definition.

The ExecuteActivity call returns a Future, which can be used to get the result of the Activity Execution.

Get the result of an Activity Execution#

The ExecuteActivity API call returns an instance of workflow.Future which has the following two methods:

  • Get(): Takes an instance of the workflow.Context, that was passed to the Activity Execution, and a pointer as parameters. The variable associated with the pointer is populated with the Activity Execution result. This call blocks until the result is available.
  • IsReady(): Returns true when the result of the Activity Execution is ready.

Call the Get() method on the instance of workflow.Future to get the result of the Activity Execution. The type of the result parameter must match the type of the return value declared by the Activity function.

func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...  future := workflow.ExecuteActivity(ctx, YourActivityDefinition, yourActivityParam)  var yourActivityResult YourActivityResult  if err := future.Get(ctx, &yourActivityResult); err != nil {    // ...  }  // ...}

Use the IsReady() method first to make sure the Get() call doesn't cause the Workflow Execution to wait on the result.

func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...  future := workflow.ExecuteActivity(ctx, YourActivityDefinition, yourActivityParam)  // ...  if(future.IsReady()) {    var yourActivityResult YourActivityResult    if err := future.Get(ctx, &yourActivityResult); err != nil {      // ...    }  }  // ...}

It is idiomatic to invoke multiple Activity Executions from within a Workflow. Therefore it is also idiomatic to either block on the results of any of the Activity Executions or continue on to execute additional logic, checking for the Activity Execution results at a later time.

Develop the program to spawn a Workflow Execution#

To spawn a Workflow Execution, use the ExecuteWorkflow() method on the Go SDK Client, which is available via NewClient() in the go.temporal.io/sdk/client package.

The Go SDK Client should never be used inside a Workflow Definition. To spawn a Workflow Execution from within another Workflow, use the ExecuteChildWorkflow API. For more information, see How to spawn a Child Workflow Execution in Go.

The ExecuteWorkflow() API call requires an instance of context.Context, an instance of StartWorkflowOptions, a Workflow Type name, and all variables to be passed to the Workflow Execution. The ExecuteWorkflow() call returns a Future, which can be used to get the result of the Workflow Execution.

package main
import (  // ...
  "go.temporal.io/sdk/client")
func main() {  c, err := client.NewClient(client.Options{})  if err != nil {    // ...  }  defer c.Close()  // ...  workflowOptions := client.StartWorkflowOptions{    ID: "Your-Custom-Workflow-Id",    TaskQueue: "your-task-queue",  }  workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition, param)  if err != nil {    // ...  }  // ...}
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...}

Start the preceding process by running go run <filename>.go.

Notice that the Task Queue name is the same as the name provided when a Worker is created.

The only field, of the StartWorkflowOptions instance, that requires a value is the TaskQueue. A Task Queue name is also provided to the Worker that is registered to execute that particular Workflow Type. The Task Queue name must be the same for both.

We recommend supplying your own custom Workflow Id that can be used to get the result of the Workflow Execution asynchronously at another point in time. A custom Workflow Id is intended to correspond to a business-level identifier.

By default, the Workflow Type name is the same as the function name. If the invocation process has access to the function directly, then the Workflow Type name parameter can be passed as if the function name were a variable, without quotations.

If the invocation process does not have direct access to the statically defined Workflow Definition, for example, if the Workflow Definition is in an un-importable package, or it is written in a completely different language, then the Workflow Type can be provided as a string.

workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, "YourWorkflowDefinition", param)

In Go, the name of the Workflow Type can be customized when the Workflow Definition is registered with a Worker.

Get the result of a Workflow Execution#

The ExecuteWorkflow call returns an instance of WorkflowRun, which is the workflowRun variable below.

  workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, app.YourWorkflowDefinition, param)  if err != nil {    // ...  }  // ...}

The instance of WorkflowRun has the following three methods:

  • GetWorkflowID(): Returns the Workflow Id of the invoked Workflow Execution.
  • GetRunID(): Always returns the Run Id of the initial Run (See Continue As New) in the series of Runs that make up the full Workflow Execution.
  • Get: Takes a pointer as a parameter and populates the associated variable with the Workflow Execution result.

To wait on the result of Workflow Execution in the same process that invoked it, call Get() on the instance of WorkflowRun that is returned by the ExecuteWorkflow() call.

  workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition, param)  if err != nil {    // ...  }  var result YourWorkflowResponse  err = workflowRun.Get(context.Background(), &result)  if err != nil {      // ...  }  // ...}

However, the result of a Workflow Execution can be obtained from a completely different process. All that is needed is the Workflow Id. (A Run Id is optional if more than one closed Workflow Execution has the same Workflow Id.) The result of the Workflow Execution is available for as long as the Workflow Execution History remains in the system.

Call the GetWorkflow() method on the an instance of the Go SDK Client and pass it the Workflow Id used to spawn the Workflow Execution. Then call the Get() method on the instance of WorkflowRun that is returned, passing it a pointer to populate the result.

  // ...  workflowID := "Your-Custom-Workflow-Id"  workflowRun := c.GetWorkflow(context.Background, workflowID)
  var result YourWorkflowResponse  err = workflowRun.Get(context.Background(), &result)  if err != nil {      // ...  }  // ...

Develop a Worker Program#

First, create a new instance of a Worker by calling worker.New(), available via the go.temporal.io/sdk/worker package and pass it the following parameters:

  1. An instance of the The Temporal Go SDK Client.
  2. The name of the Task Queue that it will poll.
  3. An instance of worker.Options, which can be empty.

Then, register the Workflow Types and the Activity Types that the Worker will be capable of executing.

Lastly, call either the Start() or the Run() method on the instance of the Worker. Run accepts an interrupt channel as a parameter, so that the Worker can be stopped in the terminal. Otherwise the Stop() method must be called to stop the Worker.

package main
import (    "go.temporal.io/sdk/client"    "go.temporal.io/sdk/worker")
func main() {    c, err := client.NewClient(client.Options{})    if err != nil {        // ...    }    defer c.Close()    w := worker.New(c, "your-task-queue", worker.Options{})    w.RegisterWorkflow(YourWorkflowDefinition)    w.RegisterActivity(YourActivityDefinition)    err = w.Run(worker.InterruptCh())    if err != nil        // ...    }  // ...}
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {  // ...}
func YourActivityDefinition(ctx context.Context, param YourActivityParam) (YourActivityResponse, error) {  // ...}

Start a Worker Process by running go run <filename>.go.

The RegisterWorkflow() and RegisterActivity calls essentially create an in-memory mapping between the Workflow Types and their implementations, inside the Worker process.

Notice that that the Task Queue name is the same as the name provided when the Workflow Execution is invoked.

The name of the Task Queue that is provided to the Worker must be the same Task Queue name that is provided with the invocation of the Workflow Execution.

note

All Workers listening to the same Task Queue name must be registered to handle the exact same Workflows Types and Activity Types.

If a Worker polls a Task for a Workflow Type or Activity Type it does not know about, it will fail that Task. However, the failure of the Task will not cause the associated Workflow Execution to fail.