Objects, Records, and Fields
Henceforth structures data as objects, fields, and records – analogous to tables, columns, and rows in a relational database. This page introduces these core concepts, explains how they compose, and shows what they look like as metadata, in PostgreSQL, and through the API.
Objects
Section titled “Objects”An object defines a type of record. If you are building a CRM, you might define objects for Account, Contact, and Opportunity. Each object becomes a PostgreSQL table, a read view, and a set of API endpoints.
An object is declared in a JSON5 metadata file:
{ objectKey: "Account", label: "Account", description: "A business or organization",}This definition compiles to a table named crm.account, a view named crm.account_view, and API routes under /data/crm/Account. The object has no columns of its own yet – fields provide the structure.
This guide uses a
crmmodule for its examples becausecrmis something you author from files. Thecoremodule is builtin – its objects (Individual,Organization,ContactPoint,Note) are defined in Rust and cannot be created or overridden from metadata files. Seedev/metadata/README.md.
Objects can also participate in inheritance hierarchies. A base object like PaymentMethod can declare itself abstract, and derived objects like CreditCard and BankTransfer can extend it. See Inheritance below.
For the full set of object properties, see ObjectDefinition.
Records
Section titled “Records”A record is one instance of an object – a specific account, a specific contact. When you create a record through the API, the runtime generates a UUIDv7 identifier, inserts a row into hf.object (the universal root, setting objectType, createdAt, and updatedAt), then inserts a row into the object’s own table, and finally reads the full record back from the object’s view before returning it. For inherited objects, the runtime inserts into each table in the inheritance chain, base to leaf, all sharing the same UUID – see Inheritance below.
Every record in the system has an entry in hf.object, regardless of which object type it belongs to. This universal root stores the system fields that are common to all records:
| Field | Type | Description |
|---|---|---|
id |
UUID | Globally unique identifier, generated as UUIDv7 on creation. |
objectType |
string | Fully qualified type name (e.g., crm.Account). |
createdAt |
timestamp | When the record was created. |
updatedAt |
timestamp | When the record was last modified. |
System fields are read-only – they are managed by the runtime and cannot be set through the API. A fifth system field, ownerId (the PG role name of the creating principal), is not part of the universal-root table above but is read alongside it on every record – see System fields.
A record returned by the API includes both system fields and user-defined fields:
{ "id": "019502a4-b4e7-7f3a-8c1e-0a1b2c3d4e5f", "objectType": "crm.Account", "createdAt": "2024-03-15T10:30:00.000Z", "updatedAt": "2024-03-15T10:30:00.000Z", "name": "Acme Corp", "industry": "Technology", "active": true}Fields
Section titled “Fields”Fields give objects their structure. Every field is authored as a separate JSON5 file and compiles to a column on the object’s table (or an expression in its view).
Henceforth has four field kinds, each serving a different purpose:
| Field kind | Purpose | Example |
|---|---|---|
| Scalar | Stores a user-provided value | Account name, email address, active flag |
| Formula | Derives a value from fields on the same record or a related object | Contact display name, account industry via lookup traversal |
| Lookup | References a record in another object | Contact’s parent account |
| Rollup | Computes an aggregate across child records | Number of contacts on an account |
Scalar fields
Section titled “Scalar fields”A scalar field stores a value that a user or system writes directly. It is the most common field kind.
{ fieldKind: "scalar", fieldKey: "name", label: "Account Name", fieldType: "text", maxLength: 255, required: true,}This compiles to a VARCHAR(255) NOT NULL column named name on the crm.account table. The required: true property means the API rejects creates and updates that omit this field. Without maxLength, the column would be TEXT (unlimited length).
Scalar fields support seven data types: text, int, decimal, bool, timestamp, date, and json. Each maps to a specific PostgreSQL type. For the full type mapping, see FieldDefinition: Scalar Types.
Formula fields
Section titled “Formula fields”A formula field derives its value from an expression that references other fields. Formula fields are read-only in the API – their values are computed, not stored.
Same-row formula:
{ fieldKind: "formula", fieldKey: "displayName", label: "Display Name", returnType: "text", formula: "firstName & ' ' & lastName",}This compiles to a virtual generated column. The & operator is null-safe string concatenation – if firstName is null, the result is " Smith" rather than null.
Cross-object formula:
{ fieldKind: "formula", fieldKey: "accountIndustry", label: "Account Industry", returnType: "text", formula: "account.industry",}This traverses the account lookup to fetch the industry field from the related Account. Cross-object formulas compile to computed columns in the object’s view (correlated subqueries), not generated columns on the table.
For the full expression syntax, operators, and functions, see Formula Language.
Lookup fields
Section titled “Lookup fields”A lookup field stores a reference to a record in another object. It compiles to a UUID column with a foreign key, giving database-enforced referential integrity. Lookup fields are authored as managed relationships, not as raw fieldKind: "lookup" field files (that authoring form was deprecated by #517 – see FieldDefinition: Lookup).
{ relationshipKey: "contactAccount", label: "Account", from: "crm.Contact", to: "crm.Account", fromField: "account", toField: "contacts", cardinality: "manyToOne",}This compiles to a column named account_id on crm.contact with a foreign key to crm.account(id) and an index for join performance. The inverse side (Account.contacts) is derived from toField – it does not require a separate metadata file.
Relationships support both single-target and polymorphic multi-target references, and carry composable properties that control referential behavior (required, on-delete, mutability, uniqueness). The next page covers this in detail.
Rollup fields
Section titled “Rollup fields”A rollup field computes an aggregate value across child records of a lookup relationship. Rollups are maintained by database triggers and are always transactionally consistent.
{ fieldKind: "rollup", fieldKey: "contactCount", label: "Number of Contacts", returnType: "int", relationship: "contacts", function: "count",}This compiles to a BIGINT NOT NULL DEFAULT 0 column on crm.account and a trigger on crm.contact that recalculates the count on every insert, update, or delete.
Rollups are decoupled from relationship type – they work on any lookup, not just cascade-delete or master/detail relationships. Five aggregation functions are available: count, sum, min, max, avg.
For the full rollup property reference, see FieldDefinition: Rollup.
Inheritance
Section titled “Inheritance”Objects can form type hierarchies using class-table inheritance. A base object defines common fields, and derived objects add specialized fields.
{ objectKey: "PaymentMethod", label: "Payment Method", abstract: true, // Cannot create PaymentMethod records directly}
// crm_CreditCard_object.json5{ objectKey: "CreditCard", label: "Credit Card", extends: "crm.PaymentMethod", extensionKind: "subtype",}PaymentMethod defines shared fields like billingName. CreditCard adds last4. The compiler produces a table for each level – PaymentMethod fields live in crm.payment_method, CreditCard fields live in crm.credit_card. A view joins both tables for reads, so querying CreditCard returns all PaymentMethod fields plus CreditCard-specific fields in one flat result.
When you create a CreditCard record, the runtime inserts into hf.object, then crm.payment_method, then crm.credit_card – all sharing the same UUID.
Abstract objects cannot be instantiated directly. The data API rejects create requests for abstract objects.
Lookups targeting a base type automatically accept all derived types through the real FK. A lookup to: "crm.PaymentMethod" accepts CreditCard, BankTransfer, and any future subtypes – no discriminator column needed.
For the full set of inheritance properties (extends, abstract, extensionKind), see ObjectDefinition.
How It All Fits Together
Section titled “How It All Fits Together”Consider a CRM with accounts, contacts, and a contact count rollup. The metadata set looks like this:
dev/metadata/ crm_module.json5 # module namespace crm_Account_object.json5 # Account object crm_Account.name_field.json5 # scalar: required string crm_Account.industry_field.json5 # scalar: optional string crm_Account.contactCount_field.json5 # rollup: count of contacts crm_Contact_object.json5 # Contact object crm_Contact.firstName_field.json5 # scalar: required string crm_Contact.lastName_field.json5 # scalar: required string crm_Contact.displayName_field.json5 # formula: firstName & ' ' & lastName crm_Contact.account_field.json5 # lookup: -> crm.AccountWhen you apply this metadata, the compiler produces:
hf.object(universal root, created once at bootstrap)crm.accounttable withname,industry,contact_countcolumnscrm.contacttable withfirst_name,last_name,account_idcolumns, plus a generateddisplay_namecolumn- Views joining each table with
hf.object - A foreign key from
crm.contact.account_idtocrm.account.id - An index on
account_id - A rollup trigger on
crm.contactthat maintainscontact_countoncrm.account
The data API then serves CRUD endpoints for both objects, validating field values against the metadata on every request.
What’s Next
Section titled “What’s Next”With objects and fields defined, the next step is connecting objects to each other through relationships. Henceforth uses composable lookup properties instead of rigid relationship types – see Relationships and Lookups.
Reference
Section titled “Reference”- ObjectDefinition – full object properties, compilation output, and inheritance rules
- FieldDefinition – scalar, formula, lookup, and rollup field properties
- Formula Language – expression syntax, operators, functions, and null behavior