Columns and links
A column definition should describe the value’s meaning, not only how it happens to look in one form. Choosing the right type gives every client the same rules for editing, validation, display, and storage.
Choosing a type
Section titled “Choosing a type”| Need | Suitable type |
|---|---|
| Free-form text | TEXT |
| Yes/no state | BOOLEAN |
| Whole number | INT or BIGINT |
| Exact fractional number | DECIMAL(p,s) |
| Currency-bearing amount | MONEY |
| Calendar date | DATE |
| Time of day | TIME |
| Real instant viewed across time zones | INSTANT |
| Timezone-free civil date and time | RAW_DATETIME |
| Elapsed amount or calendar interval | DURATION or PERIOD |
| Another row in the same profile | LINK(table) |
| Structured contact or identifier value | PHONE, EMAIL_ADDRESS, IBAN, or CREDIT_CARD |
| Trade item identifier | GTIN_8, GTIN_12, GTIN_13, or GTIN_14 |
Use the type catalog supplied by the server when building a table editor. It identifies types that users may declare, types that take arguments, types that create generated companions, and types restricted to initial table creation.
Numbers
Section titled “Numbers”Use INT for ordinary whole numbers. Use BIGINT only when the larger range is genuinely needed; some consumers, including Steel, intentionally restrict how BIGINT values can be used.
Use DECIMAL(p,s) for exact fractional values:
DECIMAL(12,3)p is total precision and s is the number of fractional digits. The scale cannot exceed the precision. This is generally a better fit than an unconstrained numeric value when the domain has a known number of decimal places.
A money column always declares its own ISO-4217 currency:
unit_price: MONEY, EURDo not use a plain decimal when the value semantically represents money. The money type lets calculations and validation reject accidental operations between different currencies.
Rounding may be left unset or configured as half-up. It applies when the value is normalized for storage. Currency and rounding belong to the money column; they are not general settings for other numeric types.
Time values
Section titled “Time values”Choose temporal types by meaning:
DATEfor a calendar day such as an invoice issue date;TIMEfor a timezone-free time of day;INSTANTfor a real moment that should display in the viewer’s timezone;RAW_DATETIMEfor a civil date and time whose meaning must not shift with timezone;DURATIONfor an elapsed amount;PERIODfor a calendar-oriented interval.
The common mistake is using a timezone-free datetime for an event that happened at a real global instant. Decide whether the value should appear differently to viewers in different time zones.
Required values
Section titled “Required values”A required column rejects both omission and an explicit null value.
Use required when a row is not meaningful without the value—for example, an invoice line without its invoice, or an invoice without an issue date. Do not make a value required merely because one current screen happens to ask for it.
Empty text and null are different concepts. Required guarantees that a value is present; additional validation decides whether an empty string is acceptable.
Generated and read-only columns
Section titled “Generated and read-only columns”Some logical declarations create companion columns:
PHONEcreates extension, type, country, and calling-code information;IBANcreates country, BBAN, bank, and branch information;ACCOUNTINGcreates the fields that turn each business row into one journal-line source;ACCOUNTING_TRANSFERcreates source and target balance-transfer fields.
Clients receive two separate behavior flags:
| Flag | Meaning |
|---|---|
generated |
The server created this column as part of another declaration |
read_only |
The client must display but not submit edits for it |
generated_from identifies the source declaration. Do not offer server-generated companion types as choices when creating a column.
Quantity-ledger balances
Section titled “Quantity-ledger balances”A quantity-ledger column is a server-owned balance projected from contributions made elsewhere. It is appropriate for stock on hand, reserved quantity, accumulated units, or a similar balance that must be derived rather than edited freely.
Only INT, BIGINT, DECIMAL, and MONEY can be quantity-ledger backed. The column is not an ordinary writable field and cannot also be a Steel computed target. Scripts push contributions into the balance with quantity-add and quantity-subtract; see Quantity ledger.
A link gives a relationship a local name:
customer: LINK(customer)invoice: LINK(invoice)delivery_address: LINK(address)The value stored in the link is the related row’s ID. The linked table must already exist in the same profile.
Name the role, not merely the target
Section titled “Name the role, not merely the target”If only one relationship exists, using the target name can be clear:
invoice.customer → customerWhen several relationships point to the same table, use role names:
invoice.billing_address → addressinvoice.delivery_address → addressThis makes pickers, computed fields, and relationship traversal unambiguous.
Direction matters
Section titled “Direction matters”The table containing the link is the child or source of the relationship. An invoice_line.invoice link lets a line identify its invoice. The inverse collection—an invoice’s lines—is discovered through that link; it is not stored as an array on the invoice row.
Required links
Section titled “Required links”Make a link required when the child must never exist independently. An invoice line normally requires its invoice. An optional sales representative on a customer may legitimately be absent.
Link behavior
Section titled “Link behavior”Links are indexed automatically and carry a foreign-key constraint. Do not request a second ordinary index for the same link.
A table may not create a link to itself during definition. The built-in accounting relationship is also managed by the accounting definition rather than declared as an ordinary link.
Indexes
Section titled “Indexes”An index is useful when a column is frequently used to locate or order a small subset of rows. Typical candidates are invoice numbers, external identifiers, and fields used in repeated filtering.
Avoid indexing every column. Indexes consume storage and make writes more expensive. System columns and links already receive their required indexes automatically.
Only index a compound value through the concrete columns it generates; a compound declaration such as ACCOUNTING has no single physical column of its own.
Row display columns
Section titled “Row display columns”Row display columns form the human-readable label shown in a picker. Their order is significant.
Examples:
customer: nameinvoice: number, issue_dateproduct: sku, nameChoose columns that let a person distinguish rows quickly. The numeric row ID is used by itself when no display columns are configured; it should not be included explicitly in the display list.
Naming and renaming
Section titled “Naming and renaming”Use lowercase names beginning with a letter and containing only letters, digits, and underscores. Prefer stable domain language over labels tied to one screen.
These names are reserved for server-managed row state:
id deleted created_at row_revisionThe _id suffix is allowed, although a descriptive link name is usually clearer than mechanically adding _id.
Renaming changes the public alias without moving data or changing the physical identity. Existing links and calculations continue to address the same field internally, but client configuration must switch to the new alias.