Skip to main content

Search Attributes

A simple example inspired by temporalio/samples-go/searchattributes

example.proto
syntax = "proto3";

package example.searchattributes.v1;

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "temporal/v1/temporal.proto";

service Example {
option (temporal.v1.service) = {task_queue: "searchattributes"};

rpc SearchAttributes(SearchAttributesInput) returns (google.protobuf.Empty) {
option (temporal.v1.workflow) = {
id: 'search_attributes_${! uuid_v4() }'
search_attributes:
'CustomKeywordField = customKeywordField \n'
'CustomTextField = customTextField \n'
'CustomIntField = customIntField.int64() \n'
'CustomDoubleField = customDoubleField \n'
'CustomBoolField = customBoolField \n'
'CustomDatetimeField = customDatetimeField.ts_parse("2006-01-02T15:04:05Z") \n'
};
}

rpc TypedSearchAttributes(TypedSearchAttributesInput) returns (TypedSearchAttributesOutput) {
option (temporal.v1.workflow) = {
id: 'searchattributes.v1.TypedSearchAttributes/${! uuid_v4() }'
typed_search_attributes:
'keyword.CustomKeywordField = customKeywordField \n'
'string.CustomTextField = customTextField \n'
'int64.CustomIntField = customIntField.int64() \n'
'float64.CustomDoubleField = customDoubleField \n'
'bool.CustomBoolField = customBoolField \n'
'time.CustomDatetimeField = customDatetimeField.ts_parse("2006-01-02T15:04:05Z") \n'
'keyword_list.CustomKeywordListField = customKeywordListField \n'
};
}
}

message SearchAttributesInput {
string custom_keyword_field = 1;
string custom_text_field = 2;
int64 custom_int_field = 3;
double custom_double_field = 4;
bool custom_bool_field = 5;
google.protobuf.Timestamp custom_datetime_field = 6;
}

message TypedSearchAttributesInput {
string custom_keyword_field = 1;
string custom_text_field = 2;
int64 custom_int_field = 3;
double custom_double_field = 4;
bool custom_bool_field = 5;
google.protobuf.Timestamp custom_datetime_field = 6;
repeated string custom_keyword_list_field = 7;
}

message TypedSearchAttributesOutput {
string custom_keyword_field = 1;
string custom_text_field = 2;
int64 custom_int_field = 3;
double custom_double_field = 4;
bool custom_bool_field = 5;
google.protobuf.Timestamp custom_datetime_field = 6;
repeated string custom_keyword_list_field = 7;
}
main.go
package main

import (
"log"
"os"
"strings"
"time"

searchattributesv1 "github.com/cludden/protoc-gen-go-temporal/gen/example/searchattributes/v1"
"github.com/urfave/cli/v2"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/converter"
tlog "go.temporal.io/sdk/log"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
"google.golang.org/protobuf/types/known/timestamppb"
)

var (
customKeywordField = temporal.NewSearchAttributeKeyKeyword("CustomKeywordField")
customKeywordListField = temporal.NewSearchAttributeKeyKeywordList("CustomKeywordListField")
customTextField = temporal.NewSearchAttributeKeyString("CustomTextField")
customIntField = temporal.NewSearchAttributeKeyInt64("CustomIntField")
customDoubleField = temporal.NewSearchAttributeKeyFloat64("CustomDoubleField")
customBoolField = temporal.NewSearchAttributeKeyBool("CustomBoolField")
customDatetimeField = temporal.NewSearchAttributeKeyTime("CustomDatetimeField")
)

