Skip to content

Authoring a wizard

A wizard is one *.wizard.xml file: a <Wizard> root, a flat list of <Node> elements, and a flat list of <Link> edges connecting them. Every example below is drawn from the canonical sample, NewProductRequest.wizard.xml.

The XML is a lossless serialisation of the visual designer model — you can hand-author it, paste it into the Wizard Designer, or round-trip between the two.

<Wizard Name="NewProductRequest" Label="New Product Request" Slug="new-product-request" Schema="Inventory">
...
</Wizard>
Attribute Required Meaning
Name yes Unique PascalCase identifier. The immutable lookup key; resolvable in the API by name.
Label yes Human-readable display title.
Slug no URL slug for the runner route /form/{slug}. Omitted → derived (kebab-cased) from Name.
Schema no Database schema context for the wizard’s SQL.

Every node shares the same envelope; the Type selects which children and extra attributes apply.

<Node Id="details" Type="Input" Label="Product Details" X="220" Y="-12" Width="200" Height="160">
...
</Node>
Attribute Meaning
Id Node identifier, unique within the wizard; referenced by <Link>.
Type One of Startup, Input, Decision, Action, Terminal.
Label Display label (step heading in the runner, node caption in the designer).
X, Y Canvas position (designer layout only).
Width, Height Optional canvas size.

Initialises global variables then auto-advances. Globals are visible to every later step’s SQL and Decision expressions.

<Node Id="start" Type="Startup" Label="Start" X="-60" Y="38" Width="160" Height="60">
<Variables>
<Variable Name="Channel" Type="String" Default="Wizard" />
</Variables>
</Node>

<Variable> attributes: Name, Type (String | Int | Decimal | Bool | Date | DateTime), and optional Default.

Collects user data. Holds a <Fields> block, an optional <Layout>, and an optional <SubmitSql>.

<Node Id="details" Type="Input" Label="Product Details" X="220" Y="-12" Width="200" Height="160">
<Fields>
<Field Name="ProductName" Type="String" Label="Product Name" Required="true" />
<Field Name="CategoryId" Type="Select" Label="Category" Required="true" ControlType="Select2">
<OptionsSql>
SELECT Id AS Value, Name AS Label
FROM Inventory.Categories
WHERE IsDeleted = 0 AND IsActive = 1
ORDER BY Name
</OptionsSql>
</Field>
<Field Name="UnitPrice" Type="Number" Label="Unit Price" Required="true" />
<Field Name="ReorderLevel" Type="Number" Label="Reorder Level" Default="10" />
<Field Name="Justification" Type="Text" Label="Justification" />
</Fields>
</Node>
Attribute Meaning
Name Variable name. Must be PascalCase and unique across all Input nodes in the wizard.
Type String | Text | Number | Date | DateTime | Boolean | Select | Attachment. Text renders multi-line.
Label Field label.
Required true to require a value before advancing.
Indexed true to store this field as a real, indexable column on the response table — see Storage and indexes. Not available for Text or Attachment.
Disabled / Readonly Render the control disabled / read-only.
Default Seeded value if the field is empty when the step is first shown.
Mask Input mask.
ControlType For Select: Select2 (searchable) or ModalSelector.
MaxSize, MaxFiles, Accept For Attachment: byte cap, file count, accepted MIME/extensions.

Field children:

  • <Options> — static select options: <Option Value="…" Label="…" />.
  • <OptionsSql> — dynamic select options; the query must return Value and Label columns.
  • <SuggestionsSql> — autocomplete suggestions for a Text field (@SearchText is injected).
  • <ExtraMappings> — copy extra columns from the picked dataset row into other variables: <Map Column="Email" Variable="ContactEmail" /> (e.g. autofill from a chosen record).

An Input step may arrange its fields with the same layout grammar as views — <Section> / <Grid> / <Row> / <Column Span> / <Item Name Width> / <Line>. See Form layout. Layout is pure presentation: it is stored in the XML and consumed by the React runner; fields not placed in the layout fall through in declaration order.

Branches on the current variables. A Decision has no children — it routes purely through its outgoing <Link Expression="…"> edges (see Links).

<Node Id="creditCheck" Type="Decision" Label="Over 50k?" X="480" Y="30" />

A side effect that runs during the auto-advance burst. The ActionType attribute selects the flavour:

Alert — pause and show a message the user must acknowledge before the flow resumes:

<Node Id="notify" Type="Action" Label="Heads up" ActionType="Alert">
<Subject>Note</Subject>
<Message>This request will need manager approval.</Message>
</Node>

ExecuteSQL — run a SQL statement for its side effect:

<Node Id="touch" Type="Action" Label="Log" ActionType="ExecuteSQL">
<SQL>UPDATE Inventory.Requests SET TouchedAt = SYSDATETIMEOFFSET() WHERE Id = @RequestId;</SQL>
</Node>

SetVariable — compute a variable from a static value or a SQL scalar. Use the SQL form with a trailing SELECT <scalar> to capture something (e.g. a new row’s SCOPE_IDENTITY()) into a variable for later steps:

