Skip to main content

Creating a Workshop

This guide explains how to create and configure workshops using the JSON format.

Where workshops live

Writing these JSON files by hand is currently the only way to author or change a workshop — there is no editor. The files in apps/web/workshops/ are the workshops themselves, not samples to copy from.

They are seed data: the running server reads workshops from Nakama's object storage, and /StoragePush in the web app is what puts them there. A file that has never been pushed does not exist for the server, and a workshop edited after being pushed needs pushing again.

Workshop Structure​

Workshops consist of two main parts:

  1. Settings - Global configuration for the workshop
  2. Nodes - Individual activities and content elements

Basic Structure​

{
"settings": {
// Workshop configuration goes here
},
"nodes": [
// Array of workshop activities goes here
]
}

Settings Configuration​

The settings object configures global workshop properties:

{
"settings": {
"name": "Workshop Title",
"hostAsPresenter": true,
"removeLobby": true,
"allowJoin": true,
"showTimer": true,
"useChat": true,
"darkText": false,
"background": "",
"team": []
}
}

Available Settings​

PropertyTypeDescriptionOptional?
namestringWorkshop title displayed to participants𐄂
hostAsPresenterbooleanWhether the host controls the flow𐄂
removeLobbybooleanSkip the lobby and start directly✓
allowJoinbooleanWhether participants may join the match (default true); hosts are never blocked✓
showTimerbooleanDisplay countdown timers✓
useChatbooleanEnable chat functionality✓
darkTextbooleanUse dark text color✓
backgroundstringBackground image or color✓
teamarrayTeam member information✓
skipAvatarSelectionbooleanAuto-confirm the avatar on a PlayerSelect node instead of asking✓

Team Configuration​

"team": [
{
"name": "Max",
"img": "techagogics-Max.png",
"show": true
}
]

Node Types​

Nodes are the building blocks of your workshop. Each node has a type and associated data. For specific nodes and their structure, checkout the node types.

Automatic progression​

Two keys sit next to type on any node, outside data:

PropertyTypeDescription
autoNextnumberSeconds until the workshop moves on by itself; omit or -1 to wait for the host
autoNextStoppablebooleanLets the host halt that countdown from the footer (default false)
{
"type": "Slide",
"autoNext": 8,
"autoNextStoppable": true,
"data": { "headline": "Level 1" }
}

With autoNextStoppable: true, a Stopp button appears in the footer next to Zurück/Weiter while the countdown runs. It freezes the countdown where it is — the remaining seconds stay on screen — and turns into Fortsetzen, which picks it up from that point. Only hosts see the button and only hosts can trigger it; the server ignores the request from anyone else and from any node that did not opt in. A halt never carries over: moving to another node clears it.

A Quiz node can be halted in both of its timed phases. The question phase runs on answerTime, the solution phase on autoNext — a halt freezes whichever is running, and it also holds the scoring clock and stops the quiz from moving on just because everyone happened to answer. The button is offered only while something actually counts down, so a quiz with showAnswerManually: true or without an answerTime shows no button during the question.

Sound Configuration​

Add sound effects to any node:

"sound": {
"url": "/sound/plop.wav",
"volume": 0.5,
"repeat": false
}

Content Formatting​

Images​

In content arrays or standalone:

"content": ["img(filename.jpg)", "img(another.png)"]

With captions:

"content": ["img(photo.jpg) Photo by Author Name"]

Multiple Content Items​

"content": [
"img(image1.jpg)",
"img(image2.png)",
"Text content can also be included"
]

Where the media live​

Workshop definitions are versioned in Git; the images and sounds they reference are not — those live in the media bucket that NEXT_PUBLIC_RESOURCE_LOCATION points at. Adding a workshop therefore has two halves, and forgetting the second one only shows up as a broken slide in front of participants.

Most references resolve into a directory named after the workshop, which is the JSON file name without its extension. deepfake_detective_quiz.json referencing einstein.jpg expects the object at deepfake_detective_quiz/einstein.jpg. That applies to imageUrl, img, background, and to the img(...) and video(...) entries inside content.

A value starting with a slash is different: it is resolved from the bucket root, so "/sound/plop.wav" means sound/plop.wav. Files that ship in apps/web/public — the avatars, for instance — are served by Next.js and are never expected in the bucket.

Checking before you merge​

pnpm run media-manifest        # what this repo state expects in the bucket
pnpm run media-check "$NEXT_PUBLIC_RESOURCE_LOCATION"

media-manifest derives the list from the workshop files, so there is no list to maintain. media-check fetches each object and reports what is missing, which workshop asked for it, and whether the object is absent (404) or present but not publicly readable (403) — two different fixes.

The same check runs automatically on every pull request that touches apps/web/workshops/, against the staging bucket. Upload your media there before opening the PR and it stays green.

Sounds go by name, not by path

The default sound library is registered in soundUtil.ts, so a node asks for a sound by name and never spells out a path:

"sound": { "name": "Plop", "volume": 0.5, "repeat": false, "target": "Everyone" }

target is Host, Client or Everyone. Writing a url instead works, but then the file has to exist in the bucket under exactly that path — and a name that is not in the library resolves to nothing.

The manifest reads the map out of soundUtil.ts rather than copying it, so a sound added there is checked without anyone remembering this script.

Complete Example​

{
"settings": {
"name": "My First Workshop",
"hostAsPresenter": true,
"removeLobby": true,
"showTimer": true
},
"nodes": [
{
"type": "Slide",
"data": {
"headline": "Welcome!",
"text": "Let's get started"
}
},
{
"type": "Countdown",
"autoNext": 5
},
{
"type": "Quiz",
"data": {
"text": "What color is the sky?",
"options": ["Red", "Blue", "Green"],
"answer": [2],
"answerTime": 30,
"showScoreboard": true
}
},
{
"type": "Scoreboard",
"data": {
"showPodium": true
}
},
{
"type": "End"
}
]
}

Best Practices​

  1. Start with a welcome slide to introduce the workshop
  2. Use countdowns between major sections for smooth transitions
  3. Add presenter notes in hostdata for important talking points
  4. Test quiz timing - ensure answerTime is appropriate for question difficulty
  5. End with a scoreboard if using quizzes to celebrate participants
  6. Always include an End node to properly conclude the workshop

Tips​

  • Use removeLobby: true for seamless start in formal presentations
  • For a workshop that runs unattended on a screen, see Unattended (Kiosk) Workshops
  • Add background colors/images to slides for visual variety
  • Include hostdata.notes with key points and discussion questions
  • For image quizzes, use objects in options: [{"imageUrl": "pic.jpg"}]