Go Worker
This guide shows you how to build a production-ready Priostack job worker in Go using only the standard library — no SDK required.
Prerequisites
- Go 1.21 or later
- A Priostack API key (set as
PRIOSTACK_API_KEYenvironment variable) - A deployed BPMN process with a Service Task of type
payment-processor
Complete Worker Implementation
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
const (
baseURL = "https://api.priostack.com"
workerType = "payment-processor"
pollTimeout = 30 * time.Second
maxJobs = 5
)
type Job struct {
Key int64 `json:"key"`
Type string `json:"type"`
ProcessInstanceKey int64 `json:"processInstanceKey"`
Variables map[string]interface{} `json:"variables"`
Retries int `json:"retries"`
}
type ActivateResponse struct {
Jobs []Job `json:"jobs"`
}
type PriostackClient struct {
httpClient *http.Client
apiKey string
baseURL string
}
func NewClient(apiKey, baseURL string) *PriostackClient {
return &PriostackClient{
httpClient: &http.Client{Timeout: 35 * time.Second},
apiKey: apiKey,
baseURL: baseURL,
}
}
func (c *PriostackClient) post(ctx context.Context, path string, body interface{}) (*http.Response, error) {
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+path, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("new request: %w", err)
}
req.Header.Set("X-API-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
return c.httpClient.Do(req)
}
func (c *PriostackClient) ActivateJobs(ctx context.Context) ([]Job, error) {
resp, err := c.post(ctx, "/api/v1/jobs/activate", map[string]interface{}{
"type": workerType,
"maxJobsToActivate": maxJobs,
"requestTimeout": pollTimeout.Milliseconds(),
"fetchVariables": []string{"orderId", "amount", "currency"},
})
if err != nil {
return nil, fmt.Errorf("activate: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("activate status %d: %s", resp.StatusCode, body)
}
var result ActivateResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
return result.Jobs, nil
}
func (c *PriostackClient) CompleteJob(ctx context.Context, jobKey int64, variables map[string]interface{}) error {
path := fmt.Sprintf("/api/v1/jobs/%d/complete", jobKey)
resp, err := c.post(ctx, path, map[string]interface{}{"variables": variables})
if err != nil {
return fmt.Errorf("complete: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("complete status %d: %s", resp.StatusCode, body)
}
return nil
}
func (c *PriostackClient) FailJob(ctx context.Context, jobKey int64, retries int, errMsg string) error {
path := fmt.Sprintf("/api/v1/jobs/%d/fail", jobKey)
resp, err := c.post(ctx, path, map[string]interface{}{
"retries": retries - 1,
"errorMessage": errMsg,
"retryBackoff": 5000,
})
if err != nil {
return fmt.Errorf("fail: %w", err)
}
defer resp.Body.Close()
return nil
}
// processPayment is your actual business logic
func processPayment(job Job) (map[string]interface{}, error) {
orderId, _ := job.Variables["orderId"].(string)
amount, _ := job.Variables["amount"].(float64)
log.Printf("Processing payment for order %s, amount %.2f", orderId, amount)
// TODO: call your actual payment gateway here
// For now, simulate success
return map[string]interface{}{
"transactionId": fmt.Sprintf("txn_%s", orderId),
"paymentStatus": "success",
"processedAt": time.Now().UTC().Format(time.RFC3339),
}, nil
}
func runWorker(ctx context.Context, client *PriostackClient) {
log.Printf("Worker started. Polling for jobs of type %q...", workerType)
for {
select {
case <-ctx.Done():
log.Println("Worker shutting down.")
return
default:
}
jobs, err := client.ActivateJobs(ctx)
if err != nil {
if ctx.Err() != nil {
return // context cancelled
}
log.Printf("Error activating jobs: %v. Retrying in 5s...", err)
select {
case <-time.After(5 * time.Second):
case <-ctx.Done():
return
}
continue
}
for _, job := range jobs {
job := job // capture for goroutine
go func() {
log.Printf("Activated job %d (instance %d)", job.Key, job.ProcessInstanceKey)
result, err := processPayment(job)
if err != nil {
log.Printf("Job %d failed: %v", job.Key, err)
if failErr := client.FailJob(ctx, job.Key, job.Retries, err.Error()); failErr != nil {
log.Printf("Failed to report job failure: %v", failErr)
}
return
}
if err := client.CompleteJob(ctx, job.Key, result); err != nil {
log.Printf("Failed to complete job %d: %v", job.Key, err)
return
}
log.Printf("Job %d completed successfully", job.Key)
}()
}
}
}
func main() {
apiKey := os.Getenv("PRIOSTACK_API_KEY")
if apiKey == "" {
log.Fatal("PRIOSTACK_API_KEY environment variable is required")
}
client := NewClient(apiKey, baseURL)
ctx, cancel := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer cancel()
runWorker(ctx, client)
log.Println("Worker stopped.")
}
Running the Worker
# Set your API key
export PRIOSTACK_API_KEY=ps_live_your_key_here
# Run the worker
go run worker.go
# Output:
# 2026/05/01 14:00:00 Worker started. Polling for jobs of type "payment-processor"...
# 2026/05/01 14:00:05 Activated job 2251799813685290 (instance 2251799813685281)
# 2026/05/01 14:00:05 Processing payment for order ORD-001, amount 149.99
# 2026/05/01 14:00:05 Job 2251799813685290 completed successfully
Production Deployment Tips
| Concern | Recommendation |
|---|---|
| Scaling | Run multiple instances of the worker binary. Each instance polls independently. Use Kubernetes HPA to scale based on pending job count. |
| Observability | Use structured logging (log/slog in Go 1.21+). Emit metrics for job processing time, error rate, and throughput. |
| Retries | Decrease retries by 1 each time you call fail. Use retryBackoff for exponential backoff between retries. |
| Timeouts | Set processing timeouts shorter than the job deadline. If you exceed the deadline, the job returns to the queue and may be double-processed. |
| Secrets | Inject PRIOSTACK_API_KEY via Kubernetes Secrets or a secrets manager. Never bake it into the Docker image. |
Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o worker ./worker.go
FROM alpine:3.19
COPY --from=builder /app/worker /worker
ENTRYPOINT ["/worker"]