---
title: "Your First Temporal Workflow in Go: DI, Idempotency, and the Worker Pattern"
description: "Build a complete Temporal workflow in Go with dependency injection for testability, idempotent activities for safe retries, and a production-ready worker binary."
url: https://agent-zone.ai/knowledge/workflow-orchestration/temporal-go-workflow-basics/
section: knowledge
date: 2026-02-22
categories: ["workflow-orchestration"]
tags: ["temporal","go","workflow","dependency-injection","idempotency","worker","testing"]
skills: ["temporal-go-development","dependency-injection","idempotent-activity-design","temporal-worker-setup"]
tools: ["temporal","go"]
levels: ["beginner"]
word_count: 897
formats:
  json: https://agent-zone.ai/knowledge/workflow-orchestration/temporal-go-workflow-basics/index.json
  html: https://agent-zone.ai/knowledge/workflow-orchestration/temporal-go-workflow-basics/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Your+First+Temporal+Workflow+in+Go%3A+DI%2C+Idempotency%2C+and+the+Worker+Pattern
---


# Your First Temporal Workflow in Go

This article establishes the patterns used throughout the Temporal series: dependency injection for testable activities, idempotency for safe retries, and a clean worker binary. Every subsequent article builds on these foundations.

All code lives in the companion repo at [github.com/statherm/temporal-examples](https://github.com/statherm/temporal-examples). For background, see [Introduction to Temporal](../temporal-introduction/) and [Namespaces and Task Queues](../temporal-namespaces-task-queues/).

## Project Structure

The companion repo organizes code by domain:

```
temporal-examples/
  cmd/worker/main.go         # Worker binary
  cmd/starter/main.go        # Workflow starter CLI
  internal/container/
    activities.go             # Activity implementations with DI
    workflow.go               # Workflow definitions
    types.go                  # Interfaces and types
  Makefile
```

## Workflows and Activities

A workflow is a deterministic function that orchestrates work. It takes `workflow.Context`, must not perform side effects, and dispatches work through activities. Activities use standard `context.Context` and perform real I/O:

```go
func HelloWorkflow(ctx workflow.Context, name string) (string, error) {
    ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
        StartToCloseTimeout: 10 * time.Second,
    })
    var result string
    err := workflow.ExecuteActivity(ctx, Greet, name).Get(ctx, &result)
    return result, err
}

func Greet(ctx context.Context, name string) (string, error) {
    return fmt.Sprintf("Hello, %s!", name), nil
}
```

They are separate because: (1) activity results are recorded and replayed, preventing duplicate side effects; (2) failed activities retry independently; (3) each has its own timeout and retry policy.

## Dependency Injection Pattern

If your activity calls a Docker API, unit tests should not need a running daemon. Define an interface, hold it in a struct, and inject it through a constructor:

```go
// internal/container/types.go
type ContainerClient interface {
    Inspect(ctx context.Context, id string) (ContainerInfo, error)
    Stop(ctx context.Context, id string) error
    Remove(ctx context.Context, id string) error
}

// internal/container/activities.go
type ContainerActivities struct {
    client ContainerClient
}

func NewContainerActivities(c ContainerClient) *ContainerActivities {
    return &ContainerActivities{client: c}
}

func (a *ContainerActivities) StopContainer(ctx context.Context, req StopRequest) error {
    info, err := a.client.Inspect(ctx, req.ContainerID)
    if err != nil {
        return fmt.Errorf("inspect container %s: %w", req.ContainerID, err)
    }
    if info.State == "exited" {
        return nil // already stopped -- idempotent
    }
    return a.client.Stop(ctx, req.ContainerID)
}

func (a *ContainerActivities) RemoveContainer(ctx context.Context, id string) error {
    _, err := a.client.Inspect(ctx, id)
    if err != nil {
        return nil // does not exist -- treat as success
    }
    return a.client.Remove(ctx, id)
}
```

This gives you testability (mock `ContainerClient`), swappable implementations (Docker, Podman, cloud API), and clean separation from runtime-specific libraries.

## Idempotency Pattern

Temporal retries failed activities. If an activity succeeds but the worker crashes before reporting, Temporal retries it. Activities must handle being called multiple times.

The **check-before-act** pattern is shown in `StopContainer` above -- check if the container is already stopped before acting. For create operations, use **idempotency keys** generated from the workflow (not inside the activity):

```go
func ProvisionWorkflow(ctx workflow.Context, spec ResourceSpec) error {
    key := fmt.Sprintf("%s-create", workflow.GetInfo(ctx).WorkflowExecution.ID)
    // ... pass key to activity so retries always use the same key
}
```

## The Worker Binary

The worker wires everything together: Temporal client, real dependencies, activity registration, and polling.

```go
// cmd/worker/main.go
func main() {
    c, err := client.Dial(client.Options{
        HostPort:  getEnv("TEMPORAL_HOST", "localhost:7233"),
        Namespace: getEnv("TEMPORAL_NAMESPACE", "default"),
    })
    if err != nil {
        log.Fatalln("Unable to create client:", err)
    }
    defer c.Close()

    dockerClient, err := container.NewDockerClient()
    if err != nil {
        log.Fatalln("Unable to create Docker client:", err)
    }

    containerActivities := container.NewContainerActivities(dockerClient)

    w := worker.New(c, "container-ops", worker.Options{
        MaxConcurrentActivityExecutionSize: 20,
    })

    w.RegisterWorkflow(container.CleanupWorkflow)
    w.RegisterActivity(containerActivities)

    log.Println("Starting worker on container-ops")
    err = w.Run(worker.InterruptCh())
    if err != nil {
        log.Fatalln("Worker failed:", err)
    }
}
```

`w.RegisterActivity(containerActivities)` registers every exported method on the struct as an activity. `worker.InterruptCh()` handles SIGINT/SIGTERM for graceful shutdown.

## The Cleanup Workflow

A real workflow using both patterns -- stopping and removing containers with error handling:

```go
func CleanupWorkflow(ctx workflow.Context, req CleanupRequest) (CleanupResult, error) {
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: 30 * time.Second,
        RetryPolicy: &temporal.RetryPolicy{
            InitialInterval:    time.Second,
            BackoffCoefficient: 2.0,
            MaximumAttempts:    3,
        },
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    result := CleanupResult{}
    var activities *ContainerActivities

    for _, id := range req.ContainerIDs {
        err := workflow.ExecuteActivity(ctx, activities.StopContainer, StopRequest{
            ContainerID: id, Reason: "cleanup",
        }).Get(ctx, nil)
        if err != nil {
            result.Errors = append(result.Errors, fmt.Sprintf("stop %s: %v", id, err))
            continue
        }
        result.Stopped++

        err = workflow.ExecuteActivity(ctx, activities.RemoveContainer, id).Get(ctx, nil)
        if err != nil {
            result.Errors = append(result.Errors, fmt.Sprintf("remove %s: %v", id, err))
        } else {
            result.Removed++
        }
    }
    return result, nil
}
```

The nil `activities` variable is a Go SDK convention -- the workflow uses method references to identify activity types. The actual struct with injected dependencies runs on the worker.

## Starting a Workflow

**With the CLI:**

```bash
temporal workflow start \
  --task-queue container-ops \
  --type CleanupWorkflow \
  --workflow-id cleanup-001 \
  --input '{"ContainerIDs": ["abc123", "def456"], "Force": false}'

temporal workflow show --workflow-id cleanup-001
```

**With the Go SDK:**

```go
we, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{
    ID:        "cleanup-001",
    TaskQueue: "container-ops",
}, container.CleanupWorkflow, req)
if err != nil {
    log.Fatalln("Unable to start workflow:", err)
}

var result container.CleanupResult
err = we.Get(context.Background(), &result)
```

The workflow `ID` must be unique among active workflows in the namespace. Use a meaningful business key (order ID, job ID) so you can look up workflows directly.

## The Complete Flow

```bash
make temporal-up                    # Start Temporal on minikube
kubectl port-forward -n temporal svc/temporal-frontend 7233:7233 &
make run-worker                     # Build and start worker (foreground)
make start-cleanup                  # In another terminal, start workflow
```

The worker connects, registers on `container-ops`, and polls. The starter creates a workflow execution. Temporal dispatches tasks to the worker. Each activity result is recorded in history. If the worker crashes and restarts, Temporal replays the workflow and skips completed activities.

## What's Next

- **Multi-stage workflows**: Branching logic, parallel execution with `workflow.Go`, error compensation
- **Signals and queries**: Send data to running workflows and read state without waiting
- **Testing**: Unit test workflows with mock activities -- see [Temporal Workflow Testing](../temporal-workflow-testing/)

