Getting started with the Go client
In this tutorial, you will learn how to use the the community-supported Go client in a Go application to interact with Camunda 8.
You can find a complete example on GitHub.
The Go client doesn't support multi-tenancy and can only be used when multi-tenancy is disabled.
Prerequisites
Set up a project
First, we need a new Go project. To do this, complete the following steps:
- Create a new project using your IDE, or create a new Go module with the following command:
mkdir github.com/zb-user/zb-example
cd github.com/zb-user/zb-example
go mod init zb-user/zb-example
- To use the Zeebe Go client library, run the following:
go get github.com/camunda-community-hub/zeebe-client-go/v8@v8.6.0
This adds the following dependency to your go.mod
, it should look similar to this:
module github.com/zb-user/zb-example
go 1.21
require github.com/camunda-community-hub/zeebe-client-go/v8 v8.6.0
- Set the connection settings and client credentials as environment variables in your terminal:
export ZEEBE_ADDRESS='[Zeebe API]'
export ZEEBE_CLIENT_ID='[Client ID]'
export ZEEBE_CLIENT_SECRET='[Client Secret]'
export ZEEBE_AUTHORIZATION_SERVER_URL='[OAuth API]'
When you create client credentials in Camunda 8, you have the option to download a file with the lines above filled out for you.
- Create a
main.go
file inside the module and add the following lines to bootstrap the Zeebe client:
package main
import (
"context"
"fmt"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/zbc"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/pb"
"os"
)
func main() {
client, err := zbc.NewClient(&zbc.ClientConfig{
GatewayAddress: os.Getenv("ZEEBE_ADDRESS"),
})
if err != nil {
panic(err)
}
ctx := context.Background()
topology, err := client.NewTopologyCommand().Send(ctx)
if err != nil {
panic(err)
}
for _, broker := range topology.Brokers {
fmt.Println("Broker", broker.Host, ":", broker.Port)
for _, partition := range broker.Partitions {
fmt.Println(" Partition", partition.PartitionId, ":", roleToString(partition.Role))
}
}
}
func roleToString(role pb.Partition_PartitionBrokerRole) string {
switch role {
case pb.Partition_LEADER:
return "Leader"
case pb.Partition_FOLLOWER:
return "Follower"
default:
return "Unknown"
}
}
- Run the program.
go run main.go
You will note a similar output:
Broker 0.0.0.0 : 26501
Partition 1 : Leader
Model a process
Now, we need a simple process we can deploy. Later, we will extend the process with more functionality. For now, follow the steps below:
- Web Modeler
- Desktop Modeler
Open Web Modeler and create a new BPMN diagram named
order-process.bpmn
.Add a start event named
Order Placed
and an end event namedOrder Delivered
to the diagram. Then, connect the events.
Set the ID (the BPMN process ID) to
order-process
instead of the autogenerated value so it's easier to work with in this example.[Optional] Download the BPMN file to the root of the project.
Open Desktop Modeler and create a new Camunda 8 BPMN diagram named
order-process.bpmn
.Add a start event named
Order Placed
and an end event namedOrder Delivered
to the diagram. Then, connect the events.
Set the ID (the BPMN process ID) to
order-process
instead of the autogenerated value so it's easier to work with in this example.Place the BPMN diagram in the root of the project.
Deploy a process
Next, we want to deploy the modeled process to the broker.
The broker stores the process under its BPMN process ID and assigns a version.
- Web Modeler
- Desktop Modeler
Using Web Modeler, you can deploy the BPMN diagram in the UI using the Deploy button.
Alternatively, if you took the optional step and downloaded your BPMN diagram, you can follow the instructions for Desktop Modeler for this section.
Add the following to main.go
at the bottom of func main()
.
// After the client is created (add this to the end of your func main())
response, err := client.NewDeployResourceCommand().AddResourceFile("order-process.bpmn").Send(ctx)
if err != nil {
panic(err)
}
fmt.Println(response.String())
Run the program and verify the process deployed successfully.
You will note a similar output:
key:2251799813685254 processes:{bpmnProcessId:"order-process" version:3 processDefinitionKey:2251799813685253 resourceName:"order-process.bpmn"}
Create a process instance
We are ready to create our first instance of the deployed process.
A process instance is created by a specific version of the process, which can be set on creation.
// After the process is deployed.
variables := make(map[string]interface{})
variables["orderId"] = "31243"
request, err := client.NewCreateInstanceCommand().BPMNProcessId("order-process").LatestVersion().VariablesFromMap(variables)
if err != nil {
panic(err)
}
ctx := context.Background()
msg, err := request.Send(ctx)
if err != nil {
panic(err)
}
fmt.Println(msg.String())
Run the program and verify the process instance is created. You will note an output similar to below:
processKey:2251799813686742 bpmnProcessId:"order-process" version:3 processInstanceKey:2251799813686744
Note the process in action
Want to note how the process instance is executed? Follow the steps below:
- Go to the cluster in Camunda 8 and select it.
- Click on the link to Operate.
- Select the process order process.
A process instance has now been started and finished.
Work on a task
Now, we want to do some work within our process. Follow the steps below:
Add a few service tasks to the BPMN diagram and set the required attributes.
Extend your
main.go
file and activate a job. These are created when the process instance reaches a service task.Open the BPMN diagram in Modeler. Keeping in mind how you want to deploy your model, you can choose either Web Modeler or Desktop Modeler.
Insert three service tasks between the start and the end event.
- Name the first task
Collect Money
. - Name the second task
Fetch Items
. - Name the third task
Ship Parcel
.
- Using the properties panel Task definition section, set the type of each task, which identifies the nature of the work to be performed.
- Set the type of the first task to
payment-service
. - Set the type of the second task to
fetcher-service
. - Set the type of the third task to
shipping-service
.
- Additionally, for the service task
Collect Money
set a task-header with the keymethod
and the valueVISA
. This header is used as a configuration parameter for the payment-service worker to hand over the payment method.
The consolidated example looks as follows:
- Web Modeler
- Desktop Modeler
package main
import (
"context"
"fmt"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/entities"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/worker"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/zbc"
"log"
"os"
)
const ZeebeAddr = "0.0.0.0:26500"
var readyClose = make(chan struct{})
func main() {
gatewayAddr := os.Getenv("ZEEBE_ADDRESS")
plainText:= false
if (gatewayAddr == "") {
gatewayAddr = ZeebeAddr
plainText = true
}
zbClient, err := zbc.NewClient(&zbc.ClientConfig{
GatewayAddress: gatewayAddr,
UsePlaintextConnection: plainText,
})
if err != nil {
panic(err)
}
ctx := context.Background()
// deploy process happens in the Web Modeler UI
// create a new process instance
variables := make(map[string]interface{})
variables["orderId"] = "31243"
request, err := zbClient.NewCreateInstanceCommand().BPMNProcessId("order-process-4").LatestVersion().VariablesFromMap(variables)
if err != nil {
panic(err)
}
result, err := request.Send(ctx)
if err != nil {
panic(err)
}
fmt.Println(result.String())
jobWorker := zbClient.NewJobWorker().JobType("payment-service").Handler(handleJob).Open()
<-readyClose
jobWorker.Close()
jobWorker.AwaitClose()
}
func handleJob(client worker.JobClient, job entities.Job) {
jobKey := job.GetKey()
headers, err := job.GetCustomHeadersAsMap()
if err != nil {
// failed to handle job as we require the custom job headers
failJob(client, job)
return
}
variables, err := job.GetVariablesAsMap()
if err != nil {
// failed to handle job as we require the variables
failJob(client, job)
return
}
variables["totalPrice"] = 46.50
request, err := client.NewCompleteJobCommand().JobKey(jobKey).VariablesFromMap(variables)
if err != nil {
// failed to set the updated variables
failJob(client, job)
return
}
log.Println("Complete job", jobKey, "of type", job.Type)
log.Println("Processing order:", variables["orderId"])
log.Println("Collect money using payment method:", headers["method"])
ctx := context.Background()
_, err = request.Send(ctx)
if err != nil {
panic(err)
}
log.Println("Successfully completed job")
close(readyClose)
}
func failJob(client worker.JobClient, job entities.Job) {
log.Println("Failed to complete job", job.GetKey())
ctx := context.Background()
_, err := client.NewFailJobCommand().JobKey(job.GetKey()).Retries(job.Retries - 1).Send(ctx)
if err != nil {
panic(err)
}
}
package main
import (
"context"
"fmt"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/entities"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/worker"
"github.com/camunda-community-hub/zeebe-client-go/v8/pkg/zbc"
"log"
"os"
)
const ZeebeAddr = "0.0.0.0:26500"
var readyClose = make(chan struct{})
func main() {
gatewayAddr := os.Getenv("ZEEBE_ADDRESS")
plainText:= false
if (gatewayAddr == "") {
gatewayAddr = ZeebeAddr
plainText = true
}
zbClient, err := zbc.NewClient(&zbc.ClientConfig{
GatewayAddress: gatewayAddr,
UsePlaintextConnection: plainText,
})
if err != nil {
panic(err)
}
// deploy process
ctx := context.Background()
response, err := zbClient.NewDeployResourceCommand().AddResourceFile("order-process-4.bpmn").Send(ctx)
if err != nil {
panic(err)
}
fmt.Println(response.String())
// create a new process instance
variables := make(map[string]interface{})
variables["orderId"] = "31243"
request, err := zbClient.NewCreateInstanceCommand().BPMNProcessId("order-process-4").LatestVersion().VariablesFromMap(variables)
if err != nil {
panic(err)
}
result, err := request.Send(ctx)
if err != nil {
panic(err)
}
fmt.Println(result.String())
jobWorker := zbClient.NewJobWorker().JobType("payment-service").Handler(handleJob).Open()
<-readyClose
jobWorker.Close()
jobWorker.AwaitClose()
}
func handleJob(client worker.JobClient, job entities.Job) {
jobKey := job.GetKey()
headers, err := job.GetCustomHeadersAsMap()
if err != nil {
// failed to handle job as we require the custom job headers
failJob(client, job)
return
}
variables, err := job.GetVariablesAsMap()
if err != nil {
// failed to handle job as we require the variables
failJob(client, job)
return
}
variables["totalPrice"] = 46.50
request, err := client.NewCompleteJobCommand().JobKey(jobKey).VariablesFromMap(variables)
if err != nil {
// failed to set the updated variables
failJob(client, job)
return
}
log.Println("Complete job", jobKey, "of type", job.Type)
log.Println("Processing order:", variables["orderId"])
log.Println("Collect money using payment method:", headers["method"])
ctx := context.Background()
_, err = request.Send(ctx)
if err != nil {
panic(err)
}
log.Println("Successfully completed job")
close(readyClose)
}
func failJob(client worker.JobClient, job entities.Job) {
log.Println("Failed to complete job", job.GetKey())
ctx := context.Background()
_, err := client.NewFailJobCommand().JobKey(job.GetKey()).Retries(job.Retries - 1).Send(ctx)
if err != nil {
panic(err)
}
}
In this example, we open a job worker for jobs of type payment-service
.
The job worker will repeatedly poll for new jobs of the type payment-service
and activate them subsequently. Each activated job will then be passed to the job handler, which implements the business logic of the job worker.
The handler will then complete the job with its result or fail the job if it encounters a problem while processing the job.
When observing the current state of the process in Operate, you can note the process instance moved from the first service task to the next one.
When you run the example above, note a similar output to the following:
key:2251799813685256 deployments:{process:{bpmnProcessId:"order-process-4" version:1 processDefinitionKey:2251799813685255 resourceName:"order-process.bpmn"}}
processDefinitionKey:2251799813685255 bpmnProcessId:"order-process-4" version:1 processInstanceKey:2251799813685257
2022/04/06 16:20:59 Complete job 2251799813685264 of type payment-service
2022/04/06 16:20:59 Processing order: 31243
2022/04/06 16:20:59 Collect money using payment method: VISA
2022/04/06 16:20:59 Successfully completed job
What's next?
- Learn more about the concepts behind Zeebe.
- Learn more about BPMN processes.