Skip to main content

Dagger Actions

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.