Skip to main content

Go SDK

The Go SDK lets you write Dagger modules in Go. You define plain Go structs and methods; the SDK turns them into Dagger objects and functions that anyone can call from the CLI, from another module, or over the API. In return, your module gets a generated, fully-typed Go client (dag) for the entire Dagger API — containers, directories, files, secrets, services, and every module you depend on.

This page is a standalone guide to the Go SDK. It assumes you already understand the platform concepts covered in the Module Developer Guide:

  • Type system — how SDK types map to the Dagger API
  • Checks — functions that validate your code
  • Changesets — how generators and tooling return diffs for you to apply

A useful, reusable module provides at least one of the three first-class function types — a check, a generator, or a service. See Checks, generators, services, directives, and ignore patterns for the Go syntax.

The Go SDK is itself a Dagger module — github.com/dagger/go-sdk. Install it into your workspace once, then scaffold, generate, and maintain Go modules with Dagger's SDK and module commands:

# Install the Go SDK into your workspace (once)
dagger sdk install go

# Create a Go module
dagger module init go my-module

Create a module

note

Run these commands from inside a Git repository — that's where the new module is created.

Install the Go SDK into your workspace, then create a new module. The required arguments are the SDK and module name:

dagger sdk install go
dagger module init go my-module

Like every Dagger tool that modifies your workspace, dagger module init returns a changeset — a structured diff of the files to create — which Dagger shows you to review before anything is written to disk.

tip

If the repository does not have a dagger.toml yet, use dagger sdk install --here go to create it in the current directory.

Where the module is created

By default, dagger module init places the new module under the workspace root:

<workspace>/.dagger/modules/<name>

Pass --path to choose a different location (the target must not already contain a Dagger module):

dagger module init go my-module --path ./ci

A custom path is registered as a module authored by the SDK, but is not automatically installed as a callable workspace module. Use dagger install ./ci to install it too.

Pass --legacy-template to use the Go SDK's legacy starter instead of its default minimal main.go:

dagger module init go my-module --legacy-template

List the Go SDK's module initialization options with:

dagger sdk module-options go

Resulting file layout

Once initialized and generated, a Go module looks like this:

my-module/
├── dagger-module.toml
├── go.mod
├── go.sum
├── main.go # your code
├── dagger.gen.go # generated: top-level helpers
├── .gitattributes # marks generated files for linguist
├── .gitignore # ignores local-only files such as .env
└── internal/
└── dagger/ # generated: the typed Dagger client
├── dagger.gen.go
└── my-module.gen.go
note

dagger module init writes dagger-module.toml and main.go and updates the workspace config. After accepting those changes, run dagger generate once to create go.mod, go.sum, and the generated Go files shown above. Commit the generated Go files along with the rest of the module. Dagger needs the generated client to load the module, both from your local checkout and from a Git reference.

The module config records the runtime separately from the SDK that authors it:

dagger-module.toml
name = "my-module"
engineVersion = "v1.0.0-beta.6"

[runtime]
source = "go"

runtime.source = "go" tells the engine to use the Go runtime. The workspace's dagger.toml separately records the module under modules.dagger-go-sdk.as-sdk; that authoring relationship is how Go binding generation discovers workspace modules. The generated files in dagger.gen.go and internal/ are not handwritten; see Regenerate bindings.

Define objects and functions

A Go module is an ordinary Go package named main. The main object is a struct whose name matches your module (Dagger PascalCases the module name): a module named my-module has a MyModule struct. Every exported method on that struct becomes a callable Dagger Function.

main.go
// A simple example module to say hello.

// Further documentation for the module here.

package main

import (
"fmt"
"strings"
)

type MyModule struct{}

// Return a greeting.
func (m *MyModule) Hello(
// Who to greet
name string,
// The greeting to display
greeting string,
) string {
return fmt.Sprintf("%s, %s!", greeting, name)
}

// Return a loud greeting.
func (m *MyModule) LoudHello(
// Who to greet
name string,
// The greeting to display
greeting string,
) string {
out := fmt.Sprintf("%s, %s!", greeting, name)
return strings.ToUpper(out)
}

