Workshop definition

A project which defines a single workshop can store a definition file named workshop.yaml or .workshop.yaml (the latter is hidden) in the project directory.

Filename convention

When multiple workshops are defined, their definition files must be stored in the .workshop/ subdirectory. The workshop name must also match the file name (without the .yaml extension).

Workshop names start with a lowercase letter and may include only lowercase letters, digits or hyphens.

Structure

The definition in the file is written in YAML and includes a number of mandatory and optional keys:

Key

Value

Description

name (required)

string

Workshop’s name, used to reference the workshop itself.

For workshops defined in the .workshop/ subdirectory, the definition file must have the same name (followed by .yaml).

base (required)

string

Workshop’s base image that provides the underlying OS capabilities.

It can be ubuntu@20.04, ubuntu@22.04, ubuntu@24.04, or ubuntu@26.04.

sdks

array

List of individual SDKs from the SDK Store to include in the workshop.

Each entry points to an existing SDK and specifies its retrieval channel. The SDKs are installed in the order they appear in this list; the exception is the system SDK which is always installed first.

connections

array

List of connections made by the workshop; each links a plug to a slot.

Any entry in connections must include a plug and a slot from the SDKs listed under sdks (the system SDK is always implicitly included). Both must be strings that reference a plug and a slot with the same interface, using the <SDK>:<PLUG> format.

actions

object

List of shell actions to be used with workshop run.

These are copied into the workshop before being executed by bash. The options errexit and pipefail are set by default.

Arguments passed to workshop run are available inside the script through the standard bash positional parameters: "$@", "$1", and so on.

Each SDK is described with the following keys:

Key

Value

Description

name (required)

string

Name of an existing SDK, typically from the SDK Store.

channel

string

SDK version to retrieve during launch and refresh operations.

It uses a snap-like format of <TRACK>/<RISK>/<BRANCH>. The default is latest/stable (with no branch).

Only applies to SDKs from the Store.

plugs

object

Lists plug bindings or additional plug definitions under the SDK.

  • A plug binding must name an existing plug in the SDK and set a single bind attribute that references a different plug of the same interface using the <SDK>:<PLUG> format.

  • A plug definition must specify the interface and the relevant attributes (described below).

slots

object

Defines additional slots under the SDK; each entry must specify the interface and the relevant attributes (described below).

System SDK

The system SDK is built into every workshop to expose resources provided by the host system in a consistent way. It’s not available in the SDK Store, so channel isn’t relevant and can be omitted.

Technically, the system SDK is of system type, whereas all other SDKs are of regular type, but this detail isn’t exposed in the definition files.

Several interfaces expose resources that are host-based and singular by nature; the system SDK has default eponymous slots for these interfaces: system:camera, system:desktop, system:gpu, system:mount, and system:ssh-agent. No other SDKs can declare slots for these interfaces, except for mount. The system:mount slot is still unique because it’s the only one that provides access to the host filesystem, whereas slots under regular SDKs only expose locations in the workshop.

If additional slots for interfaces like tunnel or mount are defined for the system SDK, they won’t be auto-connected at launch or refresh, largely due to security considerations, because the system SDK exposes sensitive host system resources. To the contrary, plugs added under the system SDK can be auto-connected because they expose workshop internals.

Trying out SDKs

The sdkcraft try command makes SDKs available locally without having to publish them in the Store.

Workshops consume these SDKs using names like try-<NAME>; a channel is not required in this case.

In-project SDKs

Projects can define their own SDKs to configure the workshop in a project-specific way. These SDKs are defined by files named like .workshop/<NAME>/sdk.yaml, relative to the project directory. Accordingly, SDK hooks are stored under .workshop/<NAME>/hooks/.

Workshops within the project consume these SDKs using names like project-<NAME>; a channel is not required in this case.

Camera interface

Camera interface plugs must be named camera and can’t belong to the system SDK. They have no attributes.

The only camera interface slot is system:camera.

Desktop interface

Desktop interface plugs must be named desktop and can’t belong to the system SDK. They have no attributes.

The only desktop interface slot is system:desktop.

GPU interface

GPU interface plugs must be named gpu and can’t belong to the system SDK. They have no attributes.

The only GPU interface slot is system:gpu.

Mount interface

Mount interface plugs can’t belong to the system SDK. They are described by the following attributes:

Key

Value

Description

workshop-target (required)

string

A path inside the workshop to be used as the plug’s target directory; /project/ or $SDK-based paths can be used; $SDK expands into the SDK’s installation path in the workshop.

