Skip to content

Launchpad event schema

launchpad up --headless and launchpad down --headless stream one JSON event per line to stdout. This is the automation surface: assert on these events from CI, scripts, or a driving TUI.

All events share the same top-level shape. Only fields relevant to the event kind are populated.

json
{
  "ev":       "<kind>",        // discriminator; see below
  "time":     "2026-07-02T16:05:14.089Z",
  "phase":    "<phase>",       // only on ev=phase
  "vm_key":   "mssp",          // scopes VM-level events
  "step":     "install",       // sub-phase within a VM
  "percent":  60,              // 0-100
  "message":  "...",           // human-readable
  "level":    "info",          // for vm_log
  "gate_id":  "...",           // for gate_open / gate_resolved
  "instructions": "...",       // for gate_open
  "copy_text":    "...",       // for gate_open
  "ipv4":     "100.x.x.x",     // for vm_ready
  "ipv6":     "...",
  "ssh_user": "ops",
  "ssh_port": 22,
  "fields":   { "capabilities": ["vm.plan"] },  // free-form; used by plugin_ready
  "error":    { "category":"...", "code":"...", "message":"...", "hint":"..." }
}

Event kinds

evEmitted whenTerminal?
phaseThe orchestrator transitions phase.no
plugin_readyThe provisioning plugin has spawned + returned its hello handshake.no
vm_planDry-run description of what the plugin would create, per VM.no
vm_progressPer-VM sub-step progress (has step + percent).no
vm_readyPlugin has created + verified the VM.no
vm_logLog line from either the plugin (progress relay) or a launchpad-driven install shell.no
gate_openManual gate reached; requires operator confirmation.no
gate_resolvedOperator (or --auto-resolve-gates) closed the gate.no
errorFatal error. error.category + error.code are stable identifiers.yes
completeWhole flow ran clean.yes

error and complete are the two terminal events. Every launchpad run emits exactly one.

Phase order (up)

initializing → planning → provisioning → installing → complete

Per-VM within provisioning, the steps lookup → prepare → image_cache|image_download → tailscale → cloud_init → disk → boot → wait_ready are emitted as vm_progress. The install step during installing streams the underlying installer's stdout as vm_log.

Phase order (down)

tearing_down → torn_down → complete

vm.destroy is called per VM in reverse-provision order (tenants first, MSSP last). Each per-VM emit is a vm_progress with step=destroy.

Error taxonomy

error.category is one of ten stable identifiers the launchpad + all first-party plugins commit to:

CategoryMeaningRetryable
authCredential missing, invalid, or lacks scope.no
validationConfig or input malformed.no
not_foundReferenced entity doesn't exist.no
already_existsIdempotent create failed because the entity is present.no
provider_unavailableUpstream provider (Tailscale, Hetzner, ...) is unreachable.yes
quotaProvider-side quota exhausted.no
timeoutWait exceeded a policy deadline.yes
internalPlugin/orchestrator bug — unexpected error path.no
networkLocal network / TLS / DNS.yes
cancelledCtrl-C or SIGTERM.no

error.code is a plugin-namespaced identifier under the category (e.g. qemu.image.sha256_mismatch). Categories are stable; codes may be added.

Consuming from bash

bash
launchpad up --config pilot.yaml --headless --auto-resolve-gates | \
  jq -c 'select(.ev == "phase" or .ev == "error" or .ev == "complete")'

To gate a CI job on completion:

bash
launchpad up --config pilot.yaml --headless --auto-resolve-gates > run.log
grep -q '"ev":"complete"' run.log || {
  jq -r 'select(.ev == "error") | "\(.error.category)/\(.error.code): \(.error.message)"' < run.log
  exit 1
}

Version compatibility

  • Field additions are non-breaking.
  • Field removals bump the launchpad major version.
  • error.category values are permanent. ev values are permanent.
  • error.code values may be renamed within the same category (they're plugin-scoped).

Released under the Apache 2.0 License.