Skip to content

Building a record type's field schema

An internal-form record type collects structured data — a cleaning log, a visitor register, a calibration entry. What fields it collects is described by a small piece of JSON called the field schema. This page is the complete guide to writing one, with a worked example for every field type.

You don't need to be a programmer. A field schema is a form described as a list of fields, and the editor on the record-type page checks it for you as you type — press Test schema (or just keep typing) to see the exact form your schema produces, side by side.

Let the AI assistant write it for you

You can describe the form in plain English to the QStack AI assistant and have it build the record type for you — see Generate a record type from a description at the end. This page still matters: it's how you read, check and tweak what the assistant produced.

The skeleton

Every field schema has the same outer shape:

{
  "type": "object",
  "properties": {
  },
  "required": []
}
  • "type": "object" — always this. A record is one filled-in form.
  • "properties" — the fields, one entry each. This is where your work goes.
  • "required" — the names of the fields a user must fill before submitting. Optional; leave it as [] if nothing is mandatory.

A field goes inside properties as "field_name": { …how it behaves… }:

{
  "type": "object",
  "properties": {
    "equipment_id": { "type": "string", "title": "Equipment ID" }
  },
  "required": ["equipment_id"]
}

The field name (equipment_id) is the internal key — lowercase, no spaces, use underscores. The title is the human label shown on the form. Always give a title; without one QStack falls back to a tidied-up version of the name.

The field types

QStack deliberately supports a small, dependable set of field types. Here is every one, with the JSON that produces it.

You want Use
A short text box {"type": "string"}
A long text area {"type": "string", "format": "textarea"}
A date picker {"type": "string", "format": "date"}
A dropdown (pick one) {"type": "string", "enum": ["A", "B", "C"]}
A whole number {"type": "integer"}
A decimal number {"type": "number"}
A yes/no checkbox {"type": "boolean"}
A value the system fills in {"type": "string", "x-server": "today"} (see below)

Text field

"cleaning_agent": { "type": "string", "title": "Cleaning agent" }

Cap the length with maxLength:

"equipment_id": { "type": "string", "title": "Equipment ID", "maxLength": 50 }

Long text (textarea)

"comments": { "type": "string", "format": "textarea", "title": "Comments" }

Date

Renders as a native date picker (<input type="date">):

"cleaned_on": { "type": "string", "format": "date", "title": "Cleaning date" }

List the allowed choices in enum. The stored value is exactly the text you list:

"shift": {
  "type": "string",
  "title": "Shift",
  "enum": ["Day", "Evening", "Night"]
}

Whole number and decimal

"page_number": { "type": "integer", "title": "Page number" }
"ph_reading":  { "type": "number",  "title": "pH reading" }

Bound them with minimum / maximum:

"page_number": {
  "type": "integer", "title": "Page number",
  "minimum": 1, "maximum": 999
}

Yes/no checkbox

"within_spec": { "type": "boolean", "title": "Within specification" }

A boolean listed in required must be ticked to submit — use that for an attestation checkbox. An optional boolean is just a normal checkbox.

Labels and help text

  • title is the field's label.
  • description adds a line of help text under the field.
"cleaning_agent": {
  "type": "string",
  "title": "Cleaning agent",
  "description": "The detergent or solvent used, e.g. 70% IPA."
}

Making fields mandatory

List the field names (not their titles) in required:

{
  "type": "object",
  "properties": {
    "equipment_id":   { "type": "string", "title": "Equipment ID" },
    "cleaned_on":     { "type": "string", "format": "date", "title": "Cleaning date" },
    "cleaning_agent": { "type": "string", "title": "Cleaning agent" }
  },
  "required": ["equipment_id", "cleaned_on"]
}

Here the agent is optional; the equipment and date are not.

Field order

By default fields appear in the order you wrote them. To fix a specific order (handy after editing), set the Field order box — a JSON list of the field names:

["equipment_id", "cleaned_on", "cleaning_agent", "comments"]

Any field you leave out of the list still appears, after the ones you named.

Fields the system fills in

Some values shouldn't be typed by the user — the date the record was made, or who made it. Mark those with x-server. They render disabled, and QStack fills them at submission time, so they can't be faked:

x-server value QStack fills in
"today" The date the record is created
"person" The originator's name
"performed_by": { "type": "string", "title": "Performed by", "x-server": "person" }
"logged_on":    { "type": "string", "title": "Logged on",    "x-server": "today" }

Never put an x-server field in required — the user can't fill it, so QStack supplies it for you regardless.

A complete worked example

An Equipment Cleaning Log, end to end:

{
  "type": "object",
  "properties": {
    "equipment_id":   { "type": "string", "title": "Equipment ID", "maxLength": 50 },
    "cleaned_on":     { "type": "string", "format": "date", "title": "Cleaning date" },
    "cleaning_agent": { "type": "string", "title": "Cleaning agent" },
    "within_spec":    { "type": "boolean", "title": "Visually clean and dry" },
    "performed_by":   { "type": "string", "title": "Performed by", "x-server": "person" },
    "comments":       { "type": "string", "format": "textarea", "title": "Comments" }
  },
  "required": ["equipment_id", "cleaned_on", "cleaning_agent"]
}

Field order:

["equipment_id", "cleaned_on", "cleaning_agent", "within_spec", "performed_by", "comments"]

Paste that into the editor, press Test schema, and you'll see the cleaning log render on the right.

Testing your schema

The editor validates continuously. When something is off you'll get a plain message pointing at the problem — a missing comma, an unknown field type, a misplaced bracket. Common ones:

  • "not valid JSON" — usually a missing comma between fields, or a trailing comma after the last one. JSON does not allow a comma before a closing }.
  • "Unsupported field type" — you used a type outside the supported set above (for example "array"). Pick one of the supported types.
  • "Add at least one field"properties is empty. A record type needs at least one field before it can be published.

Nothing is saved until you press Save, and no version exists until you Publish — so experiment freely.

What a field schema can't do

Keeping the set small keeps records dependable and searchable. Not supported:

  • Repeating rows / tables (a variable-length list of line items). For now, either add a fixed set of fields, or capture the tabular evidence as an external upload.
  • Nested sections (an object inside a field). Keep fields flat.
  • Conditional fields (show B only if A is ticked). Every field always shows.
  • References between schemas ($ref, allOf). Write each type out in full.

If you need one of these, capture the evidence as an external-document record type instead, or talk to your QStack administrator.

Versioning — why editing is safe

Editing a field schema only touches the draft. Records already in flight keep the exact schema version they were created under and render against it forever. Your edit becomes a new immutable version only when you Publish (or when the governing Form document becomes Effective). So you can correct a schema without disturbing a single existing record. See Quality Records for the full versioning rules.

Generate a record type from a description

If you use the QStack AI assistant (or another MCP client — see API & AI-tool access), you can describe the form in words:

"Create an internal record type called Fridge Temperature Log, code FRIDGE, with fields: fridge ID, date (auto), temperature in °C, within-range yes/no, and a comments box. Temperature and fridge ID are required."

The assistant turns that into a field schema using exactly the types on this page, checks it, and creates the record type (via the create_record_type tool). You then open it here to review, tweak the schema, and publish — the review-and-publish step stays with a human.