Skip to main content

Temporal Application development guide

This guide is meant to provide a comprehensive overview of the structures, primitives, and features used in Temporal Application development.

WORK IN PROGRESS

This guide is a work in progress. Some sections may be incomplete or missing for some languages. Information may change at any time.

This guide is meant to be a comprehensive resource for developing a Temporal Application.

It is broken down into two large sections:

  • Foundations: The minimum things required to build and run a simple Workflow with a single Activity.
  • Features: All the general features available to a Temporal Application.

Foundations

This section covers the minimum set of concepts and implementation details needed to build and run a simple Temporal Application – that is, all the relevant steps to start a Workflow Execution that executes an Activity.

Run a dev Cluster

Whenever we are developing Temporal Applications, we want to have a Temporal Cluster up and running. We can interact with a Cluster through Temporal Client APIs and tctl commands.

There are four ways to quickly install and run a Temporal Cluster:

  • Docker: Using Docker Compose makes it easy to develop your Temporal Application locally.
  • Render: Our temporalio/docker-compose experience has been translated to Render's Blueprint format for an alternative cloud connection.
  • Helm charts: Deploying a Cluster to Kubernetes is an easy way to test the system and develop Temporal Applications.
  • Gitpod: One-click deployments are available for Go and TypeScript.

We do not recommend using any of these methods in a full (production) environment.

Helm charts

Use Temporal Helm charts to deploy the Temporal Server to a Kubernetes cluster.

Deploying the Temporal Cluster with Helm is not recommended for a production environment, but it is a great way to test the system while developing Workflows.

Docker Compose

Use Docker Compose and Temporal Cluster Docker images to quickly install and run a Temporal Cluster locally while developing Workflows.

You must have Docker and Docker Compose 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.git
cd docker-compose
docker-compose up

When the Temporal Cluster is running, the Temporal Web UI becomes available in your browser: localhost:8080

The preceding steps start and run a Temporal Cluster 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.

Render

temporal-render-simple translates our docker-compose to Render by using the Auto-Setup Docker image. We do not recommend using this technique for production because all four Temporal internal services (Frontend, Matching, History, and Worker) are run in one process, but the benefit is one-click deployments.

Deploy to Render

Gitpod

You can run a Temporal Cluster and develop Temporal Applications in your browser using Gitpod.

One-click deployments are available for the temporalio/samples-go repo and the temporalio/samples-typescript repo.

A one-click deployment starts a Temporal Cluster using a Temporal Cluster Docker image, starts a Worker Process, and starts one of the application's sample Workflows.

It can take up to a full minute for the one-click deployments to get fully up and running. When it is running, you can customize the application samples.

Add your SDK

Add a Temporal SDK to your project. Both TypeScript and JavaScript can be used with the TypeScript SDK.

Build Status Coverage Status Go reference

The Temporal Go SDK provides a framework for Temporal Application development in the Go language. The SDK contains the following tools:

  • A Temporal Client to communicate with a Temporal Cluster
  • APIs to use within your Workflows
  • APIs to create and manage Worker Entities and Worker Processes

Get the SDK

Add the Temporal Go SDK to your project:

go get -u go.temporal.io/sdk@latest

Or clone the Go SDK repo to your preferred location:

git clone git@github.com:temporalio/sdk-go.git

Are there executable code samples?

You can find a complete list of executable code samples in the samples library, which includes Temporal Go SDK code samples from the temporalio/samples-go repo. Additionally, each of the Go SDK Tutorials is backed by a fully executable template application.

Where is the Go SDK technical reference?

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

Where can I find video demos?

Temporal Go SDK YouTube playlist

Develop Workflows

Workflows are the fundamental unit of a Temporal Application, and it all starts with the development of 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
}

In Go, by default, the Workflow Type name is the same as the function name.

Workflow parameters

Temporal Workflows may have any number of custom parameters. However, it is strongly recommended that objects are used as parameters, so that the object's individual fields may be altered without breaking the signature of the Workflow. All Workflow Definition parameters must be serializable.

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. These parameters can be regular type variables or safe pointers. 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, regardless of whether pointers or regular type values are used. Parameters can’t be channels, functions, variadic, or unsafe pointers.

Workflow return values

Workflow return values must also be serializable. Returning results, returning errors, or throwing exceptions is fairly idiomatic in each language that is supported. However, Temporal APIs that must be used to get the result of a Workflow Execution will only ever receive one of either the result or the error.

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's 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.

