Creating a Workshop
This guide explains how to create and configure workshops using the JSON format.
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:
- Settings - Global configuration for the workshop
- 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
| Property | Type | Description | Optional? |
|---|---|---|---|
name | string | Workshop title displayed to participants | 𐄂 |
hostAsPresenter | boolean | Whether the host controls the flow | 𐄂 |
removeLobby | boolean | Skip the lobby and start directly | ✓ |
allowJoin | boolean | Whether participants may join the match (default true); hosts are never blocked | ✓ |
showTimer | boolean | Display countdown timers | ✓ |
useChat | boolean | Enable chat functionality | ✓ |
darkText | boolean | Use dark text color | ✓ |
background | string | Background image or color | ✓ |
team | array | Team member information | ✓ |
skipAvatarSelection | boolean | Auto-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:
| Property | Type | Description |
|---|---|---|
autoNext | number | Seconds until the workshop moves on by itself; omit or -1 to wait for the host |
autoNextStoppable | boolean | Lets 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.
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
- Start with a welcome slide to introduce the workshop
- Use countdowns between major sections for smooth transitions
- Add presenter notes in
hostdatafor important talking points - Test quiz timing - ensure
answerTimeis appropriate for question difficulty - End with a scoreboard if using quizzes to celebrate participants
- Always include an End node to properly conclude the workshop
Tips
- Use
removeLobby: truefor seamless start in formal presentations - For a workshop that runs unattended on a screen, see Unattended (Kiosk) Workshops
- Add
backgroundcolors/images to slides for visual variety - Include
hostdata.noteswith key points and discussion questions - For image quizzes, use objects in
options:[{"imageUrl": "pic.jpg"}]