func main() {
app, err := searchattributesv1.NewExampleCli(
searchattributesv1.NewExampleCliOptions().WithWorker(func(cmd *cli.Context, c client.Client) (worker.Worker, error) {
w := worker.New(c, searchattributesv1.ExampleTaskQueue, worker.Options{})
searchattributesv1.RegisterExampleWorkflows(w, &Workflows{})
return w, nil
}),
)
if err != nil {
log.Fatal(err)
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

type Workflows struct{}

type SearchAttributesWorkflow struct {
*searchattributesv1.SearchAttributesWorkflowInput
log tlog.Logger
}

func (w *Workflows) SearchAttributes(
ctx workflow.Context,
input *searchattributesv1.SearchAttributesWorkflowInput,
) (searchattributesv1.SearchAttributesWorkflow, error) {
return &SearchAttributesWorkflow{input, workflow.GetLogger(ctx)}, nil
}

func (w *SearchAttributesWorkflow) Execute(ctx workflow.Context) (err error) {
sa := workflow.GetInfo(ctx).SearchAttributes
for _, attr := range strings.Split("CustomBoolField,CustomDatetimeField,CustomDoubleField,CustomIntField,CustomKeywordField,CustomTextField", ",") {
if p, ok := sa.IndexedFields[attr]; ok {
switch attr {
case "CustomBoolField":
var result bool
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
case "CustomDatetimeField":
var result time.Time
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
case "CustomDoubleField":
var result float64
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
case "CustomIntField":
var result int
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
case "CustomKeywordField":
var result string
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
case "CustomTextField":
var result string
err = converter.GetDefaultDataConverter().FromPayload(p, &result)
w.log.Info("search attribute", "name", attr, "value", result, "error", err)
}
}
}

return nil
}

type TypedSearchAttributesWorkflow struct {
*searchattributesv1.TypedSearchAttributesWorkflowInput
log tlog.Logger
}

func (w *Workflows) TypedSearchAttributes(
ctx workflow.Context, input *searchattributesv1.TypedSearchAttributesWorkflowInput,
) (searchattributesv1.TypedSearchAttributesWorkflow, error) {
return &TypedSearchAttributesWorkflow{input, workflow.GetLogger(ctx)}, nil
}

func (w *TypedSearchAttributesWorkflow) Execute(
ctx workflow.Context,
) (*searchattributesv1.TypedSearchAttributesOutput, error) {
out := &searchattributesv1.TypedSearchAttributesOutput{}
sa := workflow.GetTypedSearchAttributes(ctx)
if v, ok := sa.GetKeyword(customKeywordField); ok {
out.CustomKeywordField = v
}
if v, ok := sa.GetFloat64(customDoubleField); ok {
out.CustomDoubleField = v
}
if v, ok := sa.GetBool(customBoolField); ok {
out.CustomBoolField = v
}
if v, ok := sa.GetTime(customDatetimeField); ok {
out.CustomDatetimeField = timestamppb.New(v)
}
if v, ok := sa.GetKeywordList(customKeywordListField); ok {
out.CustomKeywordListField = v
}
if v, ok := sa.GetString(customTextField); ok {
out.CustomTextField = v
}
if v, ok := sa.GetInt64(customIntField); ok {
out.CustomIntField = v
}
return out, nil
}

Run this example

  1. Clone the examples
    git clone https://github.com/cludden/protoc-gen-go-temporal && cd protoc-gen-go-temporal
  2. Run a local Temporal server
    temporal server start-dev
  3. In a different shell, register custom search attributes and run the example worker
    temporal operator search-attribute create --name CustomDatetimeField --type Datetime
    temporal operator search-attribute create --name CustomKeywordField --type Keyword
    temporal operator search-attribute create --name CustomTextField --type Text
    temporal operator search-attribute create --name CustomIntField --type Int
    temporal operator search-attribute create --name CustomDoubleField --type Double
    temporal operator search-attribute create --name CustomBoolField --type Bool
    go run examples/searchattributes/main.go worker
  4. In a different shell, execute the workflow
    go run examples/searchattributes/main.go search-attributes \
    --custom-datetime-field=2024-01-01T00:00:00Z \
    --custom-keyword-field=foo-bar \
    --custom-text-field=foo-bar \
    --custom-int-field=42 \
    --custom-double-field=42 \
    --custom-bool-field=true