Customize Workflow Type

You can set a custom name for your Workflow Type.

To customize the Workflow Type set the Name parameter with RegisterOptions when registering your Workflow with a Worker.

  • Type: string
  • Default: function name
// ...
w := worker.New(temporalClient, "your_task_queue_name", worker.Options{})
registerOptions := workflow.RegisterOptions{
Name: "CoolWorkflowTypeName",
// ...
}
w.RegisterWorkflowWithOptions(YourWorkflowDefinition, registerOptions)
// ...

Workflow logic requirements

Workflow logic is constrained by deterministic execution requirements. Therefore, each language is limited to the use of certain idiomatic techniques. However, each Temporal SDK provides a set of APIs that can be used inside your Workflow to interact with external (to the Workflow) application code.

In Go, Workflow Definition code cannot 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 technique provides deterministic results. You can also use a Side Effect or an Activity to process the map instead.
  • Call an external API, conduct a file I/O operation, talk to another service, etc. (Use an Activity for these.)

The Temporal Go SDK has APIs to handle equivalent Go constructs:

  • workflow.Now() This is a replacement for time.Now().
  • workflow.Sleep() This is a replacement for time.Sleep().
  • workflow.GetLogger() This ensures that the provided logger does not duplicate logs during a replay.
  • workflow.Go() This is a replacement for the go statement.
  • workflow.Channel This is a replacement for the native chan type. Temporal provides support for both buffered and unbuffered channels.
  • workflow.Selector This is a replacement for the select statement. Learn more on the Go SDK Selectors page
  • workflow.Context This is a replacement for context.Context. Learn more on the Go SDK Context Propagation page.

Develop Activities

One of the primary things that Workflows do is orchestrate the execution of Activities. Activities are normal function/method executions that can interact with the world. For the Workflow to be able to execute the Activity, we must define the Activity Definition

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

Function

// basic function signature
func YourActivityDefinition(ctx context.Context) error {
// ...
return nil
}

// with parameters and return values
func SimpleActivity(ctx context.Context, value string) (string, error)

Struct method

type YourActivityStruct struct {
ActivityFieldOne string
ActivityFieldTwo int
}

func(a *YourActivityStruct) YourActivityDefinition(ctx context.Context) error {
// ...
}

func(a *YourActivityStruct) YourActivityDefinitionTwo(ctx context.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 that 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

All Activity parameters must be serializable.

There is no explicit limit to the amount of parameter data that can be passed to an Activity, but keep in mind that all parameters and return values are recorded in a Workflow Execution Event History. A large Workflow Execution Event History can adversely impact the performance of your Workflow Executions, because the entire Event History is transferred to Worker Processes with every Workflow Task.

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 {
// ...
}

Activity return values

All Activity results must be serializable.

There is no explicit limit to the amount of data that can be returned by an Activity, but keep in mind that all return values are recorded in a Workflow Execution Event History

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
}

Customize Activity Type

You can set a custom name for your Activity Type.

To customize the Activity Type set the Name parameter with RegisterOptions when registering your Activity with a Worker.

  • Type: string
  • Default: function name
// ...
w := worker.New(temporalClient, "your_task_queue_name", worker.Options{})
registerOptions := activity.RegisterOptions{
Name: "CoolActivityTypeName",
// ...
}
w.RegisterActivityWithOptions(a.YourActivityDefinition, registerOptions)
// ...

Start Activity Execution

Calls to spawn Activity Executions are written within a Workflow Definition. The call to spawn an Activity Execution generates the ScheduleActivityTask Command. This results in the set of three Activity Task related Events (ActivityTaskScheduled, ActivityTaskStarted, and ActivityTask[Closed])in your Workflow Execution Event History.

A single instance of the Activities implementation is shared across multiple simultaneous Activity invocations. Therefore, the Activity implementation code must be stateless.

The values passed to Activities through invocation parameters or returned through a result value are recorded in the Execution history. The entire Execution history is transferred from the Temporal service to Workflow Workers when a Workflow state needs to recover. A large Execution history can thus adversely impact the performance of your Workflow.

Therefore, be mindful of the amount of data you transfer through Activity invocation parameters or Return Values. Otherwise, no additional limitations exist on Activity implementations.

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 Activity results

The call to spawn an Activity Execution generates the ScheduleActivityTask Command and provides the Workflow with an Awaitable. Workflow Executions can either block progress until the result is available through the Awaitable or continue progressing, making use of the result when it becomes available.

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 results are 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 the Activity Executions or continue on to execute additional logic, checking for the Activity Execution results at a later time.

