Workshop definition

A workshop definition is the YAML file that Workshop reads to launch and refresh a workshop. It names the base image, lists the SDKs to install, declares any extra plugs, slots, or connections, and records reusable shell actions. The file is authored by the workshop’s user.

Filename and location

A project may store a single workshop definition at its root, or several under .workshop/.

  • A single workshop: workshop.yaml or .workshop.yaml in the project directory.

  • Multiple workshops: .workshop/<NAME>.yaml, one file per workshop. The <NAME> part of the filename must equal the workshop’s name field.

  • A workshop name must start with a lowercase letter and may contain lowercase letters, digits, and hyphens between them. Up to 40 characters.

Top-level fields

Key

Value

Description

name (required)

string

Workshop identifier. Subject to the naming rules above. Must match the filename when the definition is under .workshop/.

base (required)

string

Base operating system image. One of ubuntu@20.04, ubuntu@22.04, ubuntu@24.04, or ubuntu@26.04.

SDKs that declare a base must use the same value; SDKs without a base are accepted on any workshop.

sdks

array

Ordered list of SDK entries. Each entry references an existing SDK and configures it for the workshop. The system SDK is installed first implicitly and is not required here. See SDK entry.

connections

array

Explicit connections between plugs and slots, applied on top of Workshop’s auto-connect logic. See Connection entry.

actions

object

Named shell scripts available via workshop run. See Action entry.

Nested structures

SDK entry

Each item in sdks is an object with these fields:

Key

Value

Description

name (required)

string

SDK identifier. The underlying name must contain at least one lowercase letter and may consist of lowercase letters, digits, and hyphens between them. agent is reserved.

Use a prefix to select the source:

  • no prefix: an SDK from the SDK Store (default).

  • try-<NAME>: a locally tried SDK in the try area.

  • project-<NAME>: an in-project SDK defined under .workshop/<NAME>/.

  • system: the built-in system SDK; listing it explicitly is rarely needed.

The fully prefixed name is at most 40 characters without a prefix, 44 with try-, and 48 with project-.

channel

string

Store channel from which to retrieve the SDK at launch and refresh. Uses the snap channel format: <TRACK>/<RISK>/<BRANCH>, with all three parts optional except that at least one must be present.

Default: latest/stable. Has no effect for try-, project-, and system SDKs, but must still be well formed.

Note

Quote channel values in YAML when they look numeric (for example, channel: "1.26") to avoid type coercion.

plugs

object

Plug bindings or additional plug definitions grafted onto the SDK by this workshop. See Plug or slot entry (under an SDK) and Interfaces.

slots

object

Additional slot definitions grafted onto the SDK by this workshop. Each entry specifies the interface and any interface-specific attributes. See Interfaces.

Plug or slot entry (under an SDK)

Each plug under an SDK is either an inline plug definition or a binding to another plug. Slots under an SDK are always inline slot definitions; slots cannot be bound.

Key

Value

Description

interface

string

Required for an inline plug definition; identifies the interface (for example, mount, tunnel). See Interfaces for the attributes each interface accepts.

bind

string

Reference to a target plug, in the form <SDK>:<PLUG>. The <SDK> part must name a non-system SDK, since bound plugs cannot target the system SDK.

A bound plug must not carry any other attributes, cannot belong to the system SDK, cannot chain (bind to a plug that is itself bound), and cannot also appear in connections.

any interface attribute

varies

Inline plug definitions accept the attributes documented under Interfaces.

Connection entry

Each item in connections links a plug to a slot of the same interface:

Key

Value

Description

plug (required)

string

Plug reference, in the form <SDK>:<PLUG>. The <SDK> part may be empty (for example, :ssh-agent) to refer to the system SDK. The referenced SDK must appear in sdks or be implicit (system, sketch).

slot (required)

string

Slot reference, in the form <SDK>:<SLOT>. Same rules as plug.

A plug that has a bind set under its SDK entry cannot also be listed in connections.

Action entry

Each entry in actions maps an action name to a shell script body:

Key

Value

Description

action name

string

Must start with a lowercase letter and may contain lowercase letters, digits, and hyphens between them.

action body

string

A bash script. Workshop sets errexit and pipefail before running it. Arguments passed after workshop run <WORKSHOP> are available as the standard positional parameters "$@", "$1", and so on.

Actions are interpreted lazily: edits to actions are available immediately, without workshop refresh.

Interfaces

The attributes accepted by inline plug and slot definitions depend on the interface. These same attributes appear in SDK definitions (SDK definition and SDKcraft project definition); a workshop may graft additional plugs and slots that follow them.

Camera interface

The camera interface exposes a host camera device.

  • Plug attributes: none.

  • Plug name: must be camera.

  • Plug owner: any regular SDK; not the system SDK.

  • Slot: the system SDK provides a single system:camera slot. Other SDKs cannot declare camera slots.

Custom device interface

