Appearance
VM JSON reference
One JSON file holds everything you wrote for a VM: its name, the build recipe, the version watches and the guide. Export JSON in a VM's ⋯ menu writes it, and Import VM from JSON in a project's ⋯ menu reads it.
Use it to:
- copy a VM to another project or account,
- keep a demo's definition in your own git repository,
- write a VM with a tool instead of the form. The Claude Code skill writes this file for you.
The file never holds what Demoshell produces: the saved machine, the screenshot, build logs, stats or ids. An imported VM is not built automatically. Press ▶ Build after the import.
A complete file
json
{
"kind": "demoshell.vm",
"version": 1,
"vm": {
"tag": "ripgrep",
"visibility": "public",
"mode": "manifest",
"description": "Search a source tree by regex"
},
"recipe": {
"base_image": "min",
"script": "apk add --no-cache ripgrep git\ngit clone -q --depth 1 https://github.com/BurntSushi/ripgrep /root/src\nprintf 'Try: rg \"fn main\"\\n' > /etc/motd",
"after_load": "cd /root/src\ncat /etc/motd",
"timeout_s": 600,
"vcpus": 1
},
"watches": [],
"guide": {
"title": "Search a source tree by regex",
"order_mode": "fixed",
"cta_label": "The ripgrep user guide",
"cta_url": "https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md",
"publish": true,
"steps": [
{
"title": "Find every main function",
"kind": "command",
"pattern": "rg \"fn main\"",
"output_contains": "main.rs"
},
{
"title": "List only the file names",
"body": "-l prints each matching file once, one path per line.",
"kind": "command",
"pattern": "rg -l",
"match_mode": "starts_with",
"replay_command": "rg -l Searcher"
},
{
"title": "Search your shell history",
"body": "Press Ctrl-R and type rg to find the commands you just ran.",
"kind": "command",
"pattern": "C-r",
"match_mode": "keys"
}
]
}
}The sample script installs ripgrep and clones a source tree to search. after_load opens each visitor's terminal in that tree and prints the hint. The guide has two commands to type and one key to press.
The top level
| Field | Rule |
|---|---|
kind | Required. Always "demoshell.vm". |
version | Required. The format version, 1. |
vm | Required. The VM's name and settings. |
recipe | How the VM is built. Leave it out or set null for a VM with no recipe yet. |
watches | Version watches. Leave it out, or [], for none. |
guide | The visitor guide. Leave it out or set null for none. |
Fields the format does not know are ignored. Optional text fields read as "not set" when they are missing, null or empty.
vm
| Field | Default | Rule |
|---|---|---|
tag | required | The VM's name, the :vm part of org/project:vm. Lowercase letters, digits and hyphens, at most 40 characters, no hyphen at either end. |
visibility | public | public or private. See Public and private. |
mode | not set | manifest for a VM built from its recipe, which is the normal case. interactive for a VM someone sets up by hand. Not set leaves the choice open in the app. |
description | not set | One short line for your own dashboard. |
recipe
The same settings as the build form in Create a VM.
| Field | Default | Rule |
|---|---|---|
base_image | required | min (Alpine, minimal) or docker (Alpine with Docker). See Base image. |
script | empty | The base image script. At most 64 KB. Runs as root, and must exit. |
after_load | not set | Commands run every time the VM loads, for the build and for every visitor. At most 4 KB. Sets the directory the demo opens in and its first screen. Cannot be combined with start_command. |
start_command | not set | The app the demo opens in. One line, at most 512 characters. It is left running when the machine is saved. Cannot be combined with after_load. |
web_url | not set | Makes it a web page demo. http://localhost:<port>/<path> or http://127.0.0.1:<port>/<path>, plain http only, at most 255 characters. See Open a web page. |
start_wait_s | 0 | Seconds to wait after the start command before saving. 0 to 600. |
timeout_s | 600 | How long the whole build may take, in seconds. 1 to 3600. |
vcpus | 1 | Cores, 1 to 4. Keep 1 unless the software really runs work in parallel. |
net_enabled | false | Internet access for visitors. The build always has internet. Paid plan only: the import is refused on other plans. |
Numbers are whole numbers. A demo has one startup answer: a shell prompt (after_load), an app (start_command) or a web page (web_url). An after_load and a start_command together are refused.
watches
A list. Each watch follows a release feed and rebuilds the VM when a new version appears. See Automatic builds. Watches are part of the paid and open-source plans.
json
{ "name": "widget", "kind": "github_release", "target": "acme/widget", "tag_filter": "v*", "enabled": true }| Field | Default | Rule |
|---|---|---|
name | required | Lowercase letters, digits and hyphens, at most 40 characters. Unique in the file. |
kind | required | github_release or docker_tag. |
target | required | owner/repo for github_release. image or registry/image for docker_tag. |
tag_filter | not set | A glob such as v* or *-stable. |
enabled | true | false keeps the watch but stops checking it. |
The recipe uses a watch's version as {{versions.<name>}} in script, start_command or after_load. Every name used there must have a watch in the same file, or the file is refused.
guide
See Create a guide for what each kind of step does.
| Field | Default | Rule |
|---|---|---|
title | empty | The guide's name. At most 200 characters. |
order_mode | fixed | fixed for numbered steps done in order, free for any order. |
cta_label | not set | The finish card's link text. At most 120 characters. |
cta_url | not set | The finish card's link. Must start with http:// or https://. At most 1024 characters. |
publish | false | true publishes the guide right after the import. Needs a verified email. If publishing fails, the import still lands and the guide stays a draft. |
steps | [] | The steps, at most 100. |
A step
| Field | Default | Rule |
|---|---|---|
title | empty | What the step is called in the checklist. |
body | empty | The hint under the title. Required on a manual step. |
kind | command | command for something the visitor types or presses, manual for something they mark done. |
pattern | empty | What the visitor types, or the key they press. Required on a command step. |
match_mode | exact | exact, starts_with, regex or keys. keys means pattern is a key, written as in Key press steps. |
require_success | true | Tick only when the command exits cleanly. |
output_contains | not set | Text the command must print. |
replay_command | not set | The command the automatic check types after each build. Visitors never see it. Set it when pattern is not a full command on its own, as with starts_with and regex. |
optional | false | Visitors may skip the step. |
web_match | not set | Web page demos only, on a manual step: the request the page sends when the visitor does the step, such as /welcome, POST /api/signup or * for any request. The step then ticks by itself. |
Every text field in a step is capped at 4000 characters.
A guide with publish: true must be ready to publish. It needs at least one step, every step needs a title, every manual step needs a body, and every command step needs a pattern. A draft (publish: false) may be unfinished.
Importing over an existing VM
Import a file whose tag is already a VM in the project, and the import updates that VM. The app lists what will change and asks first.
An update writes the recipe and the guide draft, and adds any watch whose name the VM does not have yet. It leaves the rest as it is:
visibility,modeanddescriptionkeep their current values.- A watch the VM already has stays unchanged, whatever the file says.
- The published guide stays live. The file's guide becomes the draft, and
publish: trueis ignored. Publish it from the guide editor when it is ready. - Nothing is rebuilt. The demo keeps serving its last build until you press ▶ Build.
To change a live demo's build script, export the VM, edit the script in the file, and import it again.
When an import is refused
The import modal reads the whole file before it creates anything, and names the first problem with the field it is in, for example "guide.steps[2].pattern" must be at most 4000 characters. Fix that field and import again. Nothing was created.