Create Temporal Clients

A Temporal Client is needed to create Worker Entities and to communicate with a Temporal Cluster. Communication with the Temporal Cluster includes but is not limited to starting Workflow Executions, sending Signals to Workflow Executions, sending Queries to Workflow Executions, getting the result of a Workflow Execution.

A Temporal Client cannot be initialized and used inside Workflow code. However, it is acceptable and common to utilize a Temporal Client, to communicate with a Temporal Cluster, inside an Activity.

Use the NewClient() API available in the go.temporal.io/sdk/client package to create a new Client

import (
// ...

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

func main() {
temporalClient, err := client.NewClient(client.Options{})
if err != nil {
// ...
}
defer temporalClient.Close()
// ...
}

Run Worker Processes

The Worker Process is where Workflow Functions and Activity Functions are executed. Each Worker Entity in the Worker Process must register the exact Workflow Types and Activity Types it may execute. Each Worker Entity must also associate itself with exactly one Task Queue. Each Worker Entity polling the same Task Queue must be registered with the same Workflow Types and Activity Types.

A Worker Entity is the component within a Worker Process that listens to a specific Task Queue.

Although multiple Worker Entities can be in a single Worker Process, a single Worker Entity Worker Process may be perfectly sufficient. For more information, see the Worker tuning guide.

A Worker Entity contains both a Workflow Worker and an Activity Worker so that it can make progress for either a Workflow Execution or an Activity Execution.

Create an instance of 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 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) {
// ...
}

Register multiple types

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 fails that Task. However, the failure of the Task does not cause the associated Workflow Execution to fail.

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

Registering Activity structs

Per Activity Definition best practices, you might have an Activity struct that has multiple methods and fields. When you use RegisterActivity() for an Activity struct, that Worker has access to all exported methods.

Registering multiple Types

To register multiple Activity Types and/or Workflow Types with the Worker Entity, just make multiple Activity registration calls, but make sure each Type name is unique:

w.registerActivity(ActivityA)
w.registerActivity(ActivityB)
w.registerActivity(ActivityC)
w.registerWorkflow(WorkflowA)
w.registerWorkflow(WorkflowB)
w.registerWorkflow(WorkflowC)

Start Workflow Execution

Workflow Execution semantics rely on several parameters—that is, to start a Workflow Execution you must supply a Task Queue that will be used for the Tasks (one that a Worker is polling), the Workflow Type, language-specific contextual data, and Workflow Function parameters.

In the examples below, all Workflow Executions are started using a Temporal Client. To spawn Workflow Executions from within another Workflow Execution, use either the Child Workflow or External Workflow APIs.

See the Customize Workflow Type section to see how to customize the name of the Workflow Type.

A request to spawn a Workflow Execution causes the Temporal Cluster to create the first Event (WorkflowExecutionStarted) in the Workflow Execution Event History. The Temporal Cluster then creates the first Workflow Task, resulting in the first WorkflowTaskScheduled Event.

To spawn a Workflow Execution, use the ExecuteWorkflow() method on the Go SDK Client.

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() {
temporalClient, err := client.NewClient(client.Options{})
if err != nil {
// ...
}
defer temporalClient.Close()
// ...
workflowOptions := client.StartWorkflowOptions{
// ...
}
workflowRun, err := temporalClient.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition, param)
if err != nil {
// ...
}
// ...
}

func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) (YourWorkflowResponse, error) {
// ...
}

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.

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

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)

Set Task Queue

The only Workflow Option that must be set is the name of the Task Queue.

For any code to execute, a Worker Process must be running that contains a Worker Entity that is polling the same Task Queue name.

Create an instance of StartWorkflowOptions from the go.temporal.io/sdk/client package, set the TaskQueue field, and pass the instance to the ExecuteWorkflow call.

  • Type: string
  • Default: None, this is a required field to be set by the developer
workflowOptions := client.StartWorkflowOptions{
// ...
TaskQueue: "your-task-queue",
// ...
}
workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Set Workflow Id

Also it is not required, we recommend providing your own Workflow Id that maps to a business process or business entity identifier, such as an order identifier or customer identifier.

Create an instance of StartWorkflowOptions from the go.temporal.io/sdk/client package, set the ID field, and pass the instance to the ExecuteWorkflow call.

  • Type: string
  • Default: System generated UUID
