Skip to content

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 ​

FieldRule
kindRequired. Always "demoshell.vm".
versionRequired. The format version, 1.
vmRequired. The VM's name and settings.
recipeHow the VM is built. Leave it out or set null for a VM with no recipe yet.
watchesVersion watches. Leave it out, or [], for none.
guideThe 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 ​

FieldDefaultRule
tagrequiredThe VM's name, the :vm part of org/project:vm. Lowercase letters, digits and hyphens, at most 40 characters, no hyphen at either end.
visibilitypublicpublic or private. See Public and private.
modenot setmanifest 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.
descriptionnot setOne short line for your own dashboard.

recipe ​

The same settings as the build form in Create a VM.

FieldDefaultRule
base_imagerequiredmin (Alpine, minimal) or docker (Alpine with Docker). See Base image.
scriptemptyThe base image script. At most 64 KB. Runs as root, and must exit.
after_loadnot setCommands 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_commandnot setThe 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_urlnot setMakes 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_s0Seconds to wait after the start command before saving. 0 to 600.
timeout_s600How long the whole build may take, in seconds. 1 to 3600.
vcpus1Cores, 1 to 4. Keep 1 unless the software really runs work in parallel.
net_enabledfalseInternet 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 }
FieldDefaultRule
namerequiredLowercase letters, digits and hyphens, at most 40 characters. Unique in the file.
kindrequiredgithub_release or docker_tag.
targetrequiredowner/repo for github_release. image or registry/image for docker_tag.
tag_filternot setA glob such as v* or *-stable.
enabledtruefalse 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.

FieldDefaultRule
titleemptyThe guide's name. At most 200 characters.
order_modefixedfixed for numbered steps done in order, free for any order.
cta_labelnot setThe finish card's link text. At most 120 characters.
cta_urlnot setThe finish card's link. Must start with http:// or https://. At most 1024 characters.
publishfalsetrue 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 ​

FieldDefaultRule
titleemptyWhat the step is called in the checklist.
bodyemptyThe hint under the title. Required on a manual step.
kindcommandcommand for something the visitor types or presses, manual for something they mark done.
patternemptyWhat the visitor types, or the key they press. Required on a command step.
match_modeexactexact, starts_with, regex or keys. keys means pattern is a key, written as in Key press steps.
require_successtrueTick only when the command exits cleanly.
output_containsnot setText the command must print.
replay_commandnot setThe 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.
optionalfalseVisitors may skip the step.
web_matchnot setWeb 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, mode and description keep 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: true is 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.