Skip to content

How table definitions work

A table definition describes the shape and meaning of one kind of record. For example, an invoice-line definition might contain a description, quantity, unit price, total, and a link to its invoice.

The definition is the shared contract used by forms, stored rows, links, validation, and computed fields. Good definitions make all of those parts predictable; poor definitions force every client to invent its own interpretation.

Define one table around one kind of thing:

Table One row represents
customer One customer
invoice One invoice header
invoice_line One line belonging to an invoice
product One product

Do not copy all customer fields into every invoice just because one screen displays them together. Store the customer once and link the invoice to it. Create a separate child table when a record can contain a repeated collection, such as invoice lines.

An ordinary profile contains the tables and records for one accounting-period workspace. A profile table may link to another table in that profile or to a table in the global profile, but never directly to another ordinary profile’s table. A global table may link only to another global table. See Accounting-period profiles and the global profile for the scope model.

The profile also has one accounting currency. This is not necessarily the currency of every money column. A table may contain EUR and USD money fields while the profile keeps its books in EUR.

A definition decides:

  • which columns a row has;
  • the type and meaning of each value;
  • which values are required;
  • which columns link to rows in other tables;
  • how a row is named in pickers;
  • which columns are indexed for repeated lookup;
  • which values are managed by the server instead of edited directly.

The server adds row identity, deletion state, creation time, and revision tracking automatically. Do not model those as user columns.

An invoice is naturally split into a header and repeated lines:

invoice
├── number: TEXT, required
├── customer: LINK(customer), required
├── issue_date: DATE, required
└── total: MONEY(EUR)
invoice_line
├── invoice: LINK(invoice), required
├── description: TEXT, required
├── quantity: DECIMAL(12,3), required
├── unit_price: MONEY(EUR), required
└── line_total: MONEY(EUR)

This structure answers important questions without client-specific rules:

  • every line belongs to exactly one invoice because its link is required;
  • quantity supports fractional units because it is decimal;
  • prices and totals carry an explicit currency;
  • invoice and customer pickers can use meaningful display columns;
  • line_total and invoice.total can later be backed by Steel calculations without changing their storage types.

Row display columns define how another part of the application identifies a row. A customer might use name; an invoice might use number followed by issue_date.

Choose fields that are short, recognizable, and reasonably stable. Avoid long notes, calculated balances, or values that change constantly. If no display columns are selected, the row is identified only by its numeric ID.

A link column stores the ID of a row in another table and gives the relationship its name.

invoice.customer → customer
invoice_line.invoice → invoice

The name matters. If an invoice links to the same address table twice, call the links something meaningful such as billing_address and delivery_address. Consumers can then tell which relationship to follow without guessing.

Links are also used by pickers, related aggregates, hydration, and quantity effects. See Columns and links for the detailed rules.

Some returned columns are generated or read-only. Examples include parsed phone and IBAN components, accounting fields, accounting-transfer source values, and quantity-ledger balances.

A client should respect the behavior returned with the definition:

  • show generated values when they help the user;
  • disable editing when a value is read-only;
  • submit only fields the user is allowed to control;
  • never infer editability only from a column name or type.

Generated and read-only are separate properties. A generated companion may still be writable in some cases, so both flags matter.

Columns can be appended to an existing user-defined table. Existing columns keep their identity, order, data, and type.

Before adding a required column to a table that already has rows, decide how existing records will obtain a valid value. Definition changes should reflect a data-migration plan, even when the structural operation itself is accepted.

Accounting and accounting-transfer definitions must be chosen when the table is first created. They cannot be bolted onto an existing table because they provision several coordinated fields and supporting structures.

A rename changes the public alias while preserving the physical column and its data. Links, validation, and stored calculations continue to point at the same underlying field.

The rename still changes the client-facing contract. Refresh forms, imports, saved mappings, and other configuration to use the new alias. Rename history is available when old saved configuration must be reconciled.

The definition service is not a general schema editor. It does not provide ordinary operations for:

  • changing an existing column’s type;
  • reordering existing columns;
  • removing individual columns;
  • converting an ordinary column into a link;
  • changing a link to target another table.

Treat those as explicit data-migration work rather than silently changing what existing values mean.

A user-defined table can be removed only when it has no rows. Deleting it removes the table structure, not merely the rows inside it. Check incoming relationships and attached behavior before deletion.

Protected system tables are owned by their backend feature and cannot be changed or deleted through general table-definition operations.

Copying a profile creates the same structure without copying its row data. This is useful for turning a known-good profile into a template for another accounting entity or period.

The copy preserves columns, links, indexes, validation, accounting configuration, and attached Steel scripts. Relationships and script dependencies are resolved against the copied tables rather than continuing to point at the source profile.

Columns and links explains how to choose types, when fields are generated or read-only, how links behave, and which columns make useful indexes and row labels.