workflowOptions := client.StartWorkflowOptions{
// ...
ID: "Your-Custom-Workflow-Id",
// ...
}
workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Get Workflow results

If the call to start a Workflow Execution is successful, you will gain access to the Workflow Execution's Run Id.

The Workflow Id, Run Id, and Namespace may be used to uniquely identify a Workflow Execution in the system and get its result.

It's possible to both block progress on the result (synchronous execution) or get the result at some other point in time (asynchronous execution).

In the Temporal Platform, it's also acceptable to use Queries as the preferred method for accessing the state and results of Workflow Executions.

The ExecuteWorkflow call returns an instance of WorkflowRun, which is the workflowRun variable in the following line.

 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 Event History remains in the system.

Call the GetWorkflow() method on 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 {
// ...
}
// ...

Get last completion result

In the case of a Temporal Cron Job, you might need to get the result of the previous Workflow Run and use it in the current Workflow Run.

To do this, use the HasLastCompletionResult and GetLastCompletionResult APIs, available from the go.temporal.io/sdk/workflow package, directly in your Workflow code.

type CronResult struct {
Count int
}

func YourCronWorkflowDefinition(ctx workflow.Context) (CronResult, error) {
count := 1

if workflow.HasLastCompletionResult(ctx) {
var lastResult CronResult
if err := workflow.GetLastCompletionResult(ctx, &lastResult); err == nil {
count = count + lastResult.Count
}
}

newResult := CronResult {
Count: count,
}
return newResult, nil
}

This will work even if one of the cron Workflow Runs fails. The next Workflow Run gets the result of the last successfully Completed Workflow Run.

Features

This section covers many of the features that are available to use in your Temporal Application.

Signals

A Signal is a message that delivers data to a running Workflow Execution.

Signals are defined alongside your application code and handled in your Workflow Definition. Signals can be sent to Workflow Executions from a Temporal Client or from within a Workflow.

Define Signal

A Signal type and its data must be serializable.

Structs should be used to define Signals and carry data, as long as the struct is serializable via the Data Converter. The Receive() method on the Data Converter decodes the data into the Struct within the Workflow. Only public fields are serializable.

MySignal struct {
Message string // serializable
message string // not serializable
}

Handle Signal

Workflows listen for Signals by the Signal's name.

Use the GetSignalChannel() API from the go.temporal.io/sdk/workflow package to get the Signal Channel. Get a new Selector and pass it the Signal Channel and a callback function to handle the payload.

func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error {
// ...
var signal MySignal
signalChan := workflow.GetSignalChannel(ctx, "your-signal-name")
selector := workflow.NewSelector(ctx)
selector.AddReceive(signalChan, func(channel workflow.ReceiveChannel, more bool) {
channel.Receive(ctx, &signal)
// ...
})
selector.Select(ctx)
if len(signal.Message) > 0 && signal.Message != "SOME_VALUE" {
return errors.New("signal")
}
// ...
}

In the example above, the Workflow code uses workflow.GetSignalChannel to open a workflow.Channel for the Signal type (identified by the Signal name). We then use a workflow.Selector and the AddReceive() to wait on a Signal from this channel. The more bool in the callback function indicates that channel is not closed and more deliveries are possible.

Send Signal from Client

When a Signal is sent successfully from the Temporal Client, the WorkflowExecutionSignaled Event appears in the Event History of the Workflow that receives the Signal.

Use the SignalWorkflow() method on an instance of the Go SDK Temporal Client to send a Signal to a Workflow Execution.

Pass in both the Workflow Id and Run Id to uniquely identify the Workflow Execution. If only the Workflow Id is supplied (provide an empty string as the Run Id param), the Workflow Execution that is Running receives the Signal.

// ...
signal := MySignal {
Message: "Some important data",
}
err = temporalClient.SignalWorkflow(context.Background(), "your-workflow-id", runID, "your-signal-name", signal)
if err != nil {
log.Fatalln("Error sending the Signal", err)
return
}
// ...

Possible errors:

  • serviceerror.NotFound
  • serviceerror.Internal
  • serviceerror.Unavailable

Send Signal from Workflow

Sending a Signal from within a Workflow is often referred to as sending an External Signal.

The SignalExternalWorkflowExecutionInitiated Event appears in the Workflow Execution Event History of the Workflow that sent the Signal, and the WorkflowExecutionSignaled Event appears in the Event History of the Workflow that receives the Signal.