The custom device interface exposes host devices that belong to a Linux kernel subsystem.

A custom device plug is described by this attribute:

Key

Value

Description

subsystem (required)

string

The Linux kernel subsystem of the host devices to expose, for example input, tty, or usb.

Plug owner: any regular SDK; not the system SDK.

Slot: the system SDK provides a single system:custom-device slot. Other SDKs cannot declare custom device slots.

Desktop interface

The desktop interface exposes the host display server.

  • Plug attributes: none.

  • Plug name: must be desktop.

  • Plug owner: any regular SDK; not the system SDK.

  • Slot: the system SDK provides a single system:desktop slot. Other SDKs cannot declare desktop slots.

GPU interface

The GPU interface exposes host GPU devices.

  • Plug attributes: none.

  • Plug name: must be gpu.

  • Plug owner: any regular SDK; not the system SDK.

  • Slot: the system SDK provides a single system:gpu slot. Other SDKs cannot declare GPU slots.

Mount interface

The mount interface exposes a directory between a slot owner and a plug owner.

A mount plug is described by these attributes:

Key

Value

Description

workshop-target (required)

string

Path inside the workshop used as the plug’s target directory. Must be an absolute path; $SDK expands to the SDK’s installation path in the workshop.

mode

integer

File permissions, in octal, applied when creating workshop-target and any missing parent directories. Defaults to 0o775 for regular users. When uid is zero, defaults to 0o755.

uid

integer

User ID applied when creating workshop-target and any missing parent directories. Defaults to 1000 when workshop-target is under /home/workshop/, /project/, or /run/user/1000/. Defaults to 0 otherwise.

gid

integer

Group ID applied when creating workshop-target and any missing parent directories. Defaults to 1000 or 0 by the same path rule as uid, even when uid is set explicitly.

read-only

Boolean

Whether the target directory should be read-only. Defaults to false.

Plug owner: any regular SDK; not the system SDK.

The system SDK provides one mount slot, system:mount, with a dynamic host-source attribute that can be configured only at remount. It is the only mount slot whose source is on the host filesystem.

A mount slot on a regular SDK is described by this attribute:

Key

Value

Description

workshop-source (required)

string

Path inside the workshop used as the slot’s source directory. Must be an absolute path; $SDK expands to the SDK’s installation path in the workshop.

SSH interface

The SSH interface exposes the user’s SSH agent socket.

  • Plug attributes: none.

  • Plug name: must be ssh-agent.

  • Plug owner: any regular SDK; not the system SDK.

  • Slot: the system SDK provides a single system:ssh-agent slot. Other SDKs cannot declare SSH slots.

Tunnel interface

The tunnel interface forwards a network address or Unix domain socket.

Both tunnel plugs and tunnel slots take a single attribute:

Key

Value

Description

endpoint

string

Network address or Unix domain socket that forms one end of the tunnel. Defaults to localhost/tcp for both plugs and slots.

The endpoint value follows this grammar:

Field

Format

Endpoint

<ADDRESS>/<PROTOCOL> for network endpoints; may be shortened to <ADDRESS> or <PROTOCOL> alone.

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

Address

<HOST>:<PORT>; may be shortened to <HOST> or <PORT>.

Protocol

Either tcp or udp. Defaults to tcp.

Host

An IPv4 or IPv6 address. When a port is supplied, IPv6 addresses must be enclosed in square brackets.

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

Port

A TCP or UDP port number (1-65535). May be omitted, but only on one side of a connection; both sides then use the same port.

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

Path

Absolute path to a Unix domain socket.

$HOME expands to the user’s home directory and $XDG_RUNTIME_DIR expands to the user runtime directory (typically /run/user/1000).

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

String

An abstract socket name.

