Skip to content

Runtime & designer

This page covers what happens when a wizard runs: how the definition is stored, how a step’s SQL is resolved and executed, the HTTP surface the runner calls, and the visual designer that authors it.

A wizard definition is a WizardForm row in the Wizard.Forms table — Name, Slug, Label, Schema, the full FlowXml, and a VersionHash. Submitted runs are WizardFormResponse rows in Wizard.FormResponses, storing the collected values as JSON in ResponseData.

Wizards are not part of the model-migration pipeline. The build does not copy *.wizard.xml into bin/models; instead you import the XML through the designer (or POST /save) and it is persisted to the database. Keep the XML in source control as the reference copy.

Each save that changes the FlowXml mints a fresh 6-char VersionHash and snapshots the XML into the shared Workflow.FlowVersions history (via IFlowVersionService). Every submitted response is stamped with the version hash that produced it, so you can tell which definition a response came from. The designer’s History dropdown lists versions and can load an old one read-only.

On save — and again for every wizard at startup via the WizardViewInitializerService background service — Genie generates a SQL view [Wizard].[vw_{Name}Responses]. It flattens each response’s JSON into one column per Input field using JSON_VALUE(fr.ResponseData, '$.FieldName'), joined to the form and the creating/updating users. Attachment fields are projected as NVARCHAR(MAX). This lets you build an ordinary table view over submitted responses.

The runner drives the node graph and calls back to the server whenever a step needs data or SQL. WizardExecutionService is the backend engine; it locates the relevant block in the FlowXml, substitutes variables, and runs the SQL.

Field values arrive keyed as NodeId__FieldName. Before executing any SQL, the service (PrepareVariablesForSql) builds the substitution scope:

  1. copies the supplied variables;
  2. adds a bare FieldName alias for each NodeId__FieldName (so @FieldName works);
  3. seeds every declared field/Startup variable that wasn’t supplied as DBNull (→ SQL NULL);
  4. injects session parameters (@SessionUserId, @SessionCompanyId, @SessionCartId).

Substitution replaces each @Token with a formatted literal (strings single-quote-escaped and quoted, booleans as 1/0, null/DBNull as NULL), longest key first so @SessionCompanyId resolves before @Session, matched on word boundaries so @Email won’t match inside @EmailAddress. See the substitution rules in Authoring a wizard.

POST /{id}/submit (WizardExecutionService.SubmitAsync) does the following, in order:

  1. moves any Attachment temp uploads to permanent storage;
  2. transforms NodeId__FieldName variables to bare FieldName and writes a WizardFormResponse (JSON ResponseData, Status from the Terminal’s SubmissionStatus, the current VersionHash);
  3. runs <TerminalSql> if present;
  4. workflow side — mutually exclusive:
    • new run → if the Terminal declares a WorkflowName, starts that workflow via IWorkflowEngine.StartAsync(workflowName, "WizardFormResponse", responseId, formData);
    • continue → if the submission carries a FlowId (edit/resubmit), fires the resubmission transition on the existing instance via TransitionAsync and repoints it at the new response.

Loading a prior response for edit is gated: only the latest response in a lineage (UpdatedTo == null) may be edited; superseded ones return 404. On resubmit, the prior response’s UpdatedTo is pointed forward to the new row, forming an immutable edit chain. A FlowId requires an UpdatedForResponseId, must reference an active instance bound to that WizardFormResponse, or the submit is rejected.

All endpoints are under /api/v1/genie/wizard and require authentication ([Authorize]).

Method + route Purpose
GET /{key} Load a wizard by id, Name, or Slug (the runner’s entry call).
GET /{id}/response/{responseId} Load a prior response for edit mode (latest-in-lineage only).
POST /save Create or update a wizard from { Id?, Name, Slug?, Label, Schema?, FlowXml, Notes? } (upserts by Name).
GET /{id}/versions List saved versions, newest first (History picker).
GET /{id}/versions/{versionHash} Fetch one historical version’s XML (read-only load).
DELETE /{id} Soft-delete a wizard.
POST /{id}/options/{fieldName} Resolve a Select/Radio field’s OptionsSql (body = current variables).
POST /{id}/field-dataset/{fieldName} Searchable dataset for a ModalSelector/Select2 field ({ SearchText?, Variables? }).
POST /{id}/execute/{nodeId} Run an ExecuteSQL action node’s SQL (auto-advance).
POST /{id}/set-variable/{nodeId} Run a SetVariable SQL action and return the scalar.
POST /{id}/submit Submit the wizard: save the response, run terminal SQL, start/continue the workflow.

Responses use the standard SuccessDataResult<T> envelope. Field names are unique across the wizard, so the options/dataset/execute endpoints address a field or node without ambiguity.

GenieWizardRunner (driven by the useWizardRuntime hook) executes a wizard for an end user at /wizard/{slug}. It:

  • loads the definition via GET /{key}, parses the FlowXml, and finds the start node;
  • walks the graph in an auto-advance driver — Startup, Decision and non-alert Action nodes run silently (calling execute/set-variable as needed); the loop stops on an Input step, an Alert, or a Terminal;
  • renders each Input step with the shared form field controls and layout (GenieField + the form layout engine), pointed at the wizard’s options/dataset endpoints, with a progress bar and Required-field validation;
  • shows a read-only summary of every visited step for review, then submits;
  • supports edit mode via responseId (supersede a prior response) and flowId (continue an existing workflow instance), re-walking only the branches the original submission took.

GenieWizardDesigner (route /wizard-designer) is the visual node-graph editor. It provides a drag-and-drop canvas with the five node types (Startup, Input, Decision, Action, Terminal), port-to-port link drawing, per-node property panels (fields, variables, action config, terminal handoff), and a raw-XML code editor — the model round-trips losslessly between the canvas and the <Wizard> XML. Saving posts to POST /save; the History dropdown surfaces the version list. Saved wizards are listed by the registered table view at /table/wizard-definitions.