A Signal can be sent from within a Workflow to a different Workflow Execution using the SignalExternalWorkflow API from the go.temporal.io/sdk/workflow package.

// ...
func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error {
//...
signal := MySignal {
Message: "Some important data",
}
err := workflow.SignalExternalWorkflow(ctx, "some-workflow-id", "", "your-signal-name", signalData).Get(ctx, nil)
if err != nil {
// ...
}
// ...
}

Send Signal-With-Start

Signal-With-Start can be used to start a Workflow Execution (if not already running) and pass it the Signal at the same time.

Use the SignalWithStartWorkflow() API on the Go SDK Temporal Client to start a Workflow Execution (if not already running) and pass it the Signal at the same time.

Because the Workflow Execution might not exist, this API does not take a Run ID as a parameter

// ...
signal := MySignal {
Message: "Some important data",
}
err = temporalClient.SignalWithStartWorkflow(context.Background(), "your-workflow-id", "your-signal-name", signal)
if err != nil {
log.Fatalln("Error sending the Signal", err)
return
}

Queries

A Query is a synchronous operation that is used to get the state of a Workflow Execution.

Query name

A Query name (also called Query type) is simply a string name.

In Go, a Query type, also called a Query name, is a string value.

queryType := "your_query_name"

Send Query

Queries are sent from a Temporal Client.

Use the QueryWorkflow() API or the QueryWorkflowWithOptions API on the Temporal Client to send a Query to a Workflow Execution.

// ...
response, err := temporalClient.QueryWorkflow(context.Background(), workflowID, runID, queryType)
if err != nil {
// ...
}
// ...

You can pass an arbitrary number of arguments to the QueryWorkflow() function.

// ...
response, err := temporalClient.QueryWorkflow(context.Background(), workflowID, runID, queryType, "foo", "baz")
if err != nil {
// ...
}
// ...

The QueryWorkflowWithOptions() API provides similar functionality, but with the ability to set additional configurations through QueryWorkflowWithOptionsRequest. When using this API, you will also receive a structured response of type QueryWorkflowWithOptionsResponse.

// ...
response, err := temporalClient.QueryWorkflowWithOptions(context.Background(), &client.QueryWorkflowWithOptionsRequest{
WorkflowID: workflowID,
RunID: runID,
QueryType: queryType,
Args: args,
})
if err != nil {
// ...
}

Handle Query

Queries are handled by your Workflow.

Don’t include any logic that causes Command generation within a Query handler (such as executing Activities). Including such logic causes unexpected behavior.

Use the SetQueryHandler API from the go.temporal.io/sdk/workflow package to set a Query Handler that listens for a Query by name.

The handler must be a function that returns two values:

  1. A serializable result
  2. An error

The handler function can receive any number of input parameters, but all input parameters must be serializable. The following sample code sets up a Query Handler that handles the current_state Query type:

func MyWorkflow(ctx workflow.Context, input string) error {
currentState := "started" // This could be any serializable struct.
queryType := "current_state"
err := workflow.SetQueryHandler(ctx, queryType, func() (string, error) {
return currentState, nil
})
if err != nil {
currentState = "failed to register query handler"
return err
}
// Your normal Workflow code begins here, and you update the currentState as the code makes progress.
currentState = "waiting timer"
err = NewTimer(ctx, time.Hour).Get(ctx, nil)
if err != nil {
currentState = "timer failed"
return err
}
currentState = "waiting activity"
ctx = WithActivityOptions(ctx, myActivityOptions)
err = ExecuteActivity(ctx, MyActivity, "my_input").Get(ctx, nil)
if err != nil {
currentState = "activity failed"
return err
}
currentState = "done"
return nil
}

For example, suppose your query handler function takes two parameters:

err := workflow.SetQueryHandler(ctx, "current_state", func(prefix string, suffix string) (string, error) {
return prefix + currentState + suffix, nil
})

Workflow timeouts & retries

Each Workflow timeout controls the maximum duration of a different aspect of a Workflow Execution. A Retry Policy can work in cooperation with the timeouts to provide fine controls to optimize the execution experience.

Workflow Execution Timeout

Use the Workflow Execution Timeout to limit the maximum time that a Workflow Execution can be executing (have an Open status) including retries and any usage of Continue As New.

Create an instance of StartWorkflowOptions from the go.temporal.io/sdk/client package, set the WorkflowExecutionTimeout field, and pass the instance to the ExecuteWorkflow call.

  • Type: time.Duration
  • Default: Unlimited
