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_KEY environment 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

ConcernRecommendation
ScalingRun multiple instances of the worker binary. Each instance polls independently. Use Kubernetes HPA to scale based on pending job count.
ObservabilityUse structured logging (log/slog in Go 1.21+). Emit metrics for job processing time, error rate, and throughput.
RetriesDecrease retries by 1 each time you call fail. Use retryBackoff for exponential backoff between retries.
TimeoutsSet processing timeouts shorter than the job deadline. If you exceed the deadline, the job returns to the queue and may be double-processed.
SecretsInject 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"]