{"page":{"agent_metadata":{"content_type":"guide","outputs":["build-temporal-workflow-in-go","implement-di-for-activities","write-idempotent-activities","run-temporal-worker"],"prerequisites":["go-basics","temporal-concepts","temporal-namespaces"]},"categories":["workflow-orchestration"],"content_plain":"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.\nAll code lives in the companion repo at github.com/statherm/temporal-examples. For background, see Introduction to Temporal and Namespaces and Task Queues.\nProject Structure# The companion repo organizes code by domain:\ntemporal-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 MakefileWorkflows 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:\nfunc 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, \u0026amp;result) return result, err } func Greet(ctx context.Context, name string) (string, error) { return fmt.Sprintf(\u0026#34;Hello, %s!\u0026#34;, 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.\nDependency 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:\n// 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 \u0026amp;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(\u0026#34;inspect container %s: %w\u0026#34;, req.ContainerID, err) } if info.State == \u0026#34;exited\u0026#34; { 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.\nIdempotency 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.\nThe check-before-act pattern is shown in StopContainer above \u0026ndash; check if the container is already stopped before acting. For create operations, use idempotency keys generated from the workflow (not inside the activity):\nfunc ProvisionWorkflow(ctx workflow.Context, spec ResourceSpec) error { key := fmt.Sprintf(\u0026#34;%s-create\u0026#34;, 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.\n// cmd/worker/main.go func main() { c, err := client.Dial(client.Options{ HostPort: getEnv(\u0026#34;TEMPORAL_HOST\u0026#34;, \u0026#34;localhost:7233\u0026#34;), Namespace: getEnv(\u0026#34;TEMPORAL_NAMESPACE\u0026#34;, \u0026#34;default\u0026#34;), }) if err != nil { log.Fatalln(\u0026#34;Unable to create client:\u0026#34;, err) } defer c.Close() dockerClient, err := container.NewDockerClient() if err != nil { log.Fatalln(\u0026#34;Unable to create Docker client:\u0026#34;, err) } containerActivities := container.NewContainerActivities(dockerClient) w := worker.New(c, \u0026#34;container-ops\u0026#34;, worker.Options{ MaxConcurrentActivityExecutionSize: 20, }) w.RegisterWorkflow(container.CleanupWorkflow) w.RegisterActivity(containerActivities) log.Println(\u0026#34;Starting worker on container-ops\u0026#34;) err = w.Run(worker.InterruptCh()) if err != nil { log.Fatalln(\u0026#34;Worker failed:\u0026#34;, err) } }w.RegisterActivity(containerActivities) registers every exported method on the struct as an activity. worker.InterruptCh() handles SIGINT/SIGTERM for graceful shutdown.\nThe Cleanup Workflow# A real workflow using both patterns \u0026ndash; stopping and removing containers with error handling:\nfunc CleanupWorkflow(ctx workflow.Context, req CleanupRequest) (CleanupResult, error) { ao := workflow.ActivityOptions{ StartToCloseTimeout: 30 * time.Second, RetryPolicy: \u0026amp;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: \u0026#34;cleanup\u0026#34;, }).Get(ctx, nil) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf(\u0026#34;stop %s: %v\u0026#34;, 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(\u0026#34;remove %s: %v\u0026#34;, id, err)) } else { result.Removed++ } } return result, nil }The nil activities variable is a Go SDK convention \u0026ndash; the workflow uses method references to identify activity types. The actual struct with injected dependencies runs on the worker.\nStarting a Workflow# With the CLI:\ntemporal workflow start \\ --task-queue container-ops \\ --type CleanupWorkflow \\ --workflow-id cleanup-001 \\ --input \u0026#39;{\u0026#34;ContainerIDs\u0026#34;: [\u0026#34;abc123\u0026#34;, \u0026#34;def456\u0026#34;], \u0026#34;Force\u0026#34;: false}\u0026#39; temporal workflow show --workflow-id cleanup-001With the Go SDK:\nwe, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{ ID: \u0026#34;cleanup-001\u0026#34;, TaskQueue: \u0026#34;container-ops\u0026#34;, }, container.CleanupWorkflow, req) if err != nil { log.Fatalln(\u0026#34;Unable to start workflow:\u0026#34;, err) } var result container.CleanupResult err = we.Get(context.Background(), \u0026amp;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.\nThe Complete Flow# make temporal-up # Start Temporal on minikube kubectl port-forward -n temporal svc/temporal-frontend 7233:7233 \u0026amp; make run-worker # Build and start worker (foreground) make start-cleanup # In another terminal, start workflowThe 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.\nWhat\u0026rsquo;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 \u0026ndash; see Temporal Workflow Testing ","date":"2026-02-22","description":"Build a complete Temporal workflow in Go with dependency injection for testability, idempotent activities for safe retries, and a production-ready worker binary.","lastmod":"2026-02-22","levels":["beginner"],"reading_time_minutes":5,"section":"knowledge","skills":["temporal-go-development","dependency-injection","idempotent-activity-design","temporal-worker-setup"],"tags":["temporal","go","workflow","dependency-injection","idempotency","worker","testing"],"title":"Your First Temporal Workflow in Go: DI, Idempotency, and the Worker Pattern","tools":["temporal","go"],"url":"https://agent-zone.ai/knowledge/workflow-orchestration/temporal-go-workflow-basics/","word_count":897}}