---
title: "Properties (PropertyDecl)"
description: "Generated from the doc schema (CatalogDocSchema, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle…"
order: 11
id: "model/property"
---

# Properties (PropertyDecl)

Generated from the doc schema (`CatalogDocSchema`, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle decodes. A field’s Example is its value in one of those examples.

## PropertyDecl {#propertydecl}

A property of a type.

One of, told apart by `kind`:

- [PropertyDecl.stored](property.md#propertydecl.stored)
- [PropertyDecl.ref](property.md#propertydecl.ref)
- [PropertyDecl.derived](property.md#propertydecl.derived)
- [PropertyDecl.state](property.md#propertydecl.state)

## PropertyDecl.stored {#propertydecl.stored}

A stored value.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `kind` | `"stored"` | yes | — | `"stored"` | A stored property: a value in its own column. |
| `id` | [PropId](shapes.md#propid) | yes (propose assigns it; a new declaration leaves it out) | — | `"pr_2h6k8m4q0z"` | A property id (`pr_…`); renames keep it. |
| `name` | [Ident](shapes.md#ident) | yes | — | `"name"` | The property name. |
| `label` | [Text](shapes.md#text) | yes (a draft may leave it out: propose writes a default made from the name, `catalog.label_defaulted`) | — | `"Name"` | The words shown for it. |
| `type` | [ValueType](value-type.md#valuetype) | yes | — | `{"t":"text"}` | What the property holds. |
| `required` | boolean | no | `false` | `true` | Whether every row must have a value; default false. |
| `default` | [Expr](shapes.md#expr) \| [SequenceDefault](property.md#sequencedefault) | no | — | `"today()"` | The value of a new row whose create leaves it blank: a constant of the type, today() / now(), or a sequence (a text or integer property). |
| `history` | boolean | no | — | `true` | Whether every change of the value is kept as history. |
| `searchable` | [SearchSpec](type.md#searchspec) | no | — | `{"weight":"body"}` | Whether and how the property is searched. |
| `sensitive` | `"personal"` \| `"secret"` | no | — | `"personal"` | Personal data is masked for readers without the right; a secret is never shown (authz). |
| `migrate` | [PropertyDecl.stored.migrate](property.md#propertydecl.stored.migrate) | no (a draft’s only: propose takes it out before the hash, and a stored doc refuses it) | — | (see the example) | How this change converts the rows that exist; absent = the change needs no transform. |

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "stored",
  "name": "email",
  "label": "Email",
  "type": { "t": "text", "format": "email" },
  "required": true
}
```

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "stored",
  "name": "placed_on",
  "label": "Placed on",
  "type": { "t": "date" },
  "required": true,
  "default": "today()",
  "history": true,
  "searchable": { "weight": "body" },
  "sensitive": "personal"
}
```

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "stored",
  "name": "priority",
  "label": "Priority",
  "type": { "t": "integer" },
  "required": true,
  "migrate": {
    "fill": "band_rank",
    "convert": "band_rank",
    "map": { "urgent": "high" },
    "inverse": "band_rank"
  }
}
```

## PropertyDecl.ref {#propertydecl.ref}

A reference to another row.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `kind` | `"ref"` | yes | — | `"ref"` | A reference to one row of another type. |
| `id` | [PropId](shapes.md#propid) | yes (propose assigns it; a new declaration leaves it out) | — | `"pr_2d4f6h8k0m"` | A property id (`pr_…`); renames keep it. |
| `name` | [Ident](shapes.md#ident) | yes | — | `"customer"` | The property name. |
| `label` | [Text](shapes.md#text) | yes (a draft may leave it out: propose writes a default made from the name, `catalog.label_defaulted`) | — | `"Customer"` | The words shown for it. |
| `target` | [TypeId](shapes.md#typeid) | yes | — | `"ty_7k2m9q4z1a"` | The type it refers to. |
| `required` | boolean | no | `false` | `true` | Whether every row must have a value; default false. |
| `onDelete` | `"restrict"` \| `"cascade"` \| `"set_null"` | yes | — | `"cascade"` | What deleting the target row does: refuse, delete this row too, or clear the reference. |
| `history` | boolean | no | — | `true` | Whether every change of the value is kept as history. |
| `searchable` | [SearchSpec](type.md#searchspec) | no | — | `{"weight":"title"}` | Whether and how the property is searched. |
| `inverse` | [PropertyDecl.ref.inverse](property.md#propertydecl.ref.inverse) | no | — | `{"name":"orders","label":"Orders"}` | The reverse reference on the target (C22); default name = this type’s name. |
| `settles` | boolean | no | `true` | `true` | Whether a terminal state of the target locks this row (runtime); default true. |
| `sensitive` | `"personal"` \| `"secret"` | no | — | `"personal"` | Personal data is masked for readers without the right; a secret is never shown (authz). |
| `migrate` | [PropertyDecl.ref.migrate](property.md#propertydecl.ref.migrate) | no (a draft’s only: propose takes it out before the hash, and a stored doc refuses it) | — | (see the example) | How this change converts the rows that exist; absent = the change needs no transform. |

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "ref",
  "name": "customer",
  "label": "Customer",
  "target": "ty_7k2m9q4z1a",
  "required": true,
  "onDelete": "restrict",
  "history": true,
  "searchable": { "weight": "title" },
  "inverse": { "name": "orders", "label": "Orders" },
  "settles": false,
  "sensitive": "personal",
  "migrate": {
    "fill": "first_customer",
    "convert": "first_customer",
    "inverse": "customer"
  }
}
```

## PropertyDecl.ref.inverse {#propertydecl.ref.inverse}

The reverse reference on the target (C22); default name = this type’s name.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `name` | [Ident](shapes.md#ident) | yes | — | `"orders"` | The name CX reads the reverse collection by, on the target. |
| `label` | [Text](shapes.md#text) | yes (a draft may leave it out: propose writes a default made from the name, `catalog.label_defaulted`) | — | `"Orders"` | The words shown for the reverse collection. |

## PropertyDecl.derived {#propertydecl.derived}

A value computed from the row.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `kind` | `"derived"` | yes | — | `"derived"` | A derived property: a CX expression over the row, never stored. |
| `id` | [PropId](shapes.md#propid) | yes (propose assigns it; a new declaration leaves it out) | — | — | A property id (`pr_…`); renames keep it. |
| `name` | [Ident](shapes.md#ident) | yes | — | `"total"` | The property name. |
| `label` | [Text](shapes.md#text) | yes (a draft may leave it out: propose writes a default made from the name, `catalog.label_defaulted`) | — | `"Total"` | The words shown for it. |
| `params` | [ParamDecl](function.md#paramdecl)[] | no | — | `[{"name":"day","type":"date"}]` | The parameters a reader passes (cx.md §2.10). |
| `expr` | [Expr](shapes.md#expr) | yes | — | `"lines.sum(l => l.amount)"` | The CX expression it computes. |
| `type` | [ValueType](value-type.md#valuetype) | no | — | `{"t":"money","currency":"DKK"}` | The asserted type; compile checks it equals the inferred type (derived.type_mismatch). |
| `visibility` | `"over_readable"` \| `"whole_or_withheld"` | yes | — | `"over_readable"` | Over the rows the reader may read, or withheld unless the reader may read every row it uses. |
| `searchable` | [SearchSpec](type.md#searchspec) | no | — | `{"weight":"body"}` | Whether and how the property is searched. |

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "derived",
  "name": "total",
  "label": "Total",
  "expr": "lines.sum(l => l.amount)",
  "type": { "t": "money", "currency": "DKK" },
  "visibility": "over_readable",
  "searchable": { "weight": "body" }
}
```

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "derived",
  "name": "open_on",
  "label": "Open on",
  "params": [{ "name": "day", "type": "date" }],
  "expr": "placed_on <= day",
  "visibility": "whole_or_withheld"
}
```

## PropertyDecl.state {#propertydecl.state}

The row’s lifecycle state.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `kind` | `"state"` | yes | — | `"state"` | A state property: the row’s state in a lifecycle. |
| `id` | [PropId](shapes.md#propid) | yes (propose assigns it; a new declaration leaves it out) | — | — | A property id (`pr_…`); renames keep it. |
| `name` | [Ident](shapes.md#ident) | yes | — | `"status"` | The property name. |
| `label` | [Text](shapes.md#text) | yes (a draft may leave it out: propose writes a default made from the name, `catalog.label_defaulted`) | — | `"Status"` | The words shown for it. |
| `lifecycle` | [FsmId](shapes.md#fsmid) | yes | — | `"lc_2k4m6p8r0t"` | The lifecycle it follows. |
| `history` | `true` | no | — | `true` | A state always keeps its history. |

An item of `properties` in a type file (`types/<name>.json`).

```json
{
  "kind": "state",
  "name": "status",
  "label": "Status",
  "lifecycle": "lc_2k4m6p8r0t",
  "history": true
}
```

## PropertyDecl.stored.migrate {#propertydecl.stored.migrate}

How this change converts the rows that exist; absent = the change needs no transform.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `fill` | [Expr](shapes.md#expr) | no | — | `"band_rank"` | The value of each existing row when the property is added as required. |
| `convert` | [Expr](shapes.md#expr) | no | — | `"band_rank"` | The new value of each row when the property is retyped; null means the row cannot be converted. |
| `inverse` | [Expr](shapes.md#expr) | no | — | `"band_rank"` | The old value of each row, which makes a retype exact on rewind. |
| `map` | Record<[Ident](shapes.md#ident), [Ident](shapes.md#ident)> | no | — | `{"urgent":"high"}` | Old enum code to new code, for rows that hold a value this change drops. |

## PropertyDecl.ref.migrate {#propertydecl.ref.migrate}

How this change converts the rows that exist; absent = the change needs no transform.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `fill` | [Expr](shapes.md#expr) | no | — | `"first_customer"` | The value of each existing row when the property is added as required. |
| `convert` | [Expr](shapes.md#expr) | no | — | `"first_customer"` | The new value of each row when the property is retyped; null means the row cannot be converted. |
| `inverse` | [Expr](shapes.md#expr) | no | — | `"customer"` | The old value of each row, which makes a retype exact on rewind. |

## SequenceDefault {#sequencedefault}

A number the write engine allocates when a create leaves the property blank: gap-free per scope, in the write transaction.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `sequence` | string | yes | — | `"FV/{year}/"` | The template the number follows: literal text and the date parts {year} and {month}, which set the scope (one counter per year, per month, or one for the property). |
| `start` | integer (greater than or equal to 0) | no | `1` | `1001` | The first number of each scope; default 1. |
| `digits` | integer (between 1 and 12) | no | — | `4` | Writes the number with at least this many digits, zero-padded (a text property only). |

The `default` of a stored text or integer property.

```json
{ "sequence": "FV/{year}/", "start": 1001, "digits": 4 }
```
