VendoVendo Docs
Deploy & publishPublish to the catalog

Wizard layout

Design the configure-step UX tenants see when they deploy your tool.

When a tenant clicks Deploy on your catalog listing, they go through a wizard: account, configure, optionally pay, launch. The configure step is yours to shape. The wizardLayout field in the manifest splits that step into sub-steps and decides which integrations and userInputs[] keys render in each one.

When you need it

If wizardLayout is omitted (or null), the wizard falls back to a single-page Configure that stacks every required integration and every userInputs[] entry vertically. Fine for tools with 2–3 inputs. Painful at 10+.

Define wizardLayout when:

  • You have more than ~5 user inputs.
  • You want a "name your deployment" step separate from "connect integrations" separate from "advanced settings."
  • Some inputs are optional and you want them in a dedicated step.
  • You want a specific ordering for integrations vs user inputs.

Shape

The real schema lives in manifest.schema.json (WizardLayout, WizardSubStep, WizardSection defs) with the matching Zod parser in web/src/lib/wizard-layout.ts. The contract:

"wizardLayout": {
  "version": 1,
  "subSteps": [
    {
      "id": "name",
      "title": "Name your deployment",
      "sections": [
        { "type": "deployment_name" },
        { "type": "workspace" }
      ]
    },
    {
      "id": "connect",
      "title": "Connect integrations",
      "sections": [
        { "type": "integration", "providers": ["openrouter", "telegram"] }
      ]
    },
    {
      "id": "configure",
      "title": "Basic settings",
      "sections": [
        { "type": "wizard_inputs", "groups": ["basic"] }
      ]
    },
    {
      "id": "advanced",
      "title": "Advanced",
      "description": "Optional — defaults work for most cases.",
      "optional": true,
      "sections": [
        { "type": "wizard_inputs", "groups": ["*"] },
        { "type": "tool_extras" }
      ]
    }
  ]
}
  • version is always 1.
  • subSteps[] becomes the wizard pages. Each sub-step needs an id (URL-safe, used in ?phase={id}), a title (≤60 chars), and a non-empty sections[].
  • optional: true on a sub-step renders a Skip button and never blocks Continue.
  • sections[] is a discriminated union by type.

Section types

typeRendersNotes
deployment_nameThe deployment-slug input.Add once, usually in the first sub-step.
workspaceWorkspace picker (workspaces are tenant-side groupings).Optional in single-workspace tenants.
admin_credentialsEmail + password for the seeded admin.Only when adminBootstrap.authMode = "api_seed" or "manual_setup".
wizard_inputsA subset of userInputs[]. Filtered by the groups: string[] field below.The only section type that filters by an array.
integrationIntegration-binding rows for the providers listed in providers: string[].Filtered by integration provider slug.
tool_extrasPer-tool extras (deploy-hook config, custom toggles a tool registers).Most tools don't need this.

The full list is defined in web/src/lib/wizard-layout.ts — that file is the authoritative spec.

Filtering with groups[] and providers[]

Two section types take a filter array:

  • wizard_inputs.groups: string[] matches each userInputs[] entry by its wizardStep field. An entry whose wizardStep is in the section's groups array renders here.
  • integration.providers: string[] matches each entry in your top-level integrations[] by provider slug.

The sentinel ["*"] means "everything not placed by an earlier sub-step." Both filter arrays default to ["*"] when omitted. Use it in your last sub-step so a forgotten input or integration still surfaces.

{
  "type": "wizard_inputs",
  "groups": ["basic"]      // only userInputs with wizardStep="basic"
},
{
  "type": "wizard_inputs",
  "groups": ["*"]          // everything else not already placed
}
{
  "type": "integration",
  "providers": ["openrouter"]
},
{
  "type": "integration",
  "providers": ["*"]        // sweeps remaining providers
}

A userInputs[] entry whose wizardStep matches no section's groups (and where no section has ["*"]) is silently dropped from the wizard. The deploy worker will still inject the entry's default value if one is set, but the tenant has no way to override. Always end with a ["*"] sweep.

Resolution rules

  • First match wins. If two sub-steps could render the same input or provider, only the first one does. The second is a no-op.
  • Required integrations always block submission, regardless of which sub-step they appear in. Mark them "optional": true on the top-level integrations[] entry if they're skippable.
  • Empty arrays render nothing. "groups": [] is valid and means "skip this section." Useful for showWhen-style conditional gating (when v2 lands).

Designing the steps

Three patterns that work:

Pattern 1 — name, connect, configure. Three sub-steps: deployment_name/workspaceintegration (all providers) → wizard_inputs (["*"]). Hermes uses a flatter two-step variant of this.

Pattern 2 — single Configure step. Skip wizardLayout entirely. Right for tools with 1–3 inputs and zero or one integration.

Pattern 3 — name, connect, basics, advanced. Four sub-steps. Promote the most common inputs (wizardStep: "basic") to step 3; sweep the rest into step 4 with optional: true so tenants can skip past it.

Keep titles imperative ("Connect Telegram", "Configure the bot"), not descriptive. Tenants are doing each step, not reading about it.

Where it ends up

The vendo-templates sync action copies wizardLayout verbatim into tool_releases.wizard_layout. The deploy modal at web/src/components/onboarding/ reads from that column and renders each sub-step in order. The wizard-inputs sync also extracts userInputs[] into tool_releases.wizard_inputs (with wizardStep preserved) so the section filters can match.

Iterating

Wizard layout is part of the manifest, so changes ship as new versions. You don't need a code release to re-order the wizard — just push a new {slug}/{version}.json to vendo-templates and write the matching release migration. Existing tenants keep the layout they deployed against; new deploys use the active release's layout.

Next: Submission — opening the PR.

On this page