Package settings¶
Settings are values that an organization administrator changes in the live
system without a new package version and without an installation plan: an
approval threshold, a review due date, an escalation role. The package
declares their shape in the manifest, the core keeps the values with their
history, and the package's processes, rules, and screens read them as
settings.<field>. The first half of the page is for the package author, the
second is for the administrator. Rationale: TAI-ADR-0067, CP-ADR-0081.
Setting or installation variable¶
A package has two ways to keep a value out of its files. They do not replace each other:
Installation variable ${NAME} |
Setting settings.<field> |
|
|---|---|---|
| Declared in | spec.variables of the manifest |
spec.settings of the manifest |
| Where the value lives | the installation file, .env, the environment |
the core, with a version history |
| Who changes it and how | the installation author: edit, plan, apply |
the administrator: the console or PUT /api/v1/packages/{key}/settings |
| When it takes effect | after apply of a new revision of the objects |
on the next computation, without a plan and without a new process version |
| How it reaches the object | as text in the file before the expression is parsed | the settings variable, typed from the schema |
| What it suits | deployment topology: a workspace id, a system address | the organization's rules of the game: thresholds, due dates, roles |
| Secrets | no (secrets belong to credentials and connections) | no: a schema or values with secret markers are rejected |
If the business changes the value, rather than whoever installs the package, it is a setting. Installation variables are covered in Package anatomy.
Declaration¶
Settings are declared by the spec.settings section of the package.yaml
manifest: the schema of the values and an optional form layout, uischema.
apiVersion: taimen.ai/v1
kind: Package
key: claims
spec:
version: 0.2.0
locales: [en, ru]
defaultLocale: en
settings:
schema:
type: object
properties:
refundLimit: {type: number, minimum: 0, default: 500}
replyDueWorkdays: {type: integer, minimum: 1, maximum: 20, default: 2}
escalationRole: {type: string, x-ref: role}
required: [escalationRole]
uischema:
type: VerticalLayout
elements:
- type: Group
label: claims.settings.groups.refund
elements:
- {type: Control, scope: "#/properties/refundLimit"}
- {type: Control, scope: "#/properties/escalationRole"}
- type: Group
label: claims.settings.groups.reply
elements:
- {type: Control, scope: "#/properties/replyDueWorkdays"}
The JSON Schema subset¶
schema is a subset of JSON Schema, as for process data. The root is an
object; a field name is camelCase (^[a-z][A-Za-z0-9_]{0,62}$), since
expressions read the field by it.
| Keyword | For which types |
|---|---|
type |
string, integer, number, boolean, array, object |
properties, required |
object |
items, minItems, maxItems |
array; minItems, maxItems are integers from 0 |
enum |
scalars, up to 100 values |
minimum, maximum |
integer, number |
minLength, maxLength, pattern, format |
string; format is date, uri, email, or uuid |
default |
any; the value must match the field schema |
x-ref |
string: a reference to a platform object (below) |
Limits: objects nest at most three levels deep, counting the root, and at the
third level an array holds only scalars; an object has at most 100
properties. additionalProperties is allowed only as false (which is
implied anyway). Do not write title and description in the schema: the
labels come from the package dictionaries (see Labels).
x-ref says that the string references a platform object in the
organization. On save, the core checks that the object exists and is in use:
x-ref |
Value | "In use" |
|---|---|---|
role |
role id | the role exists in the organization |
principal |
principal id | the principal is not disabled |
workspace |
workspace id (not a slug) | the workspace is active |
taskType |
task type key | it has an active version |
calendar |
calendar key | the calendar is not retired |
Default values¶
Every optional scalar field and array has a default: the package works
right after installation without saving anything. An optional object needs
no default: it is assembled from the default values of its fields.
A required field (required) may do without a default, but then it has no
effective value until the first save, and the package object must account for
that: has(settings.escalationRole) in CEL. A reference to a role, principal,
or workspace gets no default: every organization has its own ids, so such a
field is declared required.
No secrets in settings¶
Settings are visible to everyone with the read permission, and their values
go to the package's agents and skills. That is why secret markers are
rejected already in the declaration: writeOnly, format: password, a field
name such as password, secret, token, apiKey, privateKey,
credential, authorization, clientSecret (the name secretRef is
allowed), and secret material in default or enum. A secret belongs to a
connection or to a named secret of an agent.
Labels¶
The schema holds no form strings: labels are keys of the package dictionaries
i18n/<locale>.yaml. A package with settings declares its languages in the
manifest (locales, defaultLocale), and every required key is present in
the dictionary of every declared language.
| String | Dictionary key | Required |
|---|---|---|
| package name on the settings screen | <package>.title |
yes |
| field label | <package>.settings.<path> |
yes, for every property |
| hint under the field | <package>.settings.<path>.help |
no |
group title (label of a Group) |
<package>.settings.groups.<id> |
yes |
label of a Control, text of a Label |
any key of the package dictionary | yes |
<path> is the property names joined by dots from the schema root
(limits.refund). Properties of objects inside the items of an array have
no labels: the form shows the array as a whole.
# i18n/en.yaml
claims.title: Customer claims
claims.settings.refundLimit: Refund without approval, up to
claims.settings.refundLimit.help: >-
A refund above this amount is approved by the claims manager. Cases where the
decision has already been made keep the previous limit
claims.settings.replyDueWorkdays: Reply to the customer within, workdays
claims.settings.escalationRole: Escalation role
claims.settings.groups.refund: Refunds
claims.settings.groups.reply: Reply to the customer
The core returns the strings in the requested language (?locale=); if the
package lacks that language, in its defaultLocale.
Form layout: uischema¶
uischema is a closed subset of JSON Forms that the console renders. Without
it, the console lays the fields out in a column in schema order, with a
nested object as a group.
type |
Fields | What |
|---|---|---|
VerticalLayout |
elements |
elements in a column |
HorizontalLayout |
elements |
elements in a row |
Group |
label, elements |
a frame with a title |
Control |
scope, label? |
a form field |
Label |
text |
a line of text |
- The root is a
VerticalLayout,HorizontalLayout, orGroup; nesting is at most 5, and there are at most 200 elements in total. scopeis a pointer to a schema property:#/properties/<field>, nested#/properties/<a>/properties/<b>. One property has at most oneControl; a pointer into theitemsof an array is not allowed.- Any element may have a
rule: aneffect(SHOW,HIDE,ENABLE,DISABLE) and acondition {scope, schema}, whereschemaisconst,enum, orminimum/maximum. A rule only changes how the form looks: a hidden field is still saved and checked. - A field without a
Controlin a givenuischemais the warningsettings_uischema_uncovered: the value is in effect, but it cannot be changed in the form. - Elements have no other fields: the core rejects
optionson aControl(multi,format: radiofrom JSON Forms) at plan time with the codesettings_uischema_unsupported.
Reading settings: settings¶
A package object reads the settings of its own package by the name
settings. Another package, and an object created by hand rather than by a
package, do not see the settings: a reference to settings in it is the
publishing error settings_ref_unknown.
| Where | How | When it is read |
|---|---|---|
| Process | the CEL variable settings.<path>, typed from the schema |
once per step transaction |
| Process step due date | {expr: settings.<field>} in place of the number in workdays, workhours, and the same in warnBefore: due: {workdays: {expr: settings.<field>}}, a non-negative integer |
once on entering the step |
| Work rule | {var: settings.<path>} in a condition, {{settings.<path>}} in a template |
once per evaluation |
Package screen (View) |
the settings variable in expressions |
once per block data query |
| Package agent | packageSettings in the GET /api/v1/agents/me response |
on request |
| Package skill | ctx.settings in skill-sdk |
pinned to the invocation attempt |
# process: a decision table input and a step due date from settings
decisions:
- id: refund-route
inputs:
- {id: small, expr: "data.refundAmount <= settings.refundLimit", type: boolean}
# …
- id: reply
human:
taskType: claim-reply
assign: [{role: claims-officer}]
due: {workdays: {expr: settings.replyDueWorkdays}, warnBefore: {workhours: 4}}
# rule: a condition on a setting
spec:
condition:
gt: [{var: payload.data.amount}, {var: settings.refundLimit}]
The effective value is the saved one over the schema default: nested
objects are merged field by field, and an array is replaced as a whole. A
changed value takes effect on the next computation; a case that has already
passed the step does not change.
Journal and replay¶
If a process version reads settings in at least one expression, every
journal entry of the instance carries settingsVersion and
settingsSchemaRevision: the values version and the schema revision the step
saw. Replay and a dry run give the step the values of that version, not the
current ones, so the decision of an earlier case is reproduced even after the
setting changes. An entry whose version is not in the database is the replay
divergence {kind: "settings", journalSeq, recorded}. A rule writes the same
pair into the evidence of its evaluation as the element
{"kind": "settings", …}.
Checking without a deployment¶
package-sdk check checks the declaration and the references with the same
codes the core uses at plan time. The path of a declaration finding starts
at /spec/settings; a reference finding has the path of the expression in
the object file.
| Code | When |
|---|---|
settings_schema_unsupported |
a keyword outside the subset, title/description, a root that is not an object, exceeded limits, an x-ref of an unknown kind or not on a string |
settings_default_missing |
an optional field has no default |
settings_default_invalid |
the default does not match the field schema |
settings_secret_field |
a secret marker in the schema, a field name, default, or enum |
settings_uischema_unsupported |
a layout outside the subset: an element field outside the table (for example, options), a scope not on a property, a second Control on the same property, a group label that is not <package>.settings.groups.<id> |
settings_uischema_uncovered |
a warning: a field has no Control |
settings_label_missing |
the package declares no locales, or a required label key is missing from a language's dictionary |
settings_ref_unknown |
settings.<path> reads an undeclared field, or the object's package declares no settings |
settings_ref_type |
the field type does not fit the place: an object in a string template, an array in a comparison, a non-integer in a due date |
check checks rules completely; the CEL expressions of processes and screens
are typed by the core code next to package-sdk (the sandbox extra).
Without it, check catches only undeclared fields and warns that the types
were not checked.
package-sdk describe shows settings next to installation variables:
settings (changed by an administrator in the live system):
escalationRole [string, ref role, required] (claims)
refundLimit [number, default 500] (claims)
replyDueWorkdays [integer, default 2] (claims)
Scenarios with settings are covered in Package tests.
Compatibility on upgrade¶
apply of a new package version writes a new revision of the settings
schema but does not touch the saved values: the values version, the history,
and the event do not change. What will change is shown by plan in its
settings section:
"settings": {
"schemaRevision": {"before": 1, "after": 2},
"added": [{"path": "/replyDueWorkdays", "default": 2}],
"removed": [{"path": "/legacyLimit", "saved": true}],
"incompatible": [{"path": "/refundLimit", "code": "maximum"}],
"uischemaChanged": true
}
| Field | Meaning |
|---|---|
schemaRevision |
the schema revision before and after; after: null means the schema does not change or the package stops declaring settings |
added |
a new field and its default: it takes effect at once, nothing needs saving |
removed |
the field left the schema; saved tells whether it had a saved value. That value stays in the history, but it no longer appears in responses and expressions |
incompatible |
a saved value does not match the schema of the new revision: the path and the keyword, without the value |
uischemaChanged |
the form layout changed |
An incompatible value is also the plan error settings_incompatible; apply
of such a plan answers 422 invalid_package. There is no automatic migration
of values: the administrator saves a fitting value, and the author builds the
plan again. Hence the rules for the author of a new version:
- give a new field a
default, and the upgrade requires nothing from the administrator; - check a narrowing (a smaller
maximum, a shorterenum, a new required field without adefault) against the deployment's saved values: the plan shows each incompatible one; - renaming a field is a removal plus an addition: the saved value of the old field is not carried over;
- a value saved between
planandapplymakes the plan stale:409 plan_stale, and the plan is built again.
If a package stops declaring settings, its objects no longer read
settings, the settings routes answer 404 settings_not_declared, and the
history stays. How the plan and its application work is covered in
Installation and release.
For the administrator¶
The Settings screen¶
In the console, package settings are the Packages section of the Settings screen
(/console/settings): one item for each installed package that has settings. The
form is built from the package's schema and layout, with labels and hints in the
user's language. Each value shows whether it is the default, saved, or changed.
Save writes a new version under your name; core errors are shown next to the
fields. Change history shows the author, the time, and the changed fields of any
version, and you can restore it: the restore is written as a new version. Without
the packages.settings.manage permission the form opens read-only.
Permissions¶
| Permission | What it allows |
|---|---|
packages.settings.read |
the list of packages with settings, the settings of a package, and their history |
packages.settings.manage |
saving the settings of a package; canManage: true in the read response |
Both permissions are at the organization level and independent of
packages.plan: whoever changes thresholds does not have to be able to
install packages, and vice versa. The admin permission includes both. A
summary of permissions is in Permissions and scopes.
Routes¶
| Method | Path | Permission | What |
|---|---|---|---|
| GET | /api/v1/package-settings |
packages.settings.read |
the packages whose installed revision declares settings |
| GET | /api/v1/packages/{key}/settings |
packages.settings.read |
the schema, the layout, the saved and the effective values |
| PUT | /api/v1/packages/{key}/settings |
packages.settings.manage |
save the values as a whole |
| GET | /api/v1/packages/{key}/settings/versions |
packages.settings.read |
the version history, newest first |
All routes except the history accept ?locale=, the language of the response strings.
Reading¶
# packages that have settings
curl -s -H "Authorization: Bearer $TOKEN" \
"https://platform.example.com/api/v1/package-settings?locale=en"
# the settings of one package
curl -si -H "Authorization: Bearer $TOKEN" \
"https://platform.example.com/api/v1/packages/claims/settings?locale=en"
{
"package": "claims",
"title": "Customer claims",
"packageVersion": "0.2.0",
"schema": {"type": "object", "properties": {
"refundLimit": {"type": "number", "minimum": 0, "default": 500,
"title": "Refund without approval, up to", "description": "…"}}},
"uischema": {"type": "VerticalLayout", "elements": ["…"]},
"values": {"refundLimit": 800, "escalationRole": "<role-id>"},
"effective": {"refundLimit": 800, "replyDueWorkdays": 2, "escalationRole": "<role-id>"},
"version": 3,
"schemaHash": "sha256:…",
"updatedBy": "<principal-id>",
"updatedAt": "2026-10-03T09:00:00Z",
"canManage": true
}
| Field | What |
|---|---|
schema, uischema |
the schema and layout of the active revision; labels are filled in as title, description, label, text in the requested language |
values |
the saved values |
effective |
the effective values: values over default |
version |
the values version; 0 means nothing has been saved yet |
canManage |
whether the caller holds packages.settings.manage |
The response header ETag: "package-settings-<version>" is needed for
saving.
Saving¶
PUT saves the set of values as a whole: a field missing from the body
returns to its default value. If-Match is required: the version you read;
before the first save, "package-settings-0".
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'If-Match: "package-settings-3"' \
-d '{"values": {"refundLimit": 1000, "escalationRole": "<role-id>"}}' \
"https://platform.example.com/api/v1/packages/claims/settings"
The response is the new state in the same form as for GET, with a new
ETag. A body equal to the saved values creates neither a version nor an
event.
| Response | Code | What to do |
|---|---|---|
| 428 | if_match_required |
send If-Match |
| 400 | invalid_if_match, invalid_request |
If-Match has the wrong form; the body has something other than values |
| 404 | package_not_installed, settings_not_declared |
the package is not installed or declares no settings |
| 422 | secret_material_rejected |
a value holds secret material; details.errors[] gives the path and the kind of match, the value is not repeated |
| 422 | settings_invalid |
the values do not match the schema; details.errors[] gives the path and the code (the keyword), all errors at once |
| 422 | unknown_ref |
an x-ref references an object that does not exist or is not in use; details.errors[] gives the path and the ref |
| 409 | version_conflict |
someone saved first; details.currentVersion — read again and retry |
The schema check does not execute the uischema rules: a hidden field is
checked like any other.
History and restoring¶
curl -s -H "Authorization: Bearer $TOKEN" \
"https://platform.example.com/api/v1/packages/claims/settings/versions?limit=20"
{
"items": [
{"version": 3, "values": {"refundLimit": 800, "escalationRole": "<role-id>"},
"changedPaths": ["/refundLimit"], "updatedBy": "<principal-id>",
"updatedAt": "2026-10-03T09:00:00Z"}
],
"nextCursor": null
}
Versions go from newest to oldest; limit is 1 to 100 (50 by default); the
next page is ?cursor=<nextCursor>. There is no separate rollback route: to
restore a version, save its values with a regular PUT. The restore
becomes a new version, and the history is not rewritten.
The package.settings_changed event¶
Every save that creates a version writes the package.settings_changed event
to the journal. It carries no values, only what changed and who changed it:
| Payload field | What |
|---|---|
package |
the package key |
version, previousVersion |
the new and the previous values version (0 on the first save) |
schemaRevision |
the schema revision the values were checked against |
changedPaths |
the paths of the changed fields |
actorId |
who saved |
A package apply does not write the event. How to subscribe to the journal is
covered in Events.
Common problems¶
| Symptom | Cause | What to do |
|---|---|---|
settings_default_missing |
an optional field has no default |
give it a default or add the field to required |
settings_label_missing |
a label key is missing from the dictionary of one of the languages, or the package declares no locales |
add the key to i18n/<locale>.yaml; declare locales and defaultLocale |
settings_ref_unknown on a process |
the field is not declared, the path has a typo, or the process was not created by a package | declare the field in spec.settings or fix the path |
settings_ref_type |
the field type does not fit the place in the expression | change the field type or the expression: a due date needs an integer field |
plan: settings_incompatible |
a saved value does not match the new schema | save a fitting value and build the plan again |
PUT: 409 version_conflict |
the values were saved after you read them | read again, retry with the new If-Match |
PUT: 422 unknown_ref |
the role, principal, or workspace was deleted or disabled, or the task type has no active version | choose an object that is in use |
| a changed value did not affect a case | the case has already passed the step that reads the setting | expected: the value takes effect on subsequent computations |
See also¶
- Package anatomy: installation variables
- Package tests:
given.settingsand thesettingsstep - Scenarios and the core plan: the process scenario format and replay
- Process expressions: CEL variables
- Rules in a package
- Installation and release
- Package schema: the
spec.settingsreference - Events