mode

integer

File permissions to use when creating workshop-target and any parent directories. Default is 0o775 for normal users, but changes to 0o755 if uid is zero.

uid

integer

User ID to apply when creating workshop-target and any parent directories. Default is 1000 if workshop-target is in /home/workshop/, /project/, or /run/user/1000/. Otherwise the default is 0.

gid

integer

Group ID to use when creating workshop-target and any parent directories. Matches uid by default.

read-only

Boolean

Whether the target directory should be read-only.

The only mount interface slot in the system SDK is system:mount. It has a single dynamic attribute named host-source, which can be only configured at remount.

Regular SDKs can declare additional mount interface slots. They are described by the following attributes:

Key

Value

Description

workshop-source (required)

string

A path inside the workshop to be used as the slot’s source directory; /project/ or $SDK-based paths can be used; $SDK expands into the SDK’s installation path in the workshop.

SSH interface

SSH interface plugs must be named ssh-agent and can’t belong to the system SDK. They have no attributes.

The only SSH interface slot is system:ssh-agent.

Tunnel interface

Tunnel interface plugs and slots are described by the following attributes:

Key

Value

Description

endpoint

string

A network address or Unix domain socket to be used as one end of the tunnel.

Endpoints are formatted as follows:

Type

Format

Endpoint

<ADDRESS>/<PROTOCOL> for network endpoints. May be shortened to <ADDRESS> or <PROTOCOL>

<PATH> or @<STRING> for Unix domain sockets.

Address

<HOST>:<PORT>. May be shortened to <HOST> or <PORT>.

Protocol

Either tcp or udp. The default is tcp.

Host

An IPv4 or IPv6 address.

If a port is supplied, IPv6 addresses must be enclosed in square brackets.

Supported aliases: localhost, ip6-localhost and ip6-loopback.

The default is localhost.

Port

A TCP or UDP port number (1–65535).

May be omitted, but only on one side of a connection. For such connections, both sides use the same port.

For security reasons, tunnel interface plugs in the system SDK cannot use privileged ports (1–1023).

Path

An absolute path to a Unix domain socket.

$HOME expands into the user’s home directory and $XDG_RUNTIME_DIR expands into the user runtime directory (e.g., /run/user/1000).

For security reasons, tunnel interface plugs in the system SDK cannot listen on sockets outside these two directories.

String

An abstract socket name.

The default endpoint is the default network address (localhost/tcp).