Key rules:

  • The package is always package main.
  • Method receivers may be value (func (m MyModule)) or pointer (func (m *MyModule)) — be consistent within a type.
  • Exported (capitalized) methods become Dagger Functions. Unexported methods are private Go helpers, invisible to callers.
  • The first parameter may be a context.Context; it does not appear as a Dagger argument and is supplied by the runtime.
  • A method may return an error as its last value; a non-nil error fails the function.

Call your functions exactly like any other module — from the directory that contains dagger-module.toml (or with -m <path>):

dagger api call hello --name=World --greeting=Hello
# Hello, World!

dagger api call loud-hello --name=World --greeting=Hello
# HELLO, WORLD!

Go method and argument names are converted to kebab-case on the CLI (LoudHelloloud-hello, name--name).

The constructor

If you define a New function in the same package, it becomes the module's constructor. Its arguments become arguments of the main object, and its return value is the initialized main object. Use it for module-wide configuration and shared state.

A common pattern is to accept a *dagger.Workspace so the module can read the project it runs against (see Workspace inputs). Dagger auto-populates it from the current workspace, and content is pulled lazily, so you store the project directory once and reuse it:

main.go
package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct {
Source *dagger.Directory
}

func New(
// The current workspace, auto-populated by Dagger.
ws *dagger.Workspace,
) *MyModule {
return &MyModule{
// Read the workspace root; nothing is uploaded until a function uses it.
Source: ws.Directory("/"),
}
}

func (m *MyModule) Foo(ctx context.Context) ([]string, error) {
return dag.Container().
From("alpine:latest").
WithMountedDirectory("/app", m.Source).
Directory("/app").
Entries(ctx)
}

Exported struct fields (like Source above) are part of the object's state and are serialized between functions in a chain. Mark a field private to Dagger — present in Go but hidden from the API — with a +private comment:

type LintRun struct {
// +private
Source *dagger.Directory
}

Arguments and return values

Dagger derives a function's argument and return types from the Go signature. The mapping is:

Go typeDagger type
stringString
intInt
float64Float
boolBoolean
[]T[T] (list)
*dagger.DirectoryDirectory
*dagger.FileFile
*dagger.ContainerContainer
*dagger.SecretSecret
*dagger.ServiceService
a custom struct Tobject T
a custom string type with constsenum

Documentation

Doc comments become API documentation, surfaced in dagger api functions and dagger api call --help. A comment directly above a method documents the function; a comment directly above a parameter documents that argument; a comment above the package main declaration documents the whole module.

// Return a greeting.
func (m *MyModule) Hello(
// Who to greet
name string,
) string {
return fmt.Sprintf("Hello, %s!", name)
}

Optional and default arguments

Dagger arguments are required by default. Make one optional, or give it a default, with a magic comment on the parameter:

optional
func (m *MyModule) Hello(
ctx context.Context,
// +optional
name string,
) (string, error) {
if name != "" {
return fmt.Sprintf("Hello, %s", name), nil
}
return "Hello, world", nil
}
default value
func (m *MyModule) Hello(
ctx context.Context,
// +default="world"
name string,
) (string, error) {
return fmt.Sprintf("Hello, %s", name), nil
}
  • +optional makes the argument optional. For scalar types it defaults to the Go zero value; for pointer types (*dagger.Directory, etc.) it defaults to nil, so you can detect "not passed."
  • +default="..." makes the argument optional and supplies a default value when the caller omits it.

Nullability

Use a pointer to a core type (e.g. *dagger.Secret) when an argument or return value may be absent. A nil pointer corresponds to a null/omitted value. Non-pointer scalars (string, int, bool) are always present.

Enums

Model a closed set of string values as a named string type with a block of typed constants. Dagger turns it into an enum and validates inputs:

main.go
package main

import "context"

type MyModule struct{}

// Vulnerability severity levels
type Severity string

