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 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.

Lifecycle of an Action

A composite action's lifecycle has 4 stages:

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


A new action is defined in a declarative template called a CUE definition. This definition describes the action's inputs, outputs, sub-actions, and the wiring between them.

Here is an example of a simple action definition:

package main

import (

// 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 that this action includes one sub-action: core.#WriteFile. An action can incorporate any number of sub-actions.

Also 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.
  • "sub-actions" are simply fields which contain another action definition. For example, write is a sub-action.

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


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.#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.


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

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



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

dagger do hello