Skip to main content

Dagger Actions

caution

This documentation is for an older version of Dagger, which is no longer actively maintained.

We encourage you to upgrade and refer to the documentation for the most current version.

If you cannot upgrade to the latest version, please contact us in the help forum on Discord. When contacting us, please let us know why you cannot move to the latest version. From there, our team will work with you on your use case.

Actions are the basic building block of the Dagger platform. An action encapsulates an arbitrarily complex automation into a simple software component that can be safely shared, and repeatably executed by any Dagger engine.

Actions can be executed directly with dagger do, or integrated as a component of a more complex action.

There are two types of actions: core actions and composite actions.

Core Actions

Core Actions are primitives implemented by the Dagger Engine itself. They can be combined into higher-level composite actions. Their definitions can be imported in the dagger.io/dagger/core package.

To learn more about core actions, see the core action reference.

Composite Actions

Composite Actions are actions made of other actions. Dagger supports arbitrary nesting of actions, so a composite action can be assembled from any combination of core and composite actions.

One consequence of arbitrary nesting is that Dagger doesn't need to distinguish between "pipelines" and "steps": everything is an action. Some actions are just more complex and powerful than others. This is a defining feature of Dagger.

Universe packages

Universe packages are ready-to-use composite actions with embedded domain logic.

They are abstractions on top of other actions, aiming to:

  1. Promote code reusability
  2. Enforce good practices
  3. Abstract complexity

For example, the netlify package is based on the docker and the bash actions. It is a composite action that we rely on internally whenever we need to deploy code on this platform.

Lifecycle of an Action

A composite action's lifecycle has 4 stages:

  1. Definition
  2. Integration
  3. Discovery
  4. Execution

Definition

A new action is defined in a declarative template called a CUE definition. This definition describes the action's inputs, outputs and its domain logic.

Here is an example of a simple action definition:

package main

import (
"dagger.io/dagger"
"dagger.io/dagger/core"
)

// Write a greeting to a file, and add it to a directory
#AddHello: {
// The input directory
dir: dagger.#FS

// The name of the person to greet
name: string | *"world"

write: core.#WriteFile & {
input: dir
path: "hello-\(name).txt"
contents: "hello, \(name)!"
}

// The directory with greeting message added
result: write.output
}

Note the free-form structure: an action definition is not structured by a rigid schema. It is simply a CUE struct with fields of various types.

  • "inputs" are simply fields which are not complete, and therefore can receive an external value at integration. For example, dir and name are inputs.
  • "outputs" are simply fields which produce a value that can be referenced externally at integration. For example, result is an output.
  • The "domain logic" is implemented via the wiring of any number of sub-actions. Sub-actions are pre-existing core actions, universe packages or any composite actions containing parts of the domain logic.

For example, this composite action includes one sub-action: core.#WriteFile, at the heart of the domain logic of #AddHello, and referenced by the write field.

There are no constraints to an action's field names or types.

Integration

Action definitions cannot be executed directly: they must be integrated into a plan.

A plan is an execution context for actions. It specifies:

  • What actions to present to the end user
  • Dependencies between those tasks, if any
  • Interactions between the tasks and the client system, if any

Actions are integrated into a plan by merging their CUE definition into the plan's CUE definition.

Here is an example of a plan:

package main

import (
"dagger.io/dagger"
)

dagger.#Plan & {
// Say hello by writing to a file
actions: hello: #AddHello & {
dir: client.filesystem.".".read.contents
}
client: filesystem: ".": {
read: contents: dagger.#FS
write: contents: actions.hello.result
}
}

Note that #AddHello was integrated directly into the plan, whereas core.#WriteFile was integrated indirectly, by virtue of being a sub-action of #AddHello.

To learn more about the structure of a plan, see it all begins with a plan.

Discovery

Once integrated into a plan, actions can be discovered by end users, by using the familiar convention of usage messages:

$ dagger do --help
Execute a dagger action.

Available Actions:
hello Say hello by writing to a file

Usage:
dagger do [OPTIONS] ACTION [SUBACTION...] [flags]

Flags:
[...]

Execution

Once the end user has discovered the action that they need, they can execute it with dagger do. For example:

dagger do hello

Example

Given this representative CUE plan:

dagger.#Plan & {
actions: {
build: {...}
deploy: {
local: {...}
cloud: {...}
}
}
}

Running:

  • dagger do build will run the build action
  • dagger do deploy will run both the local and cloud actions
  • dagger do deploy local will run the local sub-action
  • dagger do deploy cloud will run the cloud sub-action

If you specify the key path to an action regrouping several sub-actions, all of the sub-actions will run. When you specify the key path to a single action/sub-action, only one will run.

There is no depth limit to the key path you specify: it can be useful for debugging purposes.