const (
// Undetermined risk; analyze further.
Unknown Severity = "UNKNOWN"

// Minimal risk; routine fix.
Low Severity = "LOW"

// Moderate risk; timely fix.
Medium Severity = "MEDIUM"

// Serious risk; quick fix needed.
High Severity = "HIGH"

// Severe risk; immediate action.
Critical Severity = "CRITICAL"
)

func (m *MyModule) Scan(ctx context.Context, ref string, severity Severity) (string, error) {
return dag.Container().
From("aquasec/trivy:0.50.4").
WithExec([]string{
"trivy", "image",
"--severity=" + string(severity),
ref,
}).Stdout(ctx)
}

Passing a value outside the enum produces a clear error listing the allowed choices:

dagger api call scan --ref=alpine:latest --severity=FOO
# Error: value should be one of UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL

Custom object types

Return a struct to expose a custom object. Exported fields become readable values, and exported methods on the type become chainable functions. Dagger prefixes custom type names with the module name in the API schema (e.g. MyModuleOrganization) to avoid collisions:

main.go
package main

import "dagger/my-module/internal/dagger"

type MyModule struct{}

func (module *MyModule) DaggerOrganization() *Organization {
url := "https://github.com/dagger"
return &Organization{
URL: url,
Repositories: []*dagger.GitRepository{dag.Git(url + "/dagger")},
Members: []*Account{
{"jane", "jane@example.com"},
{"john", "john@example.com"},
},
}
}

type Organization struct {
URL string
Repositories []*dagger.GitRepository
Members []*Account
}

type Account struct {
Username string
Email string
}

func (account *Account) URL() string {
return "https://github.com/" + account.Username
}

This enables chaining on the CLI and API:

dagger api call dagger-organization members url

Interfaces

Interfaces let your module accept arbitrary objects from other modules without depending on their concrete types. Declare a Go interface that embeds DaggerObject and lists the functions you need; any object that provides matching functions can be passed in:

main.go
package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

type Fooer interface {
DaggerObject
Foo(ctx context.Context, bar int) (string, error)
}

func (m *MyModule) Foo(ctx context.Context, fooer Fooer) (string, error) {
return fooer.Foo(ctx, 42)
}

The DaggerObject marker (provided by the generated client) is required so Dagger knows this interface describes a Dagger object rather than a plain Go interface.

Working with core Dagger types

The generated client exposes the entire Dagger API through a package-level variable named dag. You use it to build containers, mount directories and files, handle secrets, and run services. Core types live in the internal/dagger package, imported as dagger.

Containers

Each builder method returns a new, immutable *dagger.Container. Nothing mutates in place, and every step is content-addressed and cached automatically.

package main