workflowOptions := client.StartWorkflowOptions{
// ...
WorkflowExecutionTimeout: time.Hours * 24 * 365 * 10,
// ...
}
workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Workflow Run Timeout

Use the Workflow Execution Timeout to limit the maximum time that a Workflow Execution can be executing (have an Open status) including retries and any usage of Continue As New.

Create an instance of StartWorkflowOptions from the go.temporal.io/sdk/client package, set the WorkflowRunTimeout field, and pass the instance to the ExecuteWorkflow call.

workflowOptions := client.StartWorkflowOptions{
WorkflowRunTimeout: time.Hours * 24 * 365 * 10,
// ...
}
workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Workflow Task Timeout

Use the Workflow Execution Timeout to limit the maximum time that a Workflow Execution can be executing (have an Open status) including retries and any usage of Continue As New.

Create an instance of StartWorkflowOptions from the go.temporal.io/sdk/client package, set the WorkflowTaskTimeout field, and pass the instance to the ExecuteWorkflow call.

  • Type: time.Duration
  • Default: time.Seconds * 10
workflowOptions := client.StartWorkflowOptions{
WorkflowTaskTimeout: time.Second * 10,
//...
}
workflowRun, err := c.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Workflow Retry Policy

Use a Retry Policy to retry a Workflow Execution in the event of a failure.

Workflow Executions do not retry by default, and Retry Policies should be used with Workflow Executions only in certain situations.

Create an instance of a RetryPolicy from the go.temporal.io/sdk/temporal package and provide it as the value to the RetryPolicy field of the instance of StartWorkflowOptions.

retrypolicy := &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Second * 100,
}
workflowOptions := client.StartWorkflowOptions{
RetryPolicy: retrypolicy,
// ...
}
workflowRun, err := temporalClient.ExecuteWorkflow(context.Background(), workflowOptions, YourWorkflowDefinition)
if err != nil {
// ...
}

Activity timeouts & retries

Each Activity timeout controls the maximum duration of a different aspect of an Activity Execution. A Retry Policy works in cooperation with the timeouts to provide fine controls to optimize the execution experience.

Schedule-To-Close Timeout

Use the Schedule-To-Close Timeout to limit the maximum duration of an Activity Execution.

To set a Schedule-To-Close Timeout, create an instance of ActivityOptions from the go.temporal.io/sdk/workflow package, set the ScheduleToCloseTimeout field, and then use the WithActivityOptions() API to apply the options to the instance of workflow.Context.

This or StartToCloseTimeout must be set.

  • Type: time.Duration
  • Default: ∞ (infinity - no limit)
activityoptions := workflow.ActivityOptions{
ScheduleToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, activityoptions)
var yourActivityResult YourActivityResult
err = workflow.ExecuteActivity(ctx, YourActivityDefinition, yourActivityParam).Get(ctx, &yourActivityResult)
if err != nil {
// ...
}

Start-To-Close Timeout

Use the Start-To-Close Timeout to limit the maximum duration of a single Activity Task Execution.

To set a Start-To-Close Timeout, create an instance of ActivityOptions from the go.temporal.io/sdk/workflow package, set the StartToCloseTimeout field, and then use the WithActivityOptions() API to apply the options to the instance of workflow.Context.

This or ScheduleToClose must be set.

  • Type: time.Duration
  • Default: Same as the ScheduleToCloseTimeout
activityoptions := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, activityoptions)
var yourActivityResult YourActivityResult
err = workflow.ExecuteActivity(ctx, YourActivityDefinition, yourActivityParam).Get(ctx, &yourActivityResult)
if err != nil {
// ...
}

Schedule-To-Start Timeout

Use the Schedule-To-Start Timeout to limit the maximum amount of time that an Activity Task can be enqueued to be picked up by a Worker.

To set a Schedule-To-Start Timeout, create an instance of ActivityOptions from the go.temporal.io/sdk/workflow package, set the ScheduleToStartTimeout field, and then use the WithActivityOptions() API to apply the options to the instance of workflow.Context.

  • Type: time.Duration
  • Default: ∞ (infinity - no limit)
activityoptions := workflow.ActivityOptions{
ScheduleToStartTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, activityoptions)
var yourActivityResult YourActivityResult
err = workflow.ExecuteActivity(ctx, YourActivityDefinition, yourActivityParam).Get(ctx, &yourActivityResult)
if err != nil {
// ...
}