Field modes
Every field in a class has a mode. The mode decides how the field may be written and what kslate checks before it accepts the write. There are four modes.
| Mode | How you write it | What kslate checks | Use it for |
|---|---|---|---|
append | append adds an entry | Evidence is present. Anything in based_on is still current. | Findings, hypotheses, actions: things that accumulate. |
versioned | put with the version you read | The version matches. The value fits the type and enum. Evidence if required. based_on still current. | One current value: a status, a plan, an owner. |
soft | put, no version | Type. Evidence if required. based_on still current. | Scratch context nobody acts on. |
task | claim only | The lease is free, expired, or yours. | Who is working on something. |
You set the mode in the class:
fields:
status: { type: string, mode: versioned, enum: [open, mitigating, resolved] }
affected_service:{ type: string, mode: soft }
symptoms: { type: append }
planned_action: { type: string, mode: versioned, evidence: required }
remediation: { type: task, lease: 300s }append and task are types on their own. The other modes take a value
type: string, number, integer, boolean, object or array. The
type is optional, and a field without one accepts any JSON value.
Which mode to use
Start with append. When an agent changes its mind, it adds a new entry
with new evidence. The old entry stays. Two agents that disagree both get
their entry recorded, in order, with their names on it.
Use versioned only when exactly one value can be true at a time. The
status of an incident is one value. The current plan is one value. Here
the version check matters: it stops one agent from replacing a value
another agent just wrote.
Use soft for notes nobody acts on. Use task for anything one agent
should work on at a time.
A quick test: if two agents could both be right about this field, make it
append.
append
Each entry records who wrote it, when, what evidence they cited, and what they based it on. Entries are never changed or removed.
POST /v1/records/incidents/inc-4471/fields/symptoms/entries
{"entry":"5xx rate on payments-api 12% since 14:00","evidence":"alertmanager PaymentsErrorBudgetBurn firing"}{ "record": "incidents/inc-4471", "field": "symptoms", "seq": 1, "record_version": 4 }Evidence is required on every entry. Without it:
{ "error": "EVIDENCE_REQUIRED", "field": "symptoms", "record": "incidents/inc-4471" }kslate checks that evidence is present. It does not check that it is true.
Details:
- The field’s version is its entry count. A
based_onentry for an append field uses that number. seqin the response is not a position in the field. It is a number that increases with every entry written anywhere, so useversionwhen you mean “how many entries does this field have”.
versioned
When you put a versioned field, you send the version you read. If the
field has changed since, kslate rejects the write.
PUT /v1/records/incidents/inc-4471/fields/status
{"value":"resolved","expected_version":1}{
"error": "STALE",
"field": "status",
"your_version": 1,
"current_version": 2,
"current_value": "mitigating",
"current_author": "apikey:detector",
"current_evidence": null,
"current_updated_at": "2026-10-01T22:00:36.628631-07:00",
"diff": "--- current\n+++ proposed\n-\"mitigating\"\n+\"resolved\"\n"
}The field was at version 2, not 1. The response tells you the current
value, who wrote it and when. You read it, decide again, and write with
expected_version: 2. kslate does not retry for you and does not merge.
expected_version: 0 means the field must not exist yet. Use it to create
a record safely.
Details:
expected_versionis required. Without it the write isMALFORMED.If the class gives the field an
enum, a value outside it is rejected with the allowed values in the message:HTTP 400{ "error": "MALFORMED", "field": "status", "message": "value must be one of the field's enum: open, mitigating, resolved", "record": "incidents/inc-4471" }If your role may write the field but not read it, the
current_*keys are null anddiffis empty.current_versionis always present.diffis replaced by a note when either value is over 500 lines or 64 KiB.
soft
Last write wins. You do not send a version. kslate still records who wrote it and still returns a version.
PUT /v1/records/incidents/inc-4471/fields/affected_service
{"value":"payments-db"}{ "record": "incidents/inc-4471", "field": "affected_service", "version": 2, "record_version": 3 }A soft field can still require evidence and can still carry based_on.
It can be rejected with STALE_PREMISE, but never with STALE.
task
A task field says who is working on something and until when. You write
it only with claim. See Claims and leases.
A put on a task field is rejected. Most roles see SCOPE_DENIED,
because the scope check runs before the mode check and a task field is
rarely in anyone’s write list.
Wrong operation for the mode
If you use the wrong call for a field’s mode, kslate tells you:
{ "error": "NOT_WRITABLE", "field": "symptoms",
"message": "put is only implemented for soft and versioned fields", "record": "incidents/inc-4471" }{ "error": "NOT_APPEND_FIELD", "field": "status",
"message": "append is only valid on append fields", "record": "incidents/inc-4471" }claim on a field that is not a task field is NOT_TASK_FIELD.
Changing a class later
Changes to a class apply to new writes right away. Existing records are
not migrated. A field you add is empty on old records until something
writes it. A field you remove stays readable but rejects writes with
UNKNOWN_FIELD.