Writing Workers with the Go SDK
June 22, 2026 ยท View on GitHub
A worker is responsible for executing a task. Operator and System tasks are handled by the Conductor server, while user defined tasks needs to have a worker created that awaits the work to be scheduled by the server for it to be executed.
Worker framework provides features such as polling threads, metrics and server communication.
Design Principles for Workers
Each worker embodies design pattern and follows certain basic principles:
- Workers are stateless and do not implement a workflow specific logic.
- Each worker executes a very specific task and produces well-defined output given specific inputs.
- Workers are meant to be idempotent (or should handle cases where the task that partially executed gets rescheduled due to timeouts etc.)
- Workers do not implement the logic to handle retries etc, that is taken care by the Conductor server.
Creating Task Workers
Task worker is implemented using a function that confirms to the following function
type ExecuteTaskFunction func(t *Task) (interface{}, error)
Worker returns a struct as the output of the task execution. The struct MUST be serializable to a JSON map.
If an error is returned, the task is marked as FAILED
Task worker that returns a struct
//TaskOutput struct that represents the output of the task execution
type TaskOutput struct {
Keys []string
Message string
Value float64
}
//SimpleWorker function accepts Task as input and returns TaskOutput as result
//If there is a failure, error can be returned and the task will be marked as FAILED
func SimpleWorker(t *model.Task) (interface{}, error) {
taskResult := &TaskOutput{
Keys: []string{"Key1", "Key2"},
Message: "Hello World",
Value: rand.ExpFloat64(),
}
return taskResult, nil
}
Controlling execution for long-running tasks
For the long-running tasks you might want to spawn another process/routine and update the status of the task at a later point and complete the
execution function without actually marking the task as COMPLETED. Use TaskResult struct that allows you to specify more fined grained control.
Here is an example of a task execution function that returns with IN_PROGRESS status asking server to push the task again in 60 seconds.
func LongRunningTaskWorker(t *model.Task) (interface{}, error) {
taskResult := model.NewTaskResult(t)
taskResult.OutputData = map[string]interface{}{}
//Keep the status as IN_PROGRESS
taskResult.Status = task_result_status.IN_PROGRESS
//Time after which the task should be sent back to worker
taskResult.CallbackAfterSeconds = 60
return taskResult, nil
}
Type-safe Worker API and contextual execution (Go 1.23+)
The SDK includes a new worker API that adds:
- Type-safe handlers via generics:
TypedWorker[TIn, TOut] - Contextual execution:
TaskContextwith workflow/task metadata - Clear per-task configuration via options:
WithBatchSize,WithPollInterval,WithPollTimeout,WithDomain,WithBaseContext - Unified registration using
RegisterWorker/RegisterWorkers
Worker with options
api := client.NewAPIClientFromEnv()
runner := worker.NewTaskRunnerWithApiClient(api)
w := worker.NewWorker(
"greet",
func(t *model.Task) (interface{}, error) {
name := fmt.Sprintf("%v", t.InputData["person_to_be_greated"]) // map input to your task
return map[string]any{"hello": "Hello, " + name}, nil
},
worker.WithBatchSize(2),
worker.WithPollInterval(250*time.Millisecond),
worker.WithPollTimeout(5*time.Second), // negative uses server default; zero leaves unchanged
worker.WithDomain("dev"),
)
if err := runner.RegisterWorker(w); err != nil {
panic(err)
}
runner.WaitWorkers()
TypedWorker with structured I/O and TaskContext
type GreetIn struct {
Name string `json:"person_to_be_greated"`
}
type GreetOut struct {
Hello string `json:"hello"`
}
api := client.NewAPIClientFromEnv()
runner := worker.NewTaskRunnerWithApiClient(api)
tw := worker.NewTypedWorker[GreetIn, GreetOut](
"greet",
func(ctx worker.TaskContext, in GreetIn) (GreetOut, error) {
// Access metadata when needed
_ = ctx.GetWorkflowInstanceID()
_ = ctx.GetTaskType()
return GreetOut{Hello: "Hello, " + in.Name}, nil
},
worker.WithBatchSize(1),
worker.WithPollInterval(100*time.Millisecond),
)
if err := runner.RegisterWorker(tw); err != nil {
panic(err)
}
runner.WaitWorkers()
Prefer NewSimpleTypedWorker if you want a func(context.Context, TIn) signature.
Register multiple workers
err := runner.RegisterWorkers(
worker.NewWorker("a", func(t *model.Task) (interface{}, error) { return map[string]any{"ok": true}, nil }),
worker.NewTypedWorker[In, Out]("b", func(ctx worker.TaskContext, in In) (Out, error) { return Out{}, nil }),
)
if err != nil { panic(err) }
TaskContext reference
TaskContext extends context.Context and exposes:
WorkflowInstanceID() stringWorkflowType() stringTaskID() stringTaskType() stringRetryCount() intRetriedTaskID() stringPollCount() int
Starting Workers
TaskRunner interface is used to start the workers, which takes care of polling server for the work, executing worker code and updating the results back to the server.
apiClient := client.NewAPIClient(
settings.NewAuthenticationSettings(
KEY,
SECRET,
),
settings.NewHttpSettings(
"https://play.orkes.io/api",
))
taskRunner := worker.NewTaskRunnerWithApiClient(apiClient)
//Start polling for a task by name "simple_task", with a batch size of 1 and 1 second interval
//Between polls if there are no tasks available to execute
taskRunner.StartWorker("simple_task", examples.SimpleWorker, 1, time.Second*1)
//Add more StartWorker calls as needed
//Block
taskRunner.WaitWorkers()
Task Management APIs
Get Task Details
task, err := executor.GetTask(taskId)
Updating the Task result outside the worker implementation
Update task by id
output := &TaskOutput{
Keys: []string{"Key1", "Key2"},
Message: "Hello World",
Value: rand.ExpFloat64(),
}
executor.UpdateTask(taskId, workflowInstanceId, task_result_status.COMPLETED, output)
Update task by Reference Name
output := &TaskOutput{
Keys: []string{"Key1", "Key2"},
Message: "Hello World",
Value: rand.ExpFloat64(),
}
executor.UpdateTaskByRefName("task_ref_name", workflowInstanceId, task_result_status.COMPLETED, output)
Worker Metrics
The SDK uses Prometheus for metrics collection. When
enabled, the worker starts an HTTP server to publish metrics (default: port 2112
on /metrics).
go metrics.ProvideMetrics(settings.NewDefaultMetricsSettings())
See metrics.md for the complete metrics reference, including the
legacy and canonical metric catalogs, the WORKER_CANONICAL_METRICS environment
variable, and migration guidance.