Endpoints which start with [ or @ need to be quoted in YAML:

endpoint: '[::1]:8080/tcp'
endpoint: '@abstract.sock'

JSON Schema

The following JSON Schema formalizes the description above:

Workshop definition schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://canonical.com/workshop.yaml",
  "title": "Workshop",
  "description": "Workshop definition.",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Name of the workshop.",
      "pattern": "^[a-z](?:-?[a-z0-9])*$",
      "maxLength": 40,
      "errorMessage": "A workshop's name must start with a letter and can only include digits, lowercase letters, and hyphens joining them."
    },
    "base": {
      "type": "string",
      "description": "Base system for the workshop.",
      "enum": [
        "ubuntu@20.04",
        "ubuntu@22.04",
        "ubuntu@24.04",
        "ubuntu@26.04"
      ],
      "errorMessage": "The base must be one of the supported values: ubuntu@20.04, ubuntu@22.04, ubuntu@24.04, ubuntu@26.04."
    },
    "sdks": {
      "type": "array",
      "description": "List of SDKs used in the workshop.",
      "uniqueItems": true,
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the SDK. Optionally prefix with 'try-' or 'project-' exactly once to reference those sources.",
            "if": {
              "pattern": "^try-"
            },
            "then": {
              "maxLength": 44,
              "pattern": "^(?!(?:try-|project-)?agent$)try-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
            },
            "else": {
              "if": {
                "pattern": "^project-"
              },
              "then": {
                "maxLength": 48,
                "pattern": "^(?!(?:try-|project-)?agent$)project-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
              },
              "else": {
                "maxLength": 40,
                "pattern": "^(?!(?:try-|project-)?agent$)(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
              }
            },
            "errorMessage": "An SDK's name must contain a letter, may have a single 'try-' or 'project-' prefix, must not chain prefixes, and the underlying name cannot be 'agent'."
          },
          "channel": {
            "type": "string",
            "description": "Channel used for the SDK. Default is latest/stable unless the SDK is installed from a local source.",
            "pattern": "^(?:(?:[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*/)?(?:stable|candidate|beta|edge)(?:/[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9])?|[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*|)$",
            "errorMessage": "Channel must look like [<track>/]<risk>[/<branch>] or [<track>]."
          },
          "plugs": {
            "type": "object",
            "description": "Plugs for the SDK.",
            "patternProperties": {
              "^[a-z](?:-?[a-z0-9])*$": {
                "description": "Plug definition. Provide any YAML value for inline plug attributes, or set 'bind' to reference another plug. When 'bind' is set, no other attributes may be defined.",
                "properties": {
                  "bind": {
                    "type": "string",
                    "description": "Reference to a plug in the form [<sdk>:]<plug>. If omitted, the SDK is 'system'.",
                    "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
                    "errorMessage": "Bind reference must follow the pattern [<sdk>:]<plug>."
                  }
                },
                "if": {
                  "type": "object",
                  "required": [
                    "bind"
                  ]
                },
                "then": {
                  "type": "object",
                  "additionalProperties": false,
                  "errorMessage": "When 'bind' is set, no other attributes are allowed on the plug."
                }
              }
            }
          },
          "slots": {
            "type": "object",
            "description": "Slots available for the SDK.",
            "additionalProperties": true
          }
        },
        "required": [
          "name"
        ],
        "errorMessage": {
          "required": {
            "name": "Each SDK must specify a name."
          }
        },
        "additionalProperties": false
      }
    },
    "connections": {
      "type": "array",
      "description": "List of connections between plugs and slots.",
      "uniqueItems": true,
      "items": {
        "type": "object",
        "properties": {
          "plug": {
            "type": "string",
            "description": "Reference to a plug in the form [<sdk>:]<plug>. If omitted, the SDK is 'system'.",
            "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
            "errorMessage": "Plug reference must follow the pattern [<sdk>:]<plug>."
          },
          "slot": {
            "type": "string",
            "description": "Reference to a slot in the form [<sdk>:]<slot>. If omitted, the SDK is 'system'.",
            "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
            "errorMessage": "Slot reference must follow the pattern [<sdk>:]<slot>."
          }
        },
        "required": [
          "plug",
          "slot"
        ],
        "errorMessage": {
          "required": {
            "plug": "Each connection must specify a plug.",
            "slot": "Each connection must specify a slot."
          }
        },
        "additionalProperties": false
      }
    },
    "actions": {
      "type": "object",
      "description": "List of actions to be run in the workshop.",
      "patternProperties": {
        "^[a-z](?:-?[a-z0-9])*$": {
          "type": "string",
          "description": "Shell script."
        }
      },
      "additionalProperties": false,
      "errorMessage": "Action names must be unique and only appear once."
    }
  },
  "required": [
    "name",
    "base"
  ],
  "additionalProperties": false,
  "errorMessage": {
    "required": {
      "name": "The 'name' field is required.",
      "base": "The 'base' field is required."
    }
  }
}

Examples

This YAML file defines a golang workshop with a single go SDK from the 1.26/stable channel, and some useful actions:

.workshop/golang.yaml
name: golang
base: ubuntu@22.04
sdks:
  - name: go
    channel: 1.26
actions:
  lint: |
    go vet
    golangci-lint run
  tests: go test "$@"

This YAML file defines a go-dev workshop that uses the go SDK from the Store and an in-project SDK named tunnel; the data plug defined by the tunnel SDK is bound to the mod-cache plug of the go SDK:

.workshop/go-dev.yaml
name: go-dev
base: ubuntu@22.04
sdks:
  - name: go
    channel: edge
  - name: project-tunnel
    plugs:
      data:
        bind: go:mod-cache

This YAML file, besides using the fictional tensorflow, imagenet and cuda SDKs, defines an additional slot under the imagenet SDK, a plug under tensorflow and two connections:

  • One that connects the tensorflow:images plug to the newly defined imagenet:images slot.

  • Another that connects the tensorflow:cuda plug to the preexisting cuda:libs.

.workshop/digits-cuda.yaml
base: ubuntu@22.04
name: digits-cuda
sdks:
  - name: tensorflow
    plugs:
      cuda:
        interface: mount
        workshop-target: /usr/local/cuda/lib64
  - name: imagenet
    slots:
      images:
        interface: mount
        workshop-source: $SDK/images
  - name: cuda
connections:
  - plug: tensorflow:cuda
    slot: cuda:libs
  - plug: tensorflow:images
    slot: imagenet:images

See also

Explanation:

Reference: