User-Defined Fields
Endpoints for managing user-defined fields (custom profile attributes attached to User accounts), setting field values on a specific User, and discovering the registered custom-field drivers (the keyword contexts the script and painless engines expose).
A user-defined field is addressed by its name, which is the per-tenant natural
key (there is a unique (tenant, name) index). The model's primary key is a
server-generated UUID id, but every {name} route resolves the row by the
name column, and the create route's Location header points at
/api/user-fields/\{name\}.
There is no license-feature gate on any route in this area. The three write
routes (create, update, delete) and the set-value route each require Superuser
(via permission.userField.create, permission.userField.edit, and
permission.userField.delete, all of which reduce to a superuser check). The
read routes (list and get a field) and the two driver-discovery routes are open
within the tenant with no permission pre-block. All lookups are tenant-scoped by
row-level security but not owner-scoped, so any Superuser in the tenant can read,
edit, or delete any field. User fields have no owner.
GET /api/user-fields
List the tenant's user-defined fields.
Authentication: Open within the tenant (no permission pre-block)
Response:
This is not a HAL { total, limit, offset, results } collection envelope and not
a model query. It is a hand-written raw-SQL SELECT that returns a bare JSON
array of rows, each shaped
{ name, label, description, defaultValue, type, createdAt, updatedAt }. The
UUID id is intentionally omitted. Row-level security scopes the rows to the
caller's tenant even though the SQL has no explicit tenant clause. The SQL has no
ORDER BY, so order is not guaranteed; this example is sorted by name. On a
query error the route answers 422. HAL api rel inf:user-field-list.
| Field | Type | Description |
|---|---|---|
name | string | Per-tenant natural key for the field |
label | string | Display label |
description | string | null | Field description |
defaultValue | any | null | Default value (stored as JSONB) |
type | string | Data type label (e.g. Text, Number, Date) |
createdAt / updatedAt | date | Timestamps |
[
{
"name": "costCenter",
"label": "Cost Center",
"description": "Accounting cost-center code for this user",
"defaultValue": null,
"type": "Text",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"name": "department",
"label": "Department",
"description": null,
"defaultValue": null,
"type": "Text",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"name": "region",
"label": "Sales Region",
"description": "Sales region this user belongs to",
"defaultValue": "Unassigned",
"type": "Text",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
POST /api/user-fields
Create a user-defined field.
Authentication: Superuser (permission.userField.create)
Request Body:
The payload is stripUnknown, so unrecognized fields are dropped.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Per-tenant natural key (unique (tenant, name)) |
label | string | Yes | Display label |
type | string | Yes | Data type label. A free string (e.g. Text, Number, Date); not validated against an enum here |
description | string | No | Field description |
defaultValue | any | No | Default value, stored as JSONB (allows null or "") |
Example Request:
{
"name": "employeeId",
"label": "Employee ID",
"type": "Text",
"description": "Internal employee identifier",
"defaultValue": null
}
Response:
Responds 201 Created with the created field and a Location header pointing at
/api/user-fields/\{name\}. The UUID id is server-generated. HAL api rel
inf:user-fields.
{
"name": "employeeId",
"label": "Employee ID",
"type": "Text",
"description": "Internal employee identifier",
"defaultValue": null
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/user-fields/employeeId"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "employeeId",
"label": "Employee ID",
"type": "Text",
"description": "Internal employee identifier",
"defaultValue": null,
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
GET /api/user-fields/{name}
Retrieve a single user-defined field by name.
Authentication: Open within the tenant (no permission pre-block)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | The field's name (the per-tenant natural key), not the UUID id |
Response:
The single field. Answers 404 if no field with that name exists in the tenant.
HAL rel inf:user-field.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/user-fields/region"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "region",
"tenant": "acme",
"label": "Sales Region",
"description": "Sales region this user belongs to",
"defaultValue": "Unassigned",
"type": "Text",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
PUT /api/user-fields/{name}
Update a user-defined field.
Authentication: Superuser (permission.userField.edit)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | The field's current name, used to locate the row (WHERE name = {name}) |
Request Body (partial):
The payload is stripUnknown. All fields are optional on update.
| Field | Type | Description |
|---|---|---|
name | string | New name. Send a new value here while the URL keeps the OLD name to rename the field |
label | string | Display label |
description | string | Field description |
defaultValue | any | Default value (JSONB) |
type | string | Data type label |
The {name} in the URL always locates the existing row. To rename, keep the OLD
name in the URL and send the new name in the body.
Example Request:
{
"label": "Sales Region (Updated)",
"description": "Primary sales region this user is assigned to",
"defaultValue": "North America"
}
Response:
Responds 200 with the updated field. Answers 404 if no field with that name
exists. HAL rel inf:user-field.
{
"label": "Sales Region (Updated)",
"description": "Primary sales region this user is assigned to",
"defaultValue": "North America"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/user-fields/region"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "region",
"tenant": "acme",
"label": "Sales Region (Updated)",
"description": "Primary sales region this user is assigned to",
"defaultValue": "North America",
"type": "Text",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
DELETE /api/user-fields/{name}
Delete a user-defined field by name.
Authentication: Superuser (permission.userField.delete)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | The field's name |
Response:
Responds 200 with an empty body (Informer's db.remove convention, where the
handler returns h.continue), not 204.
PUT /api/user-field-value/{username}
Set one or more user-defined-field values on a specific User.
Authentication: Superuser (permission.userField.edit)
This route does not touch the user-field table. It loads the User by username
(the User primary key), shallow-merges value into the user's userFields JSONB
column, and saves. Omitted keys are preserved.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
username | string | The User's username |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
value | object | Yes | A map of field name to value. Merged into the User's userFields column |
The {username} is not resolved through a lookup pre-block. An unknown username
makes the User lookup return null and the handler throws (a 500-class error)
rather than answering a clean 404. Send a username that exists.
Example Request:
{
"value": {
"region": "North America",
"costCenter": "CC-1001"
}
}
Response:
Responds 200 with the saved User (the full user representation). HAL rel
inf:user-field-value.
{
"value": {
"region": "North America",
"costCenter": "CC-1001"
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/user-field-value/admin"
},
"inf:memberships": {
"href": "https://informer.example.com/api/user-field-value/admin/memberships"
},
"inf:avatar-upload": {
"href": "https://informer.example.com/api/user-field-value/admin/_upload/{uploadId}/avatar-upload",
"templated": true
},
"inf:password": {
"href": "https://informer.example.com/api/user-field-value/admin/password"
},
"inf:feed": {
"href": "https://informer.example.com/api/user-field-value/admin/feed"
},
"inf:viewed-feed": {
"href": "https://informer.example.com/api/user-field-value/admin/_markFeedViewed"
},
"inf:superuser": {
"href": "https://informer.example.com/api/user-field-value/admin/superuser"
}
},
"permissions": {
"changeDomain": false,
"changeProfile": true,
"changePassword": true,
"delete": false,
"edit": true,
"reassign": true,
"superuser": true,
"enable": true,
"changeTheme": true,
"lock": true,
"setGlobalPermissions": true,
"manageLogin": true
},
"domain": "local",
"username": "admin",
"displayName": "acme Administrator",
"familyName": "Administrator",
"givenName": "acme",
"middleName": null,
"email": null,
"data": null,
"superuser": true,
"timezone": "America/New_York",
"settings": {
"log": {
"log": false,
"info": false,
"warn": false,
"debug": false,
"error": true,
"remote": false
}
},
"enabled": true,
"source": null,
"sourceId": null,
"lastViewedFeed": null,
"tenant": "acme",
"globalPermissions": {
"ai": false,
"jobCreation": true,
"viewAllTeams": true,
"viewAllUsers": true,
"painlessScriptCreation": true
},
"userFields": {
"region": "North America",
"costCenter": "CC-1001"
},
"locked": false,
"lockedAt": null,
"loginAttempts": 0,
"passwordSetAt": "2026-01-15T16:20:00.000Z",
"passwordExpiresAt": "2026-01-15T16:20:00.000Z",
"forgotPasswordRequestTime": null,
"mfa": null,
"mfaConfig": {},
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "EA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
GET /api/custom-field-drivers
List the registered custom-field drivers.
Authentication: Open within the tenant (no permission pre-block)
Response:
Lists the drivers registered in the customFields driver manager. Only the
user driver ships (id user, scope request). Each driver is spread and gets
a variables array; the driver's function-valued properties are dropped by JSON
serialization, so each item is { id, scope, label, variables }. A driver whose
variable resolution throws is returned without variables rather than failing
the whole route. The variables come from a query with no ORDER BY, so this
example sorts them by name. HAL rel inf:custom-field-drivers.
The user driver's variables are one entry per user-defined field (with
dataType equal to the field's type, context of user, and the field's UUID
id) plus six built-in user-attribute keywords (username, displayName,
email, timezone, superuser, enabled), each with dataType of Text.
| Field | Type | Description |
|---|---|---|
id | string | Driver id (e.g. user) |
scope | string | Driver scope (e.g. request) |
label | string | Driver label (an i18n message key) |
variables | array | The keyword variables this driver exposes |
[
{
"id": "user",
"scope": "request",
"label": "common:user_field_plural",
"variables": [
{
"label": "Cost Center",
"name": "costCenter",
"description": "Accounting cost-center code for this user",
"dataType": "Text",
"context": "user",
"defaultValue": null,
"id": "00000000-0000-4000-8000-000000000001"
},
{
"name": "displayName",
"label": "Display Name",
"dataType": "Text",
"context": "user"
},
{
"name": "email",
"label": "Email",
"dataType": "Text",
"context": "user"
},
"… 6 more items (9 total) — omitted from docs"
]
}
]
GET /api/custom-field-drivers/{id}
Retrieve a single custom-field driver by id.
Authentication: Open within the tenant (no permission pre-block)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Driver id (here user) |
Response:
The single driver with its variables, the same shape as one element of the
list. Variables are sorted by name for a stable example. HAL rel
inf:custom-field-driver.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/custom-field-drivers/user"
}
},
"id": "user",
"scope": "request",
"label": "common:user_field_plural",
"variables": [
{
"label": "Cost Center",
"name": "costCenter",
"description": "Accounting cost-center code for this user",
"dataType": "Text",
"context": "user",
"defaultValue": null,
"id": "00000000-0000-4000-8000-000000000001"
},
{
"name": "displayName",
"label": "Display Name",
"dataType": "Text",
"context": "user"
},
{
"name": "email",
"label": "Email",
"dataType": "Text",
"context": "user"
},
"… 6 more items (9 total) — omitted from docs"
]
}