# Wizards — one-run site installers

The site gallery (admin → Site → Scenarios → «Создать сценарий») offers **wizards** alongside the plain flow presets: pick a card, answer a short form of steps, review the resulting plan, and one run installs a complete, working piece of a site — data types, seeded demo records, scenarios, routes, templates, static files, library queries. The store card installs a complete storefront in one run in about twenty seconds; the other cards are smaller.

Wizards are the fastest way a project gets a real site on the tenant, and they are also how many tenants you will meet are shaped: a project whose scenarios are named `shop_catalog`, `shop_product`, `shop_pay_yookassa` or whose page flow renders `landing/<handle>/landing.liquid` was installed by a wizard card. Everything after the install is ordinary tenant content — editable with the same flowctl commands as any hand-built site. This page is the catalog: what each card installs, how a re-run behaves, and where to edit afterwards. The store card has a deep-dive of its own — `shop-wizard.md` (its contents, its traps, and the storefront design canon); the flow engine behind every install is `flows-guide.md`.

## The cards

Four wizards install a whole site surface; all four are flagged with a quiet «Экспериментальный» pill unless noted — expect the artifact set to evolve between versions.

| Card («Магазин» etc.) | Wizard id | What one run installs | Experimental | Docs page |
|---|---|---|---|---|
| **«Магазин»** (Online store) | `shop` | A complete storefront: 13 data types (products with variants, category tree, orders, carts, promo codes, reviews, favorites, addresses, СДЭК delivery settings, legal documents, shop settings in one record), 26 scenarios in the declaration — 21 or 24 install per the answers (the unused payment provider's pair and, unless СДЭК delivery is chosen, its trio are skipped), 27 routes, 22 templates, 12 static files, 25 library queries, 10 demo photos, order emails, legal pages (оферта + политика as markdown documents at `<store-path>/offer` and `<store-path>/privacy`, editable as content), a contacts page, optional СДЭК delivery (rates widget proxy, status webhook, per-order operations) — on a store path of your choice (`/shop`, `/catalog` or `/store`), with member login/register/account on the root. Payment provider (YooKassa or T-Bank) is a wizard step — only the chosen pack installs. | yes | `dynapi.ru/docs/shop-wizard` |
| **«Лендинг»** (Landing page) | `landing-page` | A one-page site from CMS content — **installable several times per tenant** (each landing = its own handle, e.g. `landing`, `landing-2`): 20 SHARED data types (a dozen optional page sections + the content records that feed them; the page record owns every block through ONE polymorphic `blocks` list, sections own their items), the page scenario (ONE resolver fetch keyed on the runtime handle slug + render) and a SHARED lead POST scenario answering on every landing's path, one template + css/js in the handle's file folder (`landing/<handle>/…`), a subdomain step with live availability check, a 4-palette accent picker, SEO/OG fields. Nothing is path-baked: the form action, canonical and lead redirects read the current route at runtime — moving a landing is a one-line route edit. Texts are seeded as content records — no lorem in the template. | yes | — |
| **«Приём оплаты через ЮKassa»** (YooKassa payments) | `yookassa-payment` | A payment pack for a site that already has an order entity: 3 scenarios (the payment flow lands on the scenario you have open; an order-status page and a webhook receiver are created alongside), 2 routes (your return path + `/yookassa-hook/<token>`), 1 status-page template, the `yookassa_basic` secret, optional 54-ФЗ receipt steps. | no | `dynapi.ru/docs/flows-payments` |
| **«Приём оплаты через Т-Банк»** (T-Bank payments) | `tbank-payment` | The same pack shape for T-Bank acquiring (EACQ): payment flow + order-status page + webhook receiver, your return path + `/tbank-hook/<token>`, the `tbank_password` secret, optional 54-ФЗ receipt steps. | yes | `dynapi.ru/docs/flows-payments` |
| **«Доставка СДЭК для магазина»** (СДЭК delivery) | `shop-delivery-cdek` | A delivery pack for a tenant that already has the store installed (guarded by `requires_entity_slugs`: `shop-config` + `order`): 3 scenarios (widget rates proxy, status webhook, per-order operations), 3 routes under the store path (`/api/delivery/cdek/service`, `/api/cdek/webhook/<token>`, `/api/cdek/admin`), the `cdek-config` type + an `order.cdek_uuid` field, patches to `checkout.liquid` / `order-status.liquid` / `cart.js`, the `js/cdek.js` widget, test+prod credential secret pairs. The store card carries the same pack inline behind its «Доставка» step — this standalone card adds it to a store installed earlier without it. Credentials are optional: without them the widget calculates no rates. | yes | — |

The gallery also carries five **single-flow builders** ported from the flow presets — they install exactly one scenario, not a site: **Captcha form** (one step: Turnstile / Yandex SmartCaptcha / reCAPTCHA — a guard flow that answers 403 on a missing or failed token), **Telegram notification** (relays a form message to a bot webhook; no steps), **JSON endpoint** (a `respond_json` data endpoint; no steps), **Logout** (clears the session cookie and redirects home) and **Page** (entry + your library queries + template render — the flow *is* the page; two steps: template + queries). The auth presets (login/register/forgot/change-password) are separate — `presets/*.dynflow.json` and `member-auth.md`.

## How an install runs

The runner is a three-stage modal: **steps → plan → apply**. The steps form is a declaration (text answers, selects, file uploads, a subdomain availability check, secret fields; `media_url` steps — og:image and other image URLs — get a media-library picker button with a preview, manual paste still works); «Далее» compiles it into a plan — a numbered list of every operation the run will perform (create type, seed rows, save template, publish route, …) — and the apply stage executes the plan in order with per-op status (`done` / `skipped` / `warned` / `failed`).

Worth knowing before a run:

- **Secrets never enter the plan.** Values typed into secret steps live in memory only, are written to the tenant's write-only secret store during the run, and are wiped when the modal closes. The plan shows the secret name, never the value.
- **The plan preview is the contract.** If a step answer removes a piece (an untoggled landing block, no PDF uploads), the corresponding operations simply do not appear in the plan — there are no dead leftovers to clean up afterwards.
- **A run is paced but not atomic.** A store install is ~100+ management operations; the runner spaces them out, but cancelling mid-run or a hard failure can leave a partial install (the store's cancelled-install shadow-scenario trap is in `shop-wizard.md`). Let a run finish; if it failed, re-running is safe — see the next section.
- **Every card is an API call, not just an admin-UI action.** `flowctl wizard list` shows what the platform currently installs (and every step, required ones marked `*`); `flowctl wizard install <id> --flow <slug> [--set K=V …]` applies it in one command — e.g. `flowctl wizard install landing-page --flow landing-2 --set page_path=/promo` (`--dry-run` prints the plan; answers via `--set K=V` or `--answers file.json`). The declaration and the apply logic live SERVER-side (the REST pair `GET /cms/wizards` + `POST /cms/wizards/install`): gated like the gallery (admin role or an API key scoped site:manage+data:write) plus the catalog gate — a paid card requires the tenant's purchase, free cards install directly. No client carries wizard data that can go stale.

## Re-runs: what a reinstall does and does not do

Re-running a wizard card on a tenant that already has the install is **additive for schema, refresh-by-design for everything else**:

- **Existing data types receive the delta.** Properties the live type lacks are appended on re-run (a newer wizard version's new fields arrive without a reinstall); conflicting property shapes are left untouched and flagged — the admin UI's re-run report is the conscious path for those.
- **Existing scenarios are refreshed, with one owner guard.** A flow whose slug already exists is re-saved from the canonical declaration (draft + publish — «refresh semantics» in the op log). Hand edits to flow LOGIC do not survive a re-run. The exception: if the live render node points at a hand-edited template file that exists, the refresh keeps the owner's template path — a customized page is never silently swapped to stock files.
- **Files are full-writes unless declared otherwise.** Templates/statics marked `when_missing` skip when present; ones carrying `patches` get targeted anchor patches applied to the live copy; everything else is overwritten with the canonical content — a hand-edited full-write template is lost on a re-run. Pull the live copy first when in doubt.
- **Library queries are always overwritten.** Wizard installs upsert and publish their library entries on every run, so hand edits to the installed `shop_*` queries are lost on a re-run. Never re-run a store card casually after customizing the query library.
- **Existing secrets are kept.** A secret step with no new value skips — re-running never blanks a stored credential (write-only values cannot be read back, so there is nothing to restore from).
- **An unchanged subdomain claim skips.** Empty answer = keep the current address; a re-run never silently rebinds a live site.

The consequence for version updates: schema deltas and targeted patches ride the re-run itself — a newer wizard version delivers its new type fields and file patches by re-running the card. What a re-run will NOT do is preserve your customizations: export your edited flows, pull your hand-edited files, save your library-query edits before re-running, and re-apply them after. Only a deliberate full reinstall (with content export) resets a project to the factory install.

## The landing card is the exception: multi-install, keyed on the handle

The landing wizard can be installed **several times on one tenant**. The types are shared and NOT suffixed — the install identity is the HANDLE the run was launched on (`landing`, `landing-2`… the creation screen auto-suffixes the handle slug):

- **A landing = one `landing-page` record** whose `slug` equals the handle slug. The page scenario resolves it with a single query `landingPages(where: { slug: { eq: "{{ submission.flow }}" } })` — `submission.flow` is the dispatched handle's slug, injected at runtime, so nothing in the flow is baked.
- **ONE `blocks` list owns every block (wizard v2).** The page declares a single polymorphic `blocks` array holding the hero, meta, SEO, header, menu, form and section records; each section holds its own `items` (features, stats, prices…). Queries start from the landing-page record and split the list by `__typename`. There is NO ownership field in any block type or row — the page's lists are the record of ownership. (The lead type keeps a visible `landing` relation: leads are runtime data, not blocks.)
- **The page order IS the `blocks` list.** The template renders `<main>` as one loop over the visible blocks (hero + sections) in list order, so dragging a block in the page editor moves it on the page — nothing is pinned. The section type has NO `sort` field. Chrome stays structural: meta/SEO feed `<head>`, header/menu are the site header, and the form renders inside its `lead` section. The site name in the chrome falls back to the meta title if the hero block is removed.
- **A re-run preserves the lists.** It reads `blocks` and each section's `items`, keeps every entry in place (position included) and appends only the rows this run created. Records a human adds through the page editor survive; an emptied list is re-seeded with the demo batch.
- **Nested containers resolve THROUGH the page.** A section is found among `landing-page.blocks` (`link_to.via`), never by a global slug match — two installs on one tenant share the section type and its slug values.
- **Switched-off blocks create nothing — fragments are pruned.** A block toggled off (`shows_if`) contributes no type and no rows; the installer removes its `... on <Type>` inline fragment (and named-fragment spread) from the flow and library-query strings, so the saved query always matches the created schema. A section without items renders nothing.
- **Versioning: v1 tenants stay on v1.** The current model is `wizard_version: 2`. A tenant that installed the pre-blocks v1 keeps re-running the FROZEN v1 declaration — the server dispatches by the tenant's own schema (`landing-page.blocks` marks v2; the old `hero`/`sections` fields mark v1). Old landings are never migrated by a re-run; a fresh tenant always gets v2.
- **Renaming is cheap BY DESIGN**: rename the handle (route bindings follow automatically) + edit the ONE `landing-page` record's slug. Not a single block record changes.
- **Paths are never baked.** The form action, canonical/og:url and the lead flow's redirects read `{{ submission.route_path }}` at runtime; a GET and the POST binding of one path live in a single route entry, so moving a landing is one edit in the route editor.
- **Files live in the handle's folder** (`landing/landing.liquid`, `landing-2/landing.css`, …) — created once, never renamed.
- **A second install** reuses the shared types and seeds ITS OWN rows: the new page's `blocks` list is empty, so `when_missing: linked` seeds the demo batch, while the first landing's list is untouched. The lead scenario is PER-INSTALL: handle `<flow>_lead` (e.g. `landing_lead`, `landing-2_lead`) answering only on its own landing's POST path — different landings, different lead handlers; the `page` field of a lead records the path the form actually posted to. Legacy tenants (pre-multi-install, flat `landing.liquid`) keep working; re-running the card on the old handle re-points the flow to the folder copy.
- **Subdomain is per-TENANT**, not per-landing: the second install leaves the address step empty; both landings answer on the same domain, each on its own path.

## Secrets

Wizards follow the platform secret pattern: a config leaf whose whole value is `secret://<name>` resolves server-side from the tenant's write-only encrypted store — values are never inline in flows and never readable back. The store cards write `yookassa_basic` (the composite `Basic base64(shopId:key)` the YooKassa nodes actually read) and `tbank_password`; re-issuing a provider key is a secret upsert, not a reinstall (the recipe is in `shop-wizard.md`). From the API side the same store is `flowctl secret set` / `upsertTenantSecret`; the reference rules (`secret://` in action configs and condition clauses, `secret("name")` in expr, fail-closed unknown names) are `flows-guide.md` §5.

## Editing a wizard-installed site

Everything on the tenant after an install is standard site material — routes, scenarios, templates, static files, library queries, content — and follows the same draft/publish model as any hand-built site (`workflows.md`, `flows-guide.md`). Three wizard-specific cautions:

- **Wizard-processed files carry substituted values.** Templates and statics on the site have the store path and the tenant's palette baked in; the public storefront sources carry placeholders instead (`__SHOP_BASE__`, `${variant.*}`). Pushing a raw source over the live copy breaks the site while it keeps rendering fine — the tokens.css palette trap and its byte-guard recipe are in `shop-wizard.md`. Pull the live copy first, edit that.
- **The design canon is a contract.** The storefront (and the landing) follow a written visual language — tokens-only colors, rem spacing scale, three structural hairlines, dark-theme rules, WCAG AA. It is spelled out in `shop-wizard.md` ("Storefront design canon"); keep it when editing templates or CSS.
- **Live-linked library queries.** The storefront's read queries are library entries resolved at execution time — editing an entry re-skins every scenario that references it on the next render, and a referenced entry cannot be deleted. Details: `shop-wizard.md` and `flows-guide.md` §3.

## Payment presets

All payment installs share one provider contract. In the store, the «Провайдер оплаты» step installs **only** the chosen provider's pack (pay flow + webhook flow + routes + secret + the `payment_provider` setting flip); checkout ends in a dispatcher that redirects to `/api/pay/<active provider>?order=<id>`, the pay flow guards the order status and pays the order row's amount, and webhook tokens are per-provider so the previous provider's webhook stays mounted across a switch. The standalone YooKassa / T-Bank cards install the same contract for sites that are not stores (they adapt to an existing order entity and your return path). Switching a store's provider, the per-provider token behavior, signature/receipt details and the E2E checklist are in `shop-wizard.md` ("Payment providers").

## Where the cards come from

Wizards ship with the platform as compiled-in declarations; the storefront's page sources (the HTML/CSS/JS the store template artifact set is built from) are upstream platform sources. As an API agent you normally edit the **installed result on the tenant** — `flowctl template list`, `files push`, `flow save`, `query save` — not a generator: the tenant's copies are authoritative once the install has substituted paths, palettes and secrets into them. Treat generator-level changes (new wizard steps, new packs) as upstream platform work.

## Authoring a new wizard: the витрина checklist

A new wizard reaches users through THREE surfaces at once, and an authoring agent is responsible for all of them — a spec that installs cleanly but ships with an empty card is an incomplete deliverable.

**1. The spec (`*.wizard.json`) — the grammar is closed.** The applier runs a STRICT drift guard: unknown top-level keys, unknown artifact kinds, unknown step keys, missing `format: "dynapi-wizard", "version": 1` — all loud rejections at the catalog door (the platform's release pipeline and vendor upload both validate with the same parser the installer uses). A declaration must live in its own namespace (the landing lesson): entity slugs, flow handles, template/static folders and route paths all carry the wizard's prefix or a `${flow}` key, so several wizards co-exist in one tenant without collision. Wizard-global routes (`/login`, `/404`, `GET /`) are allowed only for a declared one-per-tenant wizard (the store) — document that in the card. Artifacts under `media:` are carried to demo installs by the platform (the control plane downloads `source_url` and uploads the bytes) — keep the names stable, they are create-or-reuse keys.

**2. The card (vitrina) — what users see on dynapi.ru/templates and in the admin's «Быстрый старт».** The control plane seeds the card from the spec on sync (title/description ru+en, scenario, icon, cost) but the finished card needs the vendor's pass:

- `scenario` set (`landing` / `shop` / `blog` / `other`) — drives the category chips on /templates and the quick-start filter. Empty reads as `other`.
- `title` + `description` {ru, en} — the card copy; the description is the SHORT line (one sentence), no lorem, no marketing fog.
- `description_long` {ru, en} — the DETAIL page body (`/templates/<wizard_slug>`): two-plus paragraphs answering что / как / почему — what one run installs, how it is structured on the tenant (types, scenarios, folders), why it is built this way, what the user edits afterwards. This is the SEO surface: the detail page's meta description is cut from it. Write it as prose, not bullet soup.
- `image_url` — a 16:9 screenshot of the demo (not a logo). Uploaded to the dynapi tenant's media; pasted in the vendor console.
- `demo_url` — a live demo. Produce it with the vendor card's «Установить/Обновить демо» (demo tenant slug + the saved answers; re-runs are additive, media is carried automatically) — the demo stays in sync with the spec forever after.
- `prereq_note` {ru, en} — a pack that needs another wizard installed says so in one line («Требуется установленный Магазин»); the runtime guard is `requires_entity_slugs` / `requires_entity_props` in the spec.
- `price` — empty = free; a number turns on the purchase gate (402 → buy panel) in the admin and on install.
- Card published (the vendor «Показывать» checkbox) — draft cards never render anywhere.

**3. The delivery channel.** Two doors, one validation: the platform's own release pipeline (seeds + spec refresh for platform-shipped wizards) and the vendor console (upload/replace the spec JSON, «Проверить» first — the same grammar guard answers there). Either way the CMS pulls the spec within ~60s (the remote overlay) — a new wizard installs WITHOUT a platform release. The admin card list is the control plane's catalog (joined with the dynapi tenant's card content) — if the card looks stale, check the vendor console first, the binary second.