import (
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and return a container
func (m *MyModule) Build(source *dagger.Directory) *dagger.Container {
return dag.Container().
From("node:20").
WithDirectory("/app", source).
WithWorkdir("/app").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"})
}

Directories and files

*dagger.Directory and *dagger.File are first-class, "just-in-time" artifacts you can accept as arguments, produce as return values, mount into containers, and export to the host. Some builder methods take an options struct for less-common parameters — for example WithDirectory accepts dagger.ContainerWithDirectoryOpts:

func (m *MyModule) CopyDirectoryWithExclusions(
ctx context.Context,
// Source directory
source *dagger.Directory,
// Exclusion pattern
// +optional
exclude []string,
) *dagger.Container {
return dag.Container().
From("alpine:latest").
WithDirectory("/src", source, dagger.ContainerWithDirectoryOpts{Exclude: exclude})
}

The pattern is consistent: required parameters are positional Go arguments, optional ones live in a generated XxxOpts struct.

Workspace inputs

When a module needs to read the user's project — its source tree, config files, lockfiles — it takes a *dagger.Workspace argument, almost always on the constructor. You don't pass it: Dagger auto-populates it from the current workspace, and nothing is uploaded up front. Project content is pulled lazily, on demand when a function actually reads a path, so a module can declare access to the whole workspace cheaply and only pay for what it touches.

main.go
package main

import (
"dagger/my-module/internal/dagger"
)

type MyModule struct {
Source *dagger.Directory
}

func New(
// The current workspace, auto-populated by Dagger.
ws *dagger.Workspace,
) *MyModule {
return &MyModule{
// Pull the workspace root as a Directory (lazy — no upload yet).
Source: ws.Directory("/"),
}
}

// Functions reuse the pulled Directory like any other.
func (m *MyModule) Build() *dagger.Container {
return dag.Container().
From("node:20").
WithDirectory("/app", m.Source).
WithWorkdir("/app").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"})
}

The Workspace client type exposes accessors for reading project content:

AccessorSignatureReturns
Directoryws.Directory(path string, opts ...dagger.WorkspaceDirectoryOpts) *dagger.Directorya Directory at path
Filews.File(path string) *dagger.Filea File at path
FindUpws.FindUp(ctx, name string, opts ...dagger.WorkspaceFindUpOpts) (string, error)the workspace path of name, searching upward

Path resolution. A relative path resolves from the workspace's current working directory; an absolute path (starting with /) resolves from the workspace root (boundary). So ws.Directory("/") is the whole project root, while ws.Directory(".") is wherever the user invoked Dagger from.

Excluding files. Directory takes a dagger.WorkspaceDirectoryOpts options struct to filter what gets pulled. Tight filters matter for cache efficiency — loading less means fewer cache invalidations:

func New(ws *dagger.Workspace) *MyModule {
return &MyModule{
Source: ws.Directory("/", dagger.WorkspaceDirectoryOpts{
Exclude: []string{"node_modules", ".git", "dist"},
// Include: []string{"app/", "package.*"}, // allowlist instead
// Gitignore: true, // apply .gitignore rules
}),
}
}

FindUp walks up from a start path (relative paths resolve from the workspace cwd; pass dagger.WorkspaceFindUpOpts{From: "..."} to change it) and returns the absolute workspace path of the first match, stopping at the workspace boundary. Use it to locate a project root marker such as package.json or go.mod.

tip

To use the current workspace, declare a *dagger.Workspace argument on the module constructor or function. Dagger injects it automatically and omits it from the CLI arguments.

Secrets

Accept sensitive values as *dagger.Secret, never as plain strings. Dagger scrubs secret plaintext from logs, caches, and crash reports:

package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Query the GitHub API
func (m *MyModule) GithubApi(
ctx context.Context,
// GitHub API token
token *dagger.Secret,
) (string, error) {
return dag.Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", token).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
}

Callers supply secrets through providers on the CLI:

dagger api call github-api --token=env:GITHUB_TOKEN     # environment variable
dagger api call github-api --token=file:./token.txt # file
dagger api call github-api --token=cmd:"gh auth token" # command output
dagger api call github-api --token=op://vault/item/field # 1Password

Services

Return *dagger.Service to expose a long-running service, and bind it into other containers with WithServiceBinding. Services are content-addressed, so a given definition always gets the same hostname — no port conflicts:

package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Start and return an HTTP service
func (m *MyModule) HttpService() *dagger.Service {
return dag.Container().
From("python").
WithWorkdir("/srv").
WithNewFile("index.html", "Hello, world!").
WithExposedPort(8080).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"python", "-m", "http.server", "8080"}})
}

// Send a request to an HTTP service and return the response
func (m *MyModule) Get(ctx context.Context) (string, error) {
return dag.Container().
From("alpine").
WithServiceBinding("www", m.HttpService()).
WithExec([]string{"wget", "-O-", "http://www:8080"}).
Stdout(ctx)
}

A larger example

Real modules combine these pieces. This is adapted from Dagger's own ruff module (a Go SDK module in the Dagger repo) — note custom types, chaining, error returns, and the use of dag.CurrentModule() to reach the module's own source:

package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/dagger/dagger/modules/ruff/internal/dagger"
)

// Ruff is a fast Python linter implemented in Rust
type Ruff struct{}

// Lint a Python codebase
func (ruff Ruff) Lint(
// The Python source directory to lint
source *dagger.Directory,
) *LintRun {
return &LintRun{Source: source}
}

