Skip to main content

Return Values

In addition to returning basic types (string, boolean, ...), Dagger Functions can also return any of Dagger's core types, such as Directory, Container, Service, Secret, and many more.

This opens powerful applications to Dagger Functions. For example, a Dagger Function that builds binaries could take a directory with the source code as argument and return another directory (a "just-in-time" directory) containing just binaries or a container image (a "just-in-time" container) with the binaries included.

note

If a function doesn't have a return type annotation, it'll be translated to the dagger.Void type in the API.

String return values

Here is an example of a Dagger Function that returns operating system information for the container as a string:

package main

import (
"context"

"main/internal/dagger"
)

type MyModule struct{}

func (m *MyModule) OsInfo(ctx context.Context, ctr *dagger.Container) (string, error) {
return ctr.
WithExec([]string{"uname", "-a"}).
Stdout(ctx)
}

Here is an example call for this Dagger Function:

dagger call os-info --ctr=ubuntu:latest

The result will look like this:

Linux buildkitsandbox 6.1.0-22-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.94-1 (2024-06-21) x86_64 x86_64 x86_64 GNU/Linux

Directory return values

Directory return values might be produced by a Dagger Function that:

  • Builds language-specific binaries
  • Downloads source code from git or another remote source
  • Processes source code (for example, generating documentation or linting code)
  • Downloads or processes datasets
  • Downloads machine learning models

Here is an example of a Go builder Dagger Function that accepts a remote Git address as a Directory argument, builds a Go binary from the source code in that repository, and returns the build directory containing the compiled binary:

package main

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

type MyModule struct{}

func (m *MyModule) GoBuilder(ctx context.Context, src *dagger.Directory, arch string, os string) *dagger.Directory {
return dag.Container().
From("golang:1.21").
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithEnvVariable("GOARCH", arch).
WithEnvVariable("GOOS", os).
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "build/"}).
Directory("/src/build")
}

Here is an example call for this Dagger Function:

dagger call go-builder --src=https://github.com/golang/example#master:/hello --arch=amd64 --os=linux

Once the command completes, you should see this output:

_type: Directory
entries:
- hello

This means that the build succeeded, and a Directory type representing the build directory was returned. This Directory is called a "just-in-time" directory: a dynamically-produced artifact of a Dagger pipeline.

File return values

Similar to just-in-time directories, Dagger Functions can produce just-in-time files by returning the File type.

Just-in-time files might be produced by a Dagger Function that:

  • Builds language-specific binaries
  • Combines multiple input files into a single output file, such as a composite video or a compressed archive

Here is an example of a Dagger Function that accepts a filesystem path or remote Git address as a Directory argument and returns a ZIP archive of that directory:

package main

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

type MyModule struct{}

func (m *MyModule) Archiver(ctx context.Context, src *dagger.Directory) *dagger.File {
return dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "zip"}).
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"sh", "-c", "zip -p -r out.zip *.*"}).
File("/src/out.zip")
}

Here is an example call for this Dagger Function:

dagger call archiver --src=https://github.com/dagger/dagger#main:./docs/current_docs/quickstart

Once the command completes, you should see this output:

_type: File
name: out.zip
size: 13744

This means that the build succeeded, and a File type representing the ZIP archive was returned.

Container return values

Similar to directories and files, just-in-time containers are produced by calling a Dagger Function that returns the Container type. This type provides a complete API for building, running and distributing containers.

Just-in-time containers might be produced by a Dagger Function that:

  • Builds a container
  • Minifies a container
  • Downloads a container image from a running registry
  • Exports a container from Docker or other container runtimes
  • Snapshots the state of a running container

You can think of a just-in-time container, and the Container type that represents it, as a build stage in Dockerfile. Each operation produces a new immutable state, which can be further processed, or exported as an OCI image. Dagger Functions can accept, return and pass containers between themselves, just like regular variables.

Here's an example of a Dagger Function that returns a base alpine container image with a list of additional specified packages:

package main

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

type MyModule struct{}

func (m *MyModule) AlpineBuilder(ctx context.Context, packages []string) *dagger.Container {
ctr := dag.Container().
From("alpine:latest")
for _, pkg := range packages {
ctr = ctr.WithExec([]string{"apk", "add", pkg})
}
return ctr
}

Here is an example call for this Dagger Function:

dagger call alpine-builder --packages=curl,openssh

Once the command completes, you should see this output:

_type: Container
defaultArgs:
- /bin/sh
entrypoint: []
mounts: []
platform: linux/amd64
user: ""
workdir: ""

This means that the build succeeded, and a Container type representing the built container image was returned.

note

When calling Dagger Functions that produce a just-in-time artifact, you can use the Dagger CLI to add more functions to the pipeline for further processing - for example, inspecting the contents of a directory artifact, exporting a file artifact to the local filesystem, publishing a container artifact to a registry, and so on. This is called "function chaining", and it is one of Dagger's most powerful features.

Chaining

So long as a Dagger Function returns an object that can be JSON-serialized, its state will be preserved and passed to the next function in the chain. This makes it possible to write custom Dagger Functions that support function chaining in the same style as the Dagger API.

Here is an example module with support for function chaining:

// A Dagger module for saying hello world!

package main

import (
"context"
"fmt"
)

type MyModule struct {
Greeting string
Name string
}

func (hello *MyModule) WithGreeting(ctx context.Context, greeting string) (*MyModule, error) {
hello.Greeting = greeting
return hello, nil
}

func (hello *MyModule) WithName(ctx context.Context, name string) (*MyModule, error) {
hello.Name = name
return hello, nil
}

func (hello *MyModule) Message(ctx context.Context) (string, error) {
var (
greeting = hello.Greeting
name = hello.Name
)
if greeting == "" {
greeting = "Hello"
}
if name == "" {
name = "World"
}
return fmt.Sprintf("%s, %s!", greeting, name), nil
}

And here is an example call for this module:

dagger call with-name --name=Monde with-greeting --greeting=Bonjour message

The result will be:

Bonjour, Monde!