Functions
Dagger Functions are regular code, written in a supported programming language, and running in containers. Dagger Functions let you encapsulate common operations or workflows into discrete units with clear inputs and outputs.
Here's an example of a simple Dagger Function:
- Go
- Python
- TypeScript
package main
type MyModule struct{}
func (m *MyModule) Hello() string {
return "Hello, world"
}
from dagger import function, object_type
@object_type
class MyModule:
@function
def hello(self) -> str:
return "Hello, world"
Type annotations
Even though the Python runtime doesn’t enforce type annotations at runtime, it’s important to define them with Dagger Functions. The Python SDK needs the typing information at runtime to correctly report to the API. It can’t rely on type inference, which is only possible for external static type checkers.
If a function doesn’t have a return type annotation, it’ll be declared as None
,
which translates to the dagger.Void type in the API:
@function
def hello(self):
return "Hello world!"
# Error: cannot convert string to Void
It’s fine however, when no value actually needs to be returned:
@function
def hello(self):
...
# no return
import { object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
hello(): string {
return "Hello, world"
}
}
Here is an example call for this Dagger Function:
dagger call hello
The result will be:
Hello, world
Here's an example of a more complex Dagger Function, which calls a remote API method:
- Go
- Python
- TypeScript
package main
import (
"context"
)
type MyModule struct{}
func (m *MyModule) GetUser(ctx context.Context) (string, error) {
return dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"apk", "add", "jq"}).
WithExec([]string{"sh", "-c", "curl https://randomuser.me/api/ | jq .results[0].name"}).
Stdout(ctx)
}
Notice that this function now includes context as input and error as return in the module's function signature.
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def get_user(self) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["apk", "add", "curl"])
.with_exec(["apk", "add", "jq"])
.with_exec(
["sh", "-c", "curl https://randomuser.me/api/ | jq .results[0].name"]
)
.stdout()
)
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async getUser(): Promise<string> {
return await dag
.container()
.from("alpine:latest")
.withExec(["apk", "add", "curl"])
.withExec(["apk", "add", "jq"])
.withExec([
"sh",
"-c",
"curl https://randomuser.me/api/ | jq .results[0].name",
])
.stdout()
}
}
When using dagger call, all names (functions, arguments, struct fields, etc) are converted into a shell-friendly "kebab-case" style.
Here is an example call for this Dagger Function:
dagger call get-user
The result will look something like this:
{
"title": "Mrs",
"first": "Beatrice",
"last": "Lavigne"
}
Class methods
Dagger Functions are implemented as @dagger.function decorated methods, of a @dagger.object_type decorated class.
It's possible for a module to implement multiple classes (object types), but the first one needs to have a name that matches the module's name, in PascalCase. This object is sometimes referred to as the main object.
For example, for a module initialized with dagger init --name=my-module
,
the main object needs to be named MyModule
.
Arguments
Dagger Functions, just like regular functions, can accept arguments. In addition to basic types (string, boolean, integer, arrays...), Dagger also defines powerful core types which functions can use for their arguments, such as Directory
, Container
, Service
, Secret
, and many more.
Here's an example of modifying the previous Dagger Function to accept a string argument:
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
)
type MyModule struct{}
func (m *MyModule) GetUser(ctx context.Context, gender string) (string, error) {
return dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"apk", "add", "jq"}).
WithExec([]string{"sh", "-c", fmt.Sprintf("curl https://randomuser.me/api/?gender=%s | jq .results[0].name", gender)}).
Stdout(ctx)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def get_user(self, gender: str) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["apk", "add", "curl"])
.with_exec(["apk", "add", "jq"])
.with_exec(
[
"sh",
"-c",
(
f"curl https://randomuser.me/api/?gender={gender}"
" | jq .results[0].name"
),
]
)
.stdout()
)
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async getUser(gender: string): Promise<string> {
return await dag
.container()
.from("alpine:latest")
.withExec(["apk", "add", "curl"])
.withExec(["apk", "add", "jq"])
.withExec([
"sh",
"-c",
`curl https://randomuser.me/api/?gender=${gender} | jq .results[0].name`,
])
.stdout()
}
}
Here is an example call for this Dagger Function:
dagger call get-user --gender=male
The result will look something like this:
{
"title": "Mr",
"first": "Hans-Werner",
"last": "Thielen"
}
Here's an example of a Dagger Function that accepts a Directory
as argument:
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) Tree(ctx context.Context, src *dagger.Directory, depth string) (string, error) {
return dag.Container().
From("alpine:latest").
WithMountedDirectory("/mnt", src).
WithWorkdir("/mnt").
WithExec([]string{"apk", "add", "tree"}).
WithExec([]string{"tree", "-L", depth}).
Stdout(ctx)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def tree(self, src: dagger.Directory, depth: str) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_mounted_directory("/mnt", src)
.with_workdir("/mnt")
.with_exec(["apk", "add", "tree"])
.with_exec(["tree", "-L", depth])
.stdout()
)
import { dag, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async tree(src: Directory, depth: string): Promise<string> {
return await dag
.container()
.from("alpine:latest")
.withMountedDirectory("/mnt", src)
.withWorkdir("/mnt")
.withExec(["apk", "add", "tree"])
.withExec(["tree", "-L", depth])
.stdout()
}
}
Here is an example call for this Dagger Function:
dagger call tree --src=. --depth=1
The result will look like this:
.
├── LICENSE
├── dagger
└── dagger.json
2 directories, 2 files
You can also pass a remote Git reference, and the Dagger CLI will convert it to a Directory
referencing the contents of that repository. Here is an example call that lists the source code for the Dagger CLI, from the main
branch of the Dagger GitHub repository:
dagger call tree --src=https://github.com/dagger/dagger#main:sdk --depth=1
The result will be the same file listing as this GitHub page:
.
├── CONTRIBUTING.md
├── cue
├── dotnet
├── elixir
├── go
├── java
├── php
├── python
├── rust
└── typescript
10 directories, 1 file
When calling a Dagger Function from the CLI, its arguments are exposed as command-line flags. How the flag is interpreted depends on the argument type.
Optional arguments
Function arguments can be marked as optional. In this case, the Dagger CLI will not display an error if the argument is omitted in the function call.
Here's an example of a Dagger Function with an optional argument:
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
)
type MyModule struct{}
func (m *MyModule) Hello(
ctx context.Context,
// +optional
name string,
) (string, error) {
if name != "" {
return fmt.Sprintf("Hello, %s", name), nil
} else {
return "Hello, world", nil
}
}
from dagger import function, object_type
@object_type
class MyModule:
@function
def hello(self, name: str | None) -> str:
if name is None:
name = "world"
return f"Hello, {name}"
import { object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
hello(name?: string): string {
if (name) {
return `Hello, ${name}`
}
return "Hello, world"
}
}
Here is an example call for this Dagger Function, with the optional argument:
dagger call hello --name=John
The result will look like this:
Hello, John
Here is an example call for this Dagger Function, without the optional argument:
dagger call hello
The result will look like this:
Hello, world
Default values
Function arguments can define a default value if no value is supplied for them.
Here's an example of a Dagger Function with a default value for an argument:
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
)
type MyModule struct{}
func (m *MyModule) Hello(
ctx context.Context,
// +optional
// +default="world"
name string,
) (string, error) {
return fmt.Sprintf("Hello, %s", name), nil
}
from dagger import function, object_type
@object_type
class MyModule:
@function
def hello(self, name: str = "world") -> str:
return f"Hello, {name}"
import { object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
hello(name = "world"): string {
return `Hello, ${name}`
}
}
Here is an example call for this Dagger Function, without the required argument:
dagger call hello
The result will look like this:
Hello, world
Return values
Not only can Dagger Functions use Dagger's core types in their arguments; they can use them in their return value as well.
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 containing just binaries or a container image with the binaries included.
If a function doesn’t have a return type annotation, it’ll be translated to the dagger.Void type in the API.
https://docs.dagger.io/api/reference/#definition-Void
Here's an example of a Dagger Function that accepts a Directory
containing a Go application's source code as input, compiles it into a binary, and returns a Container
with the binary:
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) Build(ctx context.Context, src *dagger.Directory, arch string, os string) *dagger.Container {
dir := 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")
return dag.Container().
From("alpine:latest").
WithDirectory("/usr/local/bin", dir)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def build(self, src: dagger.Directory, arch: str, os: str) -> dagger.Container:
dir_ = (
dag.container()
.from_("golang:1.21")
.with_mounted_directory("/src", src)
.with_workdir("/src")
.with_env_variable("GOARCH", arch)
.with_env_variable("GOOS", os)
.with_env_variable("CGO_ENABLED", "0")
.with_exec(["go", "build", "-o", "build/"])
.directory("/src/build")
)
return (
dag.container()
.from_("alpine:latest")
.with_directory("/usr/local/bin", dir_)
)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
build(src: Directory, arch: string, os: string): Container {
const dir = dag
.container()
.from("golang:1.21")
.withMountedDirectory("/src", src)
.withWorkdir("/src")
.withEnvVariable("GOARCH", arch)
.withEnvVariable("GOOS", os)
.withEnvVariable("CGO_ENABLED", "0")
.withExec(["go", "build", "-o", "build/"])
.directory("/src/build")
return dag
.container()
.from("alpine:latest")
.withDirectory("/usr/local/bin", dir)
}
}
Here is an example call for this Dagger Function:
dagger call build --src=https://github.com/golang/example/#master:hello --os=linux --arch=amd64 terminal
This example chains two functions calls:
- a call to
Build()
, which builds a Go application from a remote GitHub repository and returns aContainer
with the compiled binary; - a call to
Terminal()
, which opens an interactive terminal session with theContainer
returned by the previous function.
The result will be an interactive terminal session with the built container, which you can use to validate the compiled Go binary:
/ # cd /usr/local/bin
/usr/local/bin # ls
hello
/usr/local/bin # ./hello
Hello, world!
/usr/local/bin #