<Node Id="register" Type="Action" Label="Create Draft" X="480" Y="30" ActionType="SetVariable">
<TargetVariable>ProductId</TargetVariable>
<ValueSource>SQL</ValueSource>
<SQL>
INSERT INTO Inventory.Products
(Sku, Name, CategoryId, UnitPrice, UnitCost, ReorderLevel, IsActive, Status,
IsDeleted, CompanyId, CreatedById, CreatedAt)
VALUES
(@SkuFormat, @ProductName, @CategoryId, @UnitPrice, @UnitCost, @ReorderLevel, 0, 'Draft',
0, @SessionCompanyId, @SessionUserId, SYSDATETIMEOFFSET());
SELECT CAST(SCOPE_IDENTITY() AS BIGINT);
</SQL>
</Node>

<ValueSource> is Static (use <StaticValue>) or SQL (use <SQL>, executed as a scalar).

Finishes the wizard and, optionally, hands off to a workflow.

<Node Id="done" Type="Terminal" Label="Submitted" X="740" Y="38" Width="160" Height="60"
SubmissionStatus="Pending" WorkflowName="ProductApproval" ResubmissionTransition="Resubmitted">
<SuccessMessage>Your product request has been submitted for approval.</SuccessMessage>
</Node>
Attribute / child Meaning
SubmissionStatus Status stamped on the saved response (default Submitted).
WorkflowName Workflow definition to start on submit, bound to the response. Omit for no handoff.
ResubmissionTransition Transition label fired when an edited response continues its existing workflow instance (default Resubmitted).
<TerminalSql> SQL to run at submission time (before the workflow starts).
<SuccessMessage> Message shown on the completion screen.

Directed edges between nodes. The runner follows them from the start node onward.

<Link From="start" To="details" />
<Link From="details" To="register" />
<Link From="register" To="done" />
Attribute Meaning
From, To Source and target node Id.
Expression On a Decision node, the branch condition. First true link wins; Expression="else" (or an expression-less link) is the catch-all.
FromSide, ToSide Which node port the edge attaches to (designer routing only).

SQL blocks (<OptionsSql>, <SuggestionsSql>, SetVariable/ExecuteSQL <SQL>, <SubmitSql>, <TerminalSql>) are string-substituted before execution — this is not a parameterised command:

  • @FieldName and @NodeId__FieldName are replaced with the collected value; strings are auto-quoted, booleans become 1/0, and NULL is emitted for empty values.
  • Session parameters are injected: @SessionUserId, @SessionCompanyId, @SessionCartId.
  • Dataset/options queries also receive @SearchText for optional filtering.
  • Any declared field or Startup variable the user didn’t supply is substituted as SQL NULL, so referencing an optional blank field never raises “Must declare the scalar variable @X”.
  • T-SQL locals you DECLARE inside the SQL body are not wizard variables and are left untouched.

Each wizard’s submissions go into its own table, Wizard.Resp{Name}. Every response keeps its full set of values as JSON, and any field you mark Indexed additionally gets a real column that can be filtered, sorted and searched.

<Field Name="SKU" Type="String" Label="SKU" Indexed="true" />

Marking a field Indexed does three things: it becomes an indexable column, it becomes searchable, and it appears in the generated response grid. Fields you leave alone stay in the JSON payload — still submitted, still readable, just not queryable. Text and Attachment fields cannot be indexed at all, because their values are unbounded in length.

Declare database indexes in an <Indexes> block at the <Wizard> root, using the same grammar as entity indexes:

<Wizard Name="NewProductRequest" Label="New Product Request">
<Indexes>
<!-- Enforce one product request per SKU -->
<Index Name="Sku" Unique="true">
<FieldRef Name="SKU" />
</Index>
<!-- Composite index for a category-filtered, newest-first grid -->
<Index Name="CategoryRecent">
<FieldRef Name="CategoryId" />
<FieldRef Name="CreatedAt" Sort="Desc" />
</Index>
<!-- Partial index: only the approved rows -->
<Index Name="ApprovedSku" Filter="Status = 'Approved'">
<FieldRef Name="SKU" />
</Index>
</Indexes>
<Node Id="details" Type="Input" ...>
Attribute Meaning
Name Author-facing name. The physical name is prefixed with the table (IX_Resp{Name}_{Name}), because PostgreSQL scopes index names to the schema rather than the table.
Unique true to enforce uniqueness.
Filter Optional predicate for a partial index. Must be a single boolean expression.
<FieldRef Name Sort> One key column, in order. Sort="Desc" for descending.

A <FieldRef> may name a wizard field or a built-in response column — Id, Status, ReferenceId, CreatedAt, CreatedById, UpdatedAt, CompanyId, IsDeleted, UpdatedTo, VersionHash, SchemaHash. Referencing a built-in is what makes a company-scoped, recency-ordered index possible. A field referenced by an index is treated as Indexed whether or not you set the attribute — you cannot index a column that was never created.

Indexes are declared at the root because a wizard can have several Terminal nodes but has exactly one response table; per-Terminal declarations could disagree with no principled way to merge them. The designer surfaces the editor in the Terminal pane, under Storage & Indexes, since that is where the whole flow’s fields are already in view — but every Terminal edits the same root-level list.

Genie also creates a few indexes on every response table regardless: latest-in-lineage lookups, status filtering, and company-scoped recency ordering.

Once provisioned, a wizard’s submissions are browsable at /table/Resp{Name} — a normal Genie table view, generated for you, with the full RBAC and row-security stack. It shows the indexed fields plus the response metadata. The object is registered as a permission resource automatically, but no role is granted it: grant Resp{Name} to the roles that should see the submissions.

See Runtime & designer for how the table is provisioned, and how SchemaHash keeps a cosmetic edit from triggering DDL.