// The result of running the Ruff lint tool
type LintRun struct {
// +private
Source *dagger.Directory
}

// Return a JSON report file for this run
func (run LintRun) Report() *dagger.File {
cmd := []string{"/ruff", "check", "--exit-zero", "--output-format", "json", "."}
return dag.
CurrentModule().
Source().
Directory("build").
DockerBuild().
WithMountedDirectory("", run.Source).
WithExec(cmd, dagger.ContainerWithExecOpts{RedirectStdout: "ruff-report.json"}).
File("ruff-report.json")
}

// Return an error if the run reported any issues
func (run LintRun) Assert(ctx context.Context) error {
contents, err := run.Report().Contents(ctx)
if err != nil {
return err
}
var issues []struct {
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(contents), &issues); err != nil {
return err
}
if len(issues) > 0 {
var lines []string
for _, i := range issues {
lines = append(lines, " - "+i.Message)
}
return errors.New(fmt.Sprintf("%d issues\n%s", len(issues), strings.Join(lines, "\n")))
}
return nil
}

Module dependencies

A module can depend on other Dagger modules and call them through dag — for example dag.Ruff() once ruff is a dependency. Dependencies are recorded in dagger-module.toml under dependencies:

dagger-module.toml
name = "dev"
engineVersion = "v1.0.0-beta.6"

[runtime]
source = "go"

[[dependencies]]
name = "go"
source = "../../modules/go"

[[dependencies]]
name = "wolfi"
source = "../wolfi"

A source may be a local path (../wolfi) or a remote reference of the form [proto://]host/repo[/subpath][@version], e.g. github.com/shykes/daggerverse/hello@v0.3.0.

Manage dependencies with the CLI's dagger module deps commands from the module directory rather than hand-editing dagger-module.toml.

Add a dependency by source:

dagger module deps add github.com/shykes/daggerverse/hello@v0.3.0

List the current dependencies:

dagger module deps list

Remove a dependency by name:

dagger module deps rm hello

After changing dependencies, regenerate bindings so the new module's functions appear on dag.

Regenerate bindings and generated files

A Go module uses generated code alongside your handwritten main.go:

  • dagger.gen.go — top-level helpers
  • internal/dagger/ — the typed Dagger client, including dag, all core types (Container, Directory, …), every dependency's functions, and the XxxOpts option structs
  • internal/telemetry/ — supporting generated code when required

You do not edit these files by hand, but you do commit them. Dagger does not regenerate them when it loads a module configured with dagger-module.toml, including a module installed from Git. If required generated files are missing, the module cannot load. The generated .gitattributes marks them as linguist-generated:

.gitattributes
/dagger.gen.go linguist-generated
/internal/dagger/** linguist-generated
/internal/telemetry/** linguist-generated

Regenerate them whenever you change your module's functions, bump the engine version, or add or remove a dependency. Use dagger generate, which returns a changeset:

# Review the regenerated files, then apply
dagger generate

dagger generate discovers and runs every generator in the workspace; for modules registered under the Go SDK, that includes binding regeneration.

Apply and commit the resulting changeset. This keeps the generated client in sync with the functions and dependencies available to your module.

note

A .dagger-go-sdk-skip-generate marker in a module or one of its ancestors skips Go binding regeneration for that module.

tip

Generated SDK bindings are not added to .gitignore; commit them with your module.

By default, Dagger still adds local-only files such as .env to .gitignore. Set codegen.automaticGitignore = false only if you want to manage all .gitignore entries yourself.

Engine version

Each module declares the Dagger engine version it requires in dagger-module.toml (engineVersion). Manage it with the CLI's dagger module engine commands from the module directory.

Read the currently required version:

dagger module engine required

Pin a specific version, the current engine, or the latest stable release:

# A specific version
dagger module engine require v1.0.0-beta.6

# Whatever engine you're running now
dagger module engine require-current

# Latest stable release
dagger module engine require-latest

Bumping the engine version usually means the generated bindings should change too, so follow with dagger generate.

Checks, generators, services, directives, and ignore patterns

A useful, reusable module provides at least one of the three first-class function types — a check, a generator, or a service — so that the platform verbs (dagger check, dagger generate, dagger up) have something to run. These work the same in Go as in any SDK; see the Module Developer Guide for the full treatment. In Go, you mark each one with a doc-comment pragma on the function:

PragmaReturn typeRun byPurpose
// +checkerror (or *dagger.Container)dagger checkvalidate the project (test/lint/scan)
// +generate*dagger.Changesetdagger generateproduce a diff to apply to the workspace
// +up*dagger.Servicedagger upstart a long-running service

The Go-specific surface is:

Magic comment directives

Go modules use // +directive comments to add Dagger metadata that Go's type system can't express:

DirectivePlacementMeaning
// +optionalabove an argumentargument is optional
// +default="x"above an argumentoptional with a default value
// +privateabove a struct fieldkeep the field out of the API
// +checkabove a methodmark the function as a check
// +generateabove a methodmark the function as a generator
// +upabove a methodmark the function as a service

Ignore patterns

A module reads the user's project through a *dagger.Workspace argument (see Workspace inputs), not a path-defaulted *dagger.Directory. To filter what gets pulled, use the exclude option when reading a workspace directory. Tight filters are essential for cache efficiency (load less → fewer cache invalidations):

func New(ws *dagger.Workspace) *MyModule {
return &MyModule{
Source: ws.Directory("/", dagger.WorkspaceDirectoryOpts{
Exclude: []string{"node_modules", ".git", "dist"},
}),
}
}

Checks

Mark a function with the // +check pragma to make it a check — a validation function (test, lint, scan) that takes no caller arguments. dagger check discovers and runs every check a module exposes. A check fails when it returns a non-nil error, or when it returns a *dagger.Container whose execution exits non-zero. The platform model is described in Checks.

// Lint the project.
//
// +check
func (m *MyModule) Lint(ctx context.Context) error {
_, err := dag.Container().
From("golangci/golangci-lint:latest").
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"golangci-lint", "run"}).
Sync(ctx)
return err
}

// A check can also return a container; a non-zero exit fails the check.
//
// +check
func (m *MyModule) Build() *dagger.Container {
return dag.Container().From("alpine:3").WithExec([]string{"true"})
}

The pragma may be placed on its own line within the doc comment (a blank // line separates the human-readable description from the pragma, as shown above). Checks can also be declared on custom object types, so you can group them (for example a Test object with Lint and Unit checks).

Generators

Mark a function with the // +generate pragma to make it a generator. It returns a *dagger.Changeset: it runs a tool, captures the resulting directory, and diffs it against the source. dagger generate discovers and runs every generator and presents the combined changeset for you to review and apply.

// Format the source.
//
// +generate
func (m *MyModule) Format() *dagger.Changeset {
formatted := dag.Container().
From("golang:latest").
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"gofmt", "-w", "."}).
Directory("/src")
return formatted.Changes(m.Source)
}
note

This author-written // +generate function is distinct from the SDK's client binding generator. The user-facing dagger generate command runs both kinds of generator.

See Generating Code and Changesets.

Services

Mark a function with the // +up pragma to make it a service. It returns a *dagger.Service, and dagger up discovers and starts every service the module exposes, exposing their ports on the host.

// Run the web server.
//
// +up
func (m *MyModule) Web() *dagger.Service {
return dag.Container().
From("nginx:alpine").
WithExposedPort(80).
AsService()
}

Like checks, // +up services can also be declared on custom object types so you can group related services (for example an Infra object exposing a Database service). This is the same *dagger.Service type described under Services above; the // +up pragma is what makes a service function runnable directly via dagger up.

Testing Go modules

Because a Go module is ordinary Go, you have two complementary testing strategies.

Idiomatic Go tests

Functions that contain pure Go logic — parsing reports, formatting summaries, computing counts — can be unit-tested with the standard testing package, with no engine involved:

main_test.go
package main

import "testing"

func TestIssueSummary(t *testing.T) {
issue := Issue{
AbsFilename: "/src/app/main.py",
Message: "undefined name",
Location: Location{Row: 12},
}
got := issue.Summary()
want := "app/main.py:12 error: undefined name"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}

Run them like any Go test (these don't require the engine):

go test ./...

Functional tests via checks

For behavior that exercises containers and the Dagger API, write functions in your module and invoke them, or model them as checks so they run under dagger check. A check that builds, lints, or tests your project doubles as both a CI gate and a smoke test:

# Smoke test: does it build?
dagger api call build

# Run all checks
dagger check

# Run generators and confirm there's no drift
dagger check --generate

In CI

Run dagger check in CI to execute every check the module exposes. Because the heavy lifting happens in content-addressed containers, the same command runs identically on a laptop and on a CI runner, with full caching:

.github/workflows/ci.yml
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: check

IDE setup, go.mod and go.work

The first dagger generate creates a go.mod and go.sum inside your Dagger module. These files tell Go which dependencies the generated client needs. If the Dagger module lives inside a larger Go project, the additional go.mod can make an editor choose the wrong Go module for a file.

Two options:

  1. Reuse a parent go.mod. If a parent directory already has a go.mod you want to use, delete the module's generated go.mod/go.sum; Dagger will fall back to the parent. Avoid this if your module pulls in dependencies irrelevant to the rest of the project, since most consumers prefer a narrower dependency set.

  2. Use a Go workspace. Keep the generated go.mod and stitch everything together with go.work:

    # in the root of your repository
    go work init
    go work use ./
    go work use ./path/to/module

    Restart your IDE and features like go-to-definition will work across both modules.

go.work should usually stay out of version control:

echo go.work >> .gitignore
echo go.work.sum >> .gitignore

Source maps

The Go SDK attaches source maps to type and function definitions — ./path/to/file:line annotations recorded during generation. Most IDEs can follow these (natively or via a plugin such as the Open file plugin for VS Code), so you can click straight from a Dagger Function in one module to its declaration in another.

Packaging and release

A Go SDK module is distributed as a Git repository. There is no build artifact to publish — consumers fetch the source by reference.

Recommended release checklist:

  1. Pin the engine version. Use dagger module engine require <version> to set the oldest engine version your module supports.
  2. Commit the generated bindings. Run dagger generate, review the changes, and commit the generated files with your module.
  3. Version with Git tags. Tag a release (for example, v1.2.0) and push it. Consumers can pin that version with @v1.2.0.

Before publishing, run dagger check --generate to confirm that the committed bindings are up to date.

Consumers can install your module into a workspace with:

dagger install github.com/you/your-module@v1.2.0

To add it as a dependency of another module, run these commands from that module's directory:

dagger module deps add github.com/you/your-module@v1.2.0
dagger generate

A module reference follows [proto://]host/repo[/subpath][@version]; the version may be a tag, branch, or commit, and is resolved over HTTPS or SSH depending on available authentication.

Troubleshooting

dagger init / dagger develop not found. Install the Go SDK with dagger sdk install go, scaffold with dagger module init go <name>, and regenerate with dagger generate.

dagger sdk install go reports "no current workspace". Add --here to create dagger.toml in the current directory: dagger sdk install --here go.

Nothing was written after dagger module init. The command returns a changeset. Review and accept it, or rerun with -y to apply without prompting.

A new function or dependency doesn't show up. Run dagger generate. The API surface comes from the generated internal/dagger package, which must be in sync with your code and dagger-module.toml.

dagger generate does not regenerate bindings. A .dagger-go-sdk-skip-generate marker is likely present in the module or one of its ancestors.

Build/import errors referencing internal/dagger or dag. The generated client is stale or missing. Regenerate as above; if go.mod/go.sum drifted, ensure they match the regenerated code (a workspace go.work can help your IDE here).

Engine version mismatch. Align the module with dagger module engine require <version> (or require-current / require-latest), then regenerate.

Next steps