Custom Fields Reference
July 11, 2026 · View on GitHub
Table of contents
- Overview
- Registration and loading
- Naming constraints
- Type coercion rules
- Enum domain isolation
- Validation rules
- Persistence and round-trip behavior
- Schema evolution and stale data
- Template defaults
- Query behavior
- Missing-field semantics
Overview
Workflow fields extend tiki's system field catalog with fields declared in workflow.yaml. This reference
covers the precise rules for how they are loaded, validated, persisted, and queried — the behavioral contract
behind the Custom Fields user guide.
Registration and loading
Custom field definitions come from the single highest-priority workflow.yaml (see
Configuration: Precedence). A missing fields: section means no custom fields are
registered.
Fields are sorted by name for deterministic ordering and registered into the field catalog alongside system fields. Once registered, workflow fields are available for ruki parsing, validation, and execution.
Registration happens during bootstrap before any task or template loading occurs.
Naming constraints
Field names must:
- match the ruki identifier pattern (letters, digits, underscores; must start with a letter)
- not collide with ruki reserved keywords (
select,update,where,and,or,not,in,is,empty,order,by,asc,desc,set,create,delete,limit, etc.) - not collide with reserved system field names, case-insensitively (
id,title,description,createdBy,createdAt,updatedAt,filepath, or thebodyalias) - not be
trueorfalse(reserved boolean literals)
Collision checks against system fields are case-insensitive: Title and TITLE both collide with title.
Type coercion rules
When custom field values are read from frontmatter YAML, they are coerced to the expected type:
| Field type | Accepted YAML values | Coercion behavior |
|---|---|---|
text | string | pass-through |
user | string | pass-through; suggestions are UI-only |
enum | string | case-insensitive match; canonical casing |
integer | integer or decimal number | decimals accepted only for whole numbers |
boolean | true / false | pass-through |
datetime | timestamp or date string | timestamp pass-through; strings parsed as dates |
stringList | YAML list of strings | strings only; trim, drop empty, dedupe |
tikiIdList | YAML list of strings | uppercase IDs; trim, drop empty, dedupe |
Enum domain isolation
Each enum field maintains its own independent set of allowed values. Two enum fields never share a domain, even if their values happen to overlap.
This isolation is enforced at three levels:
- assignment:
set severity = categoryis rejected even if both are enum fields - comparison:
where severity = categoryis rejected (comparing different enum domains) - in-expression: string literals in an
inlist are validated against the specific enum field's allowed values
Enum comparison is case-insensitive: category = "Backend" matches a stored "backend".
Validation rules
Workflow fields follow one validation pipeline:
- type compatibility: assignments and comparisons are type-checked (e.g. you cannot assign a string to an integer field, or compare an enum field with an integer literal)
- enum value validation: string literals assigned to or compared against an enum field must be in that field's allowed values
- reference validation: every
tikiIdListentry must be a bare ID that resolves to an existing loaded document - ordering: custom fields of orderable types (
text,user,integer,boolean,datetime,enum) can appear inorder byclauses; list types (stringList,tikiIdList) are not orderable
Persistence and round-trip behavior
Custom fields are stored in task file frontmatter alongside built-in fields. When a task is saved:
- custom fields appear after built-in fields, sorted alphabetically by name
- values that look ambiguous in YAML (e.g. a text or user field containing
"true","42", or"2026-05-15") are quoted to prevent YAML type coercion from corrupting them on reload
A save-then-load cycle preserves custom field values exactly. This holds as long as:
- the field definitions in
workflow.yamlhave not changed between save and load - enum values use canonical casing (enforced automatically by coercion)
- timestamps round-trip through RFC3339 format
Schema evolution and stale data
When workflow.yaml changes — fields renamed, enum values added or removed, field types changed — existing task
files may contain values that no longer match the current schema. tiki handles this gracefully:
Removed fields
If a frontmatter key no longer matches any registered custom field, it is preserved as an unknown field. Unknown fields survive load-save round-trips: they are written back to the file exactly as found. This allows manual cleanup or re-registration without data loss.
Stale enum values
If a task file contains an enum value that is no longer in the field's allowed values list (e.g.
severity: critical after critical was removed from the enum), the value is demoted to an unknown field
with a warning. The task still loads and remains visible in views. The stale value is preserved in the file for
repair.
Type mismatches
If a value cannot be coerced to the field's current type (e.g. a text value "not_a_number" in a field that was
changed to integer), the same demotion-to-unknown behavior applies: the task loads, the value is preserved,
a warning is logged.
General principle
tiki reads leniently and writes strictly. On load, unrecognized or incompatible values are preserved rather than rejected. On save, values are validated against the current schema.
Field defaults
Custom fields can declare a default: value in workflow.yaml. Default values are validated
against the field's type and enum constraints during workflow load — invalid defaults are hard
errors. Valid defaults are copied into new tasks created via create statements or the
new-task UI flow. Fields without a default: key start empty.
user defaults follow the same rules as text: the value must be a string, and no identity lookup or
candidate validation is performed.
Query behavior
Custom fields behave identically to built-in fields in ruki queries:
- usable in
where,order by,set,create, andselectfield lists - support
is empty/is not emptychecks - support
in/not infor list membership - list-type fields support
+(set union) and-(remove) operations - quantifiers (
any ... where,all ... where) work on customtikiIdListfields
Unset list fields
Unset custom list fields (stringList, tikiIdList) are absent — has(labels) is false until the
key is written. For most operations they behave as if empty:
"x" in labelsevaluates tofalse(not an error)labels + ["new"]produces["new"](not an error); the assignment then writeslabels: [new]to frontmatter, after whichhas(labels)becomes truelabels is emptyevaluates totrue(absent is treated as empty)labels = emptyevaluates totrue
But labels = [] (equality with a concrete empty list literal) is false on absent fields, since
that is a comparison against a value, not against empty. List arithmetic also does not delete
the key when the result is empty — set labels = labels - ["only-tag"] writes labels: [] and
leaves has(labels) true. Use set labels = empty to actually remove the key.
Missing-field semantics
Custom fields are presence-aware — a field is either present (its key appears in the tiki's frontmatter) or absent. Comparisons against absent custom fields follow the same rules as workflow-declared absent fields; see Absent fields in semantics.md for the full set. The key facts for custom fields:
where <field> = <concrete-value>is false on absent fields.blocked = falsedoes not match tikis that never setblocked; only an explicitblocked: falsematches.where <field> != <concrete-value>is true on absent fields.blocked != truematches both "explicitly false" and "never set".where <field> is emptyandwhere <field> = emptyare true on absent fields (the empty literal treats absent as empty).where has(<field>)is the only predicate that distinguishes "key present" from "key absent", regardless of value.
Setting a field to empty deletes it from frontmatter, so subsequent loads see it as absent.
Distinguishability by type
Enum fields preserve the missing-vs-set distinction even through is empty: "" is not a valid
enum member, so the only way category is empty is true is if the field is absent (or was cleared
to empty). A tiki with category = "frontend" is never empty.
Boolean and integer fields do not preserve the distinction through is empty: false and 0
are zero values, so an explicit blocked: false and an absent blocked both satisfy is empty. To
tell "never set" from "explicitly false" or "explicitly zero", use has(blocked) — or model the
field as an enum with named values (e.g. yes / no).
Worked examples
Suppose blocked is a custom boolean field:
| Query | never set | false | true |
|---|---|---|---|
select where blocked = true | no | no | yes |
select where blocked = false | no | yes | no |
select where blocked != false | yes | no | yes |
select where blocked is empty | yes | yes | no |
select where blocked is not empty | no | no | yes |
select where has(blocked) | no | yes | yes |
select where not has(blocked) | yes | no | no |
Suppose category is a custom enum field with values [frontend, backend, infra]:
| Query | never set | "frontend" |
|---|---|---|
select where category = "frontend" | no | yes |
select where category is empty | yes | no |
select where category is not empty | no | yes |