Endpoints that start with [ or @ must be quoted in YAML:

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

JSON Schema

The following JSON Schema describes the structure 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": "Workshop name. Must start with a lowercase letter and may contain lowercase letters, digits, and hyphens between them. Up to 40 characters. Must match the definition file basename if the workshop is stored under .workshop/.",
      "pattern": "^[a-z](?:-?[a-z0-9])*$",
      "maxLength": 40,
      "errorMessage": "A workshop's name must start with a lowercase letter and can only include digits, lowercase letters, and hyphens joining them."
    },
    "base": {
      "type": "string",
      "description": "Base operating system image for the workshop. Must be one of the supported Ubuntu releases. SDKs with a declared base must match the workshop base; SDKs without a base are accepted on any 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": "Ordered list of SDKs to install on top of the base. Each entry references an existing SDK; names must be unique within the list. The system SDK is installed first implicitly and need not be listed.",
      "uniqueItems": true,
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "SDK name. Prefix with try- for a locally tried SDK, project- for an in-project SDK, or use system for the built-in system SDK; an unprefixed name resolves to an SDK from the Store. The underlying name must contain at least one lowercase letter, may consist of lowercase letters, digits, and hyphens between them, and cannot be agent. Without a prefix the name is at most 40 characters; with try- at most 44; with project- at most 48.",
            "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": "Store channel used to retrieve the SDK at launch and refresh. Snap-like format: [<TRACK>/]<RISK>[/<BRANCH>], or <TRACK>, or <RISK>/<BRANCH>. Risk is one of stable, candidate, beta, edge. Track is at most 28 characters; branch is at most 128 characters; neither may equal a risk name. Default is latest/stable. Only applies to SDKs from the Store; for try-, project-, and system SDKs the value has no effect but must still be well formed.",
            "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": "Plug bindings and additional plug definitions for the SDK. Each key must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them.",
            "additionalProperties": false,
            "patternProperties": {
              "^[a-z](?:-?[a-z0-9])*$": {
                "description": "Either an inline plug definition (a mapping whose keys are interface-specific attributes such as interface, workshop-target, endpoint), a string naming the interface, null to use the key as the interface name, or a binding to another plug declared in the same workshop. To bind, provide a single bind key whose value is the target plug reference; bound plugs cannot define other attributes and cannot also appear in connections.",
                "properties": {
                  "bind": {
                    "type": "string",
                    "description": "Plug reference in the form <sdk>:<plug>. The target plug must use the same interface as this plug, must not itself be bound, and must not belong to the system SDK (so the <sdk> portion must name a non-system SDK).",
                    "pattern": "^(?!system:)(([a-z0-9]-?)*[a-z](-?[a-z0-9])*):[a-z](-?[a-z0-9])*$",
                    "errorMessage": "Bind reference must follow the pattern <sdk>:<plug>, where <sdk> names a non-system SDK."
                  }
                },
                "if": {
                  "type": "object",
                  "required": [
                    "bind"
                  ]
                },
                "then": {
                  "type": "object",
                  "properties": {
                    "bind": true
                  },
                  "additionalProperties": false,
                  "errorMessage": "When 'bind' is set, no other attributes are allowed on the plug."
                }
              }
            }
          },
          "slots": {
            "type": "object",
            "description": "Additional slot definitions grafted onto the SDK by the workshop. Each key must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them. Each value is either an inline slot definition (a mapping with interface and any interface-specific attributes), a string naming the interface, or null to use the key as the interface name.",
            "patternProperties": {
              "^[a-z](?:-?[a-z0-9])*$": {
                "type": ["object", "string", "null"],
                "description": "Slot definition: an inline mapping with interface and any interface-specific attributes, a string naming the interface, or null to take the interface from the key."
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "name"
        ],
        "errorMessage": {
          "required": {
            "name": "Each SDK must specify a name."
          }
        },
        "additionalProperties": false
      }
    },
    "connections": {
      "type": "array",
      "description": "Explicit connections from plugs to slots, applied on top of auto-connection. Both endpoints must reference an SDK that is present in the workshop or is implicit (system, sketch), and must share the same interface. A plug that is bound elsewhere cannot also appear here.",
      "items": {
        "type": "object",
        "properties": {
          "plug": {
            "type": "string",
            "description": "Plug reference in the form <sdk>:<plug>. The <sdk> portion may be empty (for example, :ssh-agent) to refer to the system SDK.",
            "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
            "errorMessage": "Plug reference must follow the pattern <sdk>:<plug> (the <sdk> portion may be empty for the system SDK)."
          },
          "slot": {
            "type": "string",
            "description": "Slot reference in the form <sdk>:<slot>. The <sdk> portion may be empty (for example, :ssh-agent) to refer to the system SDK.",
            "pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
            "errorMessage": "Slot reference must follow the pattern <sdk>:<slot> (the <sdk> portion may be empty for the system SDK)."
          }
        },
        "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": "Named shell scripts available via workshop run. Action names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them. Each script body runs under bash as a login shell with errexit and pipefail set; positional parameters $@, $1, $2, ... receive the arguments passed after the workshop name.",
      "patternProperties": {
        "^[a-z](?:-?[a-z0-9])*$": {
          "type": "string",
          "description": "Shell script body executed by bash inside the workshop."
        }
      },
      "additionalProperties": false,
      "errorMessage": "Action names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens between them."
    }
  },
  "required": [
    "name",
    "base"
  ],
  "additionalProperties": false,
  "errorMessage": {
    "required": {
      "name": "The 'name' field is required.",
      "base": "The 'base' field is required."
    }
  }
}

Examples

Minimal workshop with one Store SDK and two 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 "$@"

Workshop with an in-project SDK and a plug binding between SDKs:

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

Workshop that grafts a plug and a slot onto its SDKs and adds explicit connections; besides using the fictional tensorflow, imagenet and cuda SDKs, it 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
name: digits-cuda
base: ubuntu@22.04
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

Workshop that pulls an SDK from the try area:

.workshop/try-go.yaml
name: try-go
base: ubuntu@24.04
sdks:
  - name: try-go

See also

Explanation:

Reference: