Skip to content

Schema Reference ​

An Axiomers app is defined by a schema: a JSON array of collections. Everything you build in the visual editor — fields, views, actions, formulas — is stored as this JSON, and the same JSON can be read, written, or generated directly. This page is the exhaustive reference for that format. For a walkthrough of the visual editor, start with Collections & Fields, Views, Actions & Workflows, and Expressions.

This page is written to be precise enough for an AI assistant to draft a valid schema from a plain description of an app.

Collection ​

A collection is one kind of record (customers, orders, visits…).

json
{
  "name": "customers",
  "label": "Customers",
  "icon": "people",
  "order": 1,
  "schema": [],
  "actions": [],
  "views": [],
  "defaultView": "table"
}
PropertyRequiredMeaning
nameyesUnique machine key, snake_case. Used in formulas and as the table name.
labelyesHuman-readable name.
iconnoA Material icon name.
orderyesSort order in the sidebar. Also the sync order: a collection that others reference should have a lower order than the collections that reference it.
schemayesThe fields — an array of field definitions.
actionsyesButtons — an array of action definitions.
viewsyesWays to look at the records — an array of view definitions.
defaultViewyesThe name of the view that opens first. Must match one of views[].name.
isUserProfilenotrue on the single collection that holds one record per signed-in user.
navnoNavigation options. Where the collection appears: sidebar (laptop menu, default true), menu (phone hamburger menu, default follows sidebar), bottom (phone bottom bar, default false). Order in each: order (sidebar), menuOrder and bottomOrder (each defaults to order). Also onlyIf (a formula; the item is hidden when it is false), label and icon. The bottom bar shows at most 5 items. The Navigation designer writes these keys.
serverManagednoSet by the platform on system collections such as payments (see Payments). People can read these records but only the server can write them. Don't set it yourself, and don't name your own collection payments.

Fields ​

Each entry in a collection's schema array is a field definition:

json
{
  "key": "status",
  "label": "Status",
  "type": "enum",
  "description": "Where the order is in its life cycle",
  "required": true,
  "typeOptions": {
    "values": [
      { "value": "draft", "label": "Draft", "color": "grey" },
      { "value": "confirmed", "label": "Confirmed", "color": "green" }
    ]
  },
  "initialValue": "\"draft\""
}
PropertyMeaning
keyThe field's internal name. Unique within the collection; can't be changed later.
labelWhat people see as the field's name.
typeOne of the field types below.
descriptionHelp text. In a questionnaire it becomes the question text.
requiredCan't be left blank.
readOnlyShown, but not editable on forms.
isLabelMarks the record's short recognizable name (one per collection) — shown in reference pickers.
labelExpressionA formula that overrides label with a computed value.
typeOptionsOptions that depend on type — see below.
computed{ "enabled": true, "expression": "…" } — the value is calculated from a formula instead of typed in.
initialValueA formula evaluated once when the create form opens, to pre-fill the field.
showIfA formula; the field is hidden (and its value cleared) when it is false.
validIfA formula; the field is invalid on submit when it is false.
validMessageThe error shown when validIf fails.

Every record also has built-in fields you can reference in formulas: id, ownerId, updatedAt, isDeleted.

Field types ​

TypeStorestypeOptions
textA stringminLength, maxLength, placeholder, mask, enableScanner (adds a QR/barcode scan button)
longtextA multi-line stringsame as text
richtextSanitized HTML from a formatting editornone
numberA numbermin, max, step, prefix, suffix, decimals
priceA number shown as currencydefaultCurrency, allowedCurrencies, decimals
booleantrue / falsenone
date / datetime / timeA date, a date and time, or a time of daynone
enumOne of a fixed set of values (a string)values
multienumSeveral of a fixed set of values (an array of strings)values
refA link to one record in another collection (its id)collection, displayField, …
multirefLinks to several records in another collection (an array of ids)collection, displayField, …
image / fileAn uploaded fileaccept, maxSizeMb, multiple
drawingA signature or sketch, stored as an imagestrokeColor, strokeWidth, background, maxSizeMb
geolocation{ "lat": number, "lng": number } captured from the device or picked on a map (with place search)accuracy (low/medium/high), timeout
address{ "country", "state", "city" }none
phoneOne string, dial code plus number, e.g. "+2349071805738"defaultDialCode (e.g. "+234") — the country code an empty field starts on. A number already saved keeps its own code, and a code the user picked earlier on their device takes priority.
barcodeA string, rendered as a barcode or QR codeformat (qrcode, code128, ean13, upc), showValueLabel, width, height
tableAn array of row objects you type incolumns: [{ "key", "label" }]
listVirtual, always computed — a list of values from related recordsdisplayField, collection, sort
completelistVirtual, always computed — an embedded table of related recordscollection, columns, sort

values for enum and multienum is an array of { "value", "label", "color"?, "icon"? }.

One choice or several? ​

There is no "allow multiple" flag on enum or ref for new fields — multiple selections are their own types:

  • One choice from a fixed set → enum. Several ("tick all that apply") → multienum.
  • One related record → ref. Several related records → multiref.

multienum takes the same typeOptions as enum, and multiref the same as ref, just without multiple.

ref and multiref options ​

json
{
  "key": "customer",
  "label": "Customer",
  "type": "ref",
  "typeOptions": {
    "collection": "customers",
    "displayField": "full_name",
    "searchField": "phone",
    "barcodeSearch": false,
    "isDistinct": false,
    "suggestedValues": "EQUAL({status},\"active\")",
    "defaultValue": "{_THISUser.id}"
  }
}
OptionMeaning
collectionThe collection to link to. It must exist in the schema.
displayFieldThe field shown as each option's label.
searchFieldThe field matched when the user types in the picker (defaults to displayField).
barcodeSearchAdds a scan button that searches searchField.
suggestedValuesA formula evaluated per candidate record to filter the picker's options.
defaultValueA formula that pre-selects a value when the form opens.
isDistinctHides any option that another record in this collection already uses through this field (for multiref, any item in another record's list), and rejects it on save. The record being edited keeps its own value. It's a check against the records on the device, so two people offline can still pick the same item before they sync.

multiref has a few limits compared with ref:

  • Dot-traversal ({field.prop}) only works through a plain ref, not a multiref. A bare {multirefField} still works and gives the array of ids.
  • The navigate_ref action follows a plain ref only.
  • A multiref can't be the foreignKey in REF_ROWS(...).
  • multienum and multiref fields are left out of CSV import, inbound webhooks, and the "colour by" / "status badge" pickers on calendar and card views (those need exactly one value).

When you need any of these, use ref.

Virtual fields ​

list and completelist hold no data of their own. A completelist must have computed: { "enabled": true, "expression": "…" } — almost always REF_ROWS("otherCollection", "foreignKeyOnThatCollection") — otherwise it renders nothing.

Views ​

A view is one way of looking at a collection's records.

json
{
  "name": "table",
  "type": "table",
  "sections": [
    { "order": 1, "items": [
      { "order": 1, "type": "field", "key": "full_name" },
      { "order": 2, "type": "action", "key": "edit" }
    ] }
  ],
  "sort": { "field": "full_name", "dir": "asc" },
  "pagination": { "enabled": true, "limit": 25 }
}
PropertyMeaning
nameIdentifier. Unique within the collection.
typedetail, card, image-card, table, calendar, chart, dashboard, or questionnaire. There is no list type — use table for any tabular listing.
sectionsOrdered groups of items. An item is { "order", "type": "field" | "action", "key" }; the key must exist in the collection's fields or actions. A section can also have a label and an onlyIf formula.
filterFieldsField keys to expose as filter controls above the view (not for detail).
showIfA formula; the whole view tab is hidden when it is false.
filterExprA formula evaluated per record; records where it is false are hidden.
pagination{ "enabled": boolean, "limit": number }.
sort{ "field": string, "dir": "asc" | "desc" }.

Every collection needs a detail view (conventionally named detail) — it's the only way to open a single record. Avoid making it the defaultView: it can't list records, so people would see nothing on open. Use table, card, calendar, chart, dashboard, or questionnaire.

Layouts ​

View typeExtra propertyShape
card, image-cardcardLayout{ "title": key, "subtitle"?: key, "description"?: key, "status"?: key, "image"?: key } — status must be an enum, image an image field
calendarcalendarLayout{ "startDate": key, "title": key, "startTime"?: key, "endDate"?: key, "endTime"?: key, "color"?: key or color name, "defaultMode"?: "month" | "week" | "day" }
chartchartLayoutsAn array of { "type": "bar" | "line" | "pie" | "doughnut", "labelField": key, "valueField"?: key, "color"?: string, "showLegend"?: boolean }. Omit valueField to count records per label.
dashboarddashboardPanelsAn array of panels, below

Card and image-card views are laid out by cardLayout alone. Their sections aren't used, so leave it as an empty array.

color on a chart applies to line charts only; bar, pie and doughnut charts colour each label automatically. To colour by status, set color on the enum field's own values instead.

Dashboard panels — each has a type (view or stat) and optionally col (the width out of 12: 3, 4, 6 or 12), title, showIf, filter (a formula), sort and actions (keys of collection-scoped actions). A view panel also takes viewName and limit; a stat panel takes field and aggregate (count, sum, avg, min, max).

Questionnaire views ​

A questionnaire view is a multi-step, one-question-at-a-time data-entry wizard — like a survey or intake form. Use it for interviews, assessments, checklists, inspections, or any long form that is easier to fill in step by step.

json
{
  "name": "intake",
  "type": "questionnaire",
  "sections": [
    { "order": 1, "label": "About the patient", "items": [
      { "order": 1, "type": "field", "key": "full_name" },
      { "order": 2, "type": "field", "key": "date_of_birth" }
    ] },
    { "order": 2, "label": "Symptoms", "items": [
      { "order": 1, "type": "field", "key": "symptoms" }
    ] }
  ],
  "listFields": [
    { "order": 1, "type": "field", "key": "full_name" }
  ]
}
  • Each section is one step. The section's label is the step title and its items are the questions asked on that step.
  • The question text is the field's description, falling back to its label when there is no description. Keep label short (it's still used in tables and filters) and put the full question in description, e.g. label Cause of death, description What was the main cause of death?.
  • Validation runs per step (required, validIf) before the user can continue, and again on submit.
  • listFields are the columns shown in the list of already-submitted records that the view opens on. It is separate from sections. If omitted, the first step's fields are used.
  • Every field type works inside a questionnaire, including multienum and multiref.
  • Answers are saved exactly like a normal create/edit form, so a questionnaire can sit alongside table or card views of the same collection.

Actions ​

An action is a button. Each collection should include the three system actions — delete (data/delete), edit (app/navigate_edit) and create (app/navigate_create), all with "isSystem": true — unless the collection is deliberately read-only.

json
{
  "key": "mark_paid",
  "label": "Mark paid",
  "icon": "paid",
  "type": "data",
  "onlyIf": "EQUAL({status},\"confirmed\")",
  "needsConfirmation": true,
  "confirmMessage": "Mark this order as paid?",
  "actionConfig": {
    "type": "set_field",
    "updates": [ { "field": "status", "value": "\"paid\"" } ]
  }
}
PropertyMeaning
key, label, icon, colorIdentity and appearance.
typedata, external, or app.
onlyIfA formula; the button is hidden when it is false.
needsConfirmation, confirmMessageAsk before running.
scoperecord (default — a button per record) or collection (shown at the bottom of dashboard panels).
triggermanual (default, a button click), or afterCreate / afterUpdate / afterDelete to run automatically after that event, with no button.
actionConfigDepends on type — see below.

Data actions ​

actionConfig.typeOptions
set_field, bulk_set_fieldsupdates: [{ "field", "value": formula }]. Bulk also takes expression (a COLLECTIONSELECT).
delete, bulk_deleteBulk takes expression.
create_record, bulk_create_recordcollection and mappings: [{ "targetField", "sourceExpression": formula }]. Bulk also takes expression.
form_actionformTitle, formFields (full field definitions, referenced as {_FORM.key}), and updates.

External actions ​

actionConfig.typeOptions
phone, email, urltarget: a formula resolving to the number, address, or URL.
http_requesttarget (a fixed URL string, never a formula), method (GET, POST, PUT, PATCH, DELETE), headers and queryParams ([{ "key", "value", "secret" }]), body ([{ "field", "value" }]), responseMapping ([{ "field", "path" }], where path is a dot-path into the { ok, status, body } response), and optionally responseCollection to create a new record instead of updating the current one.
paymentamountField (the key of a stored number or price field on this collection, in naira; a computed field can't be used), optional description, and target set to "". The amount is read from the saved record on the server. See Payments & Earnings.

For a secret header or query parameter (an API key, a token), set "secret": true and leave value as "". The real value is entered later in the admin secrets screen and never appears in the schema.

App actions ​

actionConfig.typeOptions
navigate_detail, navigate_edit, navigate_create, navigate_listOptional collection.
navigate_refrefField — follows a plain ref field on this collection to its target record.
navigate_by_idcollection and targetIdExpression — a formula resolving to the target record's id. Use for reverse relations navigate_ref can't reach, e.g. FIRST(SELECT("stock_items","id",EQUAL({product.id},{_THISRECORD.id}))).
run_actionsactions: an array of action keys, run in sequence.
export_pdftemplate (HTML with <<{[field]}>> tokens) and filename (a formula).
open_createcollection, prefill: [{ "key", "expression" }], and optionally afterSubmitNavigate (back, list, detail) with afterSubmitCollection.

Expressions ​

Formulas appear in computed.expression, initialValue, showIf, validIf, onlyIf, filterExpr, action values, and suggestedValues / defaultValue. They are spreadsheet-style function calls with {field} references.

There are no infix operators. +, -, *, /, >=, <=, ==, &&, || all fail to parse — everything is a function: ADD(a,b), SUBTRACT(a,b), MULTIPLY(a,b), DIVIDE(a,b), GTE(a,b), LTE(a,b), EQUAL(a,b), AND(a,b), OR(a,b). A minus sign is only ever a negative number.

References ​

ReferenceMeaning
{field}A field on the current record.
{ref.prop}A field on the record a ref points at. Chains work: {shift.location.name}.
{_THISRECORD.field}Inside a query condition: the outer record, rather than the row being tested.
{_THISUser.field}The signed-in user.
{_FORM.field}A value entered in a form_action dialog.
{_PREVROW.field}, {_NEXTROW.field}, {_INDEX}, {_TOTAL}Position within a list pass.

Quoting ​

Inside a formula string, write string literals with ordinary double quotes; in JSON that is one level of escaping: "expression": "DATEDIFF({start},{end},\"days\")". Don't escape twice — the formula language has no escape syntax of its own.

The condition argument of FILTER, COLLECTIONSELECT, EXISTS, SELECT and ORDERBY (and the extraCondition of REF_ROWS) is best written as a plain inline expression: EXISTS("cartitem", EQUAL({product},{_THISRECORD.product})). The older form, where the condition is passed as a quoted string, still works but can't contain its own string literals. Collection names, returnColumn, lookupColumn, sortField and foreignKey are always string literals, never field references. Leave the condition out entirely when there isn't one.

Function library ​

Where a parameter lists fixed values (like unit), they're literals — "day" instead of "days" silently does nothing.

Collections ​

FunctionDescription
EXISTS(collection, condition?)True if any record matches.
FILTER(collection, condition)Matching records.
COLLECTIONSELECT(collection, condition)Matching records, including their computed fields.
REF_ROWS(collection, foreignKey, extraCondition?)Every record in collection whose foreignKey ref points back at this record. Shorthand for COLLECTIONSELECT(collection, EQUAL({foreignKey},{_THISRECORD.id})).
SELECT(collection, returnColumn, condition?)An array of one column's values.
ORDERBY(collection, returnColumn, condition?, sortField?)Like SELECT, sorted ascending.
ORDERBYDESCENDING(collection, returnColumn, condition?, sortField?)Like SELECT, sorted descending.
LOOKUP(value, collection, lookupColumn, returnColumn)A single value looked up across a collection.
IN(value, list)True if the value is in the array.
COUNT(array)Number of items.
FIRST(array), LAST(array)First or last element.
SUMARRAY(array)Sum of the numbers in an array.
HERE()The device's current location (client-side only — use in initialValue).
USERPROFILE(field)A field from the signed-in user's profile record.

Logic ​

FunctionDescription
IF(condition, then, else)Conditional.
IFS(cond1, val1, cond2, val2, …, default)The first true condition's value wins; a trailing unpaired argument is the default.
AND(…), OR(…), NOT(a)Boolean logic.
EQUAL(a, b)Equality.
GT(a, b), LT(a, b), GTE(a, b), LTE(a, b)Number comparison. Not for dates — use ISBEFORE / ISAFTER / ISBETWEEN.
ISBLANK(value)True for null, undefined, or an empty string.
ROLE(name)Validates a role name against the app's configured roles, returning it if valid and null if not — catches typos. Compare with it: EQUAL(USERPROFILE("role"), ROLE("admin")).

Text ​

FunctionDescription
CONCATENATE(…)Joins values (or a single array) into one string.
JOIN(separator, …)Joins values (or a single array) with a separator.
UPPER(v), LOWER(v), TRIM(v)Case and whitespace.
LEN(v)Length of a string or array.
LEFT(v, n), RIGHT(v, n)First or last N characters.
CONTAINS(v, search)True if the string contains the substring.
REPLACE(v, search, replacement)Replaces every occurrence.

Math ​

FunctionDescription
ADD(a, b, …), SUBTRACT(a, b), MULTIPLY(a, b, …), DIVIDE(a, b)Arithmetic. DIVIDE returns null on division by zero.
MOD(n, divisor), POWER(base, exp), SQRT(v), ABS(v)Remainder, power, square root, absolute value.
ROUND(v, decimals)Rounds to N decimal places.
MIN(…), MAX(…), AVERAGE(…)Pass an array (from SELECT, say) or separate values.
DISTANCEBETWEEN(a, b, unit?)Great-circle distance between two geolocation values. unit: meters (default) or km.

Dates and times ​

For date and datetime comparisons always use ISBEFORE, ISAFTER and ISBETWEEN.

FunctionDescription
NOW(), TODAY(), YESTERDAY(), TOMORROW(), TIMENOW()The current moment, or today/yesterday/tomorrow at midnight, or the current time.
DATE(year, month, day, time?)Builds a date. month is 1-based; time is "HH:mm" or "HH:mm:ss".
TIME(value, date?)Resolves an HH:mm(:ss) string against today or a given date.
DATEADD(date, n, unit)Adds a duration. unit: seconds, minutes, hours, days, weeks, months, years.
DATEDIFF(a, b, unit)Difference between two dates, in the same units.
DATEFORMAT(date, format)format: long, short, month, iso.
DAYOFWEEK(day)The date of a weekday this week (sunday … saturday, lowercase).
YEAR(d), MONTH(d), DAY(d), HOUR(d), MINUTE(d), WEEKDAY(d)Date parts. WEEKDAY is 0 (Sunday) to 6.
STARTOFDAY(d?), ENDOFDAY(d?), STARTOFYEAR(d?), ENDOFYEAR(d?), STARTOFMONTH(), ENDOFMONTH()Period boundaries (default: now).
ISBEFORE(a, b), ISAFTER(a, b), ISBETWEEN(d, start, end)Date comparisons (ISBETWEEN is inclusive).

Generating values ​

These re-roll every time they are evaluated, so use them in initialValue (evaluated once when the create form opens) — never in a computed expression, where the value would keep changing.

FunctionDescription
RAND(min?, max?)A random number from 0 up to (not including) 1 — or, with both arguments, a random whole number between them, inclusive. RAND(1000, 9999) is a random 4-digit number.
UNIQUEID()A fresh globally unique id (UUID v4).
UNIQUECHAR(type, length)A random string. type: letters, numeric, alphanumeric, special, or all (letters, numbers and special characters together). length: a whole number from 1 to 64. Returns blank for an unknown type or bad length. Random, not guaranteed unique — 4 alphanumeric characters give about 1.7 million combinations, so collisions become likely after a few thousand records. Use UNIQUEID() when a value must never repeat.
json
{ "key": "booking_code", "label": "Booking code", "type": "text",
  "readOnly": true, "initialValue": "UNIQUECHAR(\"alphanumeric\", 6)" }

Rules of thumb ​

  • Every ref / multiref typeOptions.collection must name a collection that exists.
  • Every field item in a view must exist in the collection's schema, and every action item in its actions.
  • defaultView must match a view name, and should almost never be a detail view.
  • Reverse relationships come in pairs: a completelist on one collection (with a REF_ROWS formula) and a ref on the other — add both halves together.
  • Keep formulas to the functions listed above; don't invent new ones.
  • Prefer a simple schema that works over an exhaustive one.

A complete example ​

A small visits log: a patients collection, and a visits collection with a reference, a multi-select, and a questionnaire view.

json
[
  {
    "name": "patients",
    "label": "Patients",
    "icon": "person",
    "order": 1,
    "schema": [
      { "key": "full_name", "label": "Full name", "type": "text", "required": true, "isLabel": true },
      { "key": "phone", "label": "Phone", "type": "phone" },
      { "key": "visits", "label": "Visits", "type": "completelist",
        "typeOptions": { "collection": "visits", "columns": ["visit_date", "conditions"] },
        "computed": { "enabled": true, "expression": "REF_ROWS(\"visits\", \"patient\")" } }
    ],
    "actions": [
      { "key": "delete", "label": "Delete", "icon": "delete", "type": "data", "isSystem": true,
        "needsConfirmation": true, "actionConfig": { "type": "delete" } },
      { "key": "edit", "label": "Edit", "icon": "edit", "type": "app", "isSystem": true,
        "actionConfig": { "type": "navigate_edit" } },
      { "key": "create", "label": "New", "icon": "add", "type": "app", "isSystem": true,
        "actionConfig": { "type": "navigate_create" } }
    ],
    "views": [
      { "name": "table", "type": "table",
        "sections": [ { "order": 1, "items": [
          { "order": 1, "type": "field", "key": "full_name" },
          { "order": 2, "type": "field", "key": "phone" }
        ] } ] },
      { "name": "detail", "type": "detail",
        "sections": [ { "order": 1, "label": "Patient", "items": [
          { "order": 1, "type": "field", "key": "full_name" },
          { "order": 2, "type": "field", "key": "phone" },
          { "order": 3, "type": "field", "key": "visits" },
          { "order": 4, "type": "action", "key": "edit" },
          { "order": 5, "type": "action", "key": "delete" }
        ] } ] }
    ],
    "defaultView": "table"
  },
  {
    "name": "visits",
    "label": "Visits",
    "icon": "medical_services",
    "order": 2,
    "schema": [
      { "key": "patient", "label": "Patient", "type": "ref", "required": true,
        "description": "Which patient was seen?",
        "typeOptions": { "collection": "patients", "displayField": "full_name" } },
      { "key": "visit_date", "label": "Visit date", "type": "date", "required": true,
        "description": "When did the visit take place?", "initialValue": "TODAY()" },
      { "key": "conditions", "label": "Conditions", "type": "multienum",
        "description": "Tick every condition that applies",
        "typeOptions": { "values": [
          { "value": "fever", "label": "Fever" },
          { "value": "cough", "label": "Cough" },
          { "value": "headache", "label": "Headache" }
        ] } },
      { "key": "reference", "label": "Reference", "type": "text", "readOnly": true,
        "initialValue": "UNIQUECHAR(\"alphanumeric\", 6)" }
    ],
    "actions": [
      { "key": "delete", "label": "Delete", "icon": "delete", "type": "data", "isSystem": true,
        "needsConfirmation": true, "actionConfig": { "type": "delete" } },
      { "key": "edit", "label": "Edit", "icon": "edit", "type": "app", "isSystem": true,
        "actionConfig": { "type": "navigate_edit" } },
      { "key": "create", "label": "New", "icon": "add", "type": "app", "isSystem": true,
        "actionConfig": { "type": "navigate_create" } }
    ],
    "views": [
      { "name": "intake", "type": "questionnaire",
        "sections": [
          { "order": 1, "label": "Who and when", "items": [
            { "order": 1, "type": "field", "key": "patient" },
            { "order": 2, "type": "field", "key": "visit_date" }
          ] },
          { "order": 2, "label": "Symptoms", "items": [
            { "order": 1, "type": "field", "key": "conditions" }
          ] }
        ],
        "listFields": [
          { "order": 1, "type": "field", "key": "patient" },
          { "order": 2, "type": "field", "key": "visit_date" }
        ] },
      { "name": "detail", "type": "detail",
        "sections": [ { "order": 1, "label": "Visit", "items": [
          { "order": 1, "type": "field", "key": "patient" },
          { "order": 2, "type": "field", "key": "visit_date" },
          { "order": 3, "type": "field", "key": "conditions" },
          { "order": 4, "type": "field", "key": "reference" },
          { "order": 5, "type": "action", "key": "edit" },
          { "order": 6, "type": "action", "key": "delete" }
        ] } ] }
    ],
    "defaultView": "intake"
  }
]

Released under no license yet — internal product, pre-launch.