Skip to content
Field modes

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.

ModeHow you write itWhat kslate checksUse it for
appendappend adds an entryEvidence is present. Anything in based_on is still current.Findings, hypotheses, actions: things that accumulate.
versionedput with the version you readThe 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.
softput, no versionType. Evidence if required. based_on still current.Scratch context nobody acts on.
taskclaim onlyThe 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.

request
POST /v1/records/incidents/inc-4471/fields/symptoms/entries
{"entry":"5xx rate on payments-api 12% since 14:00","evidence":"alertmanager PaymentsErrorBudgetBurn firing"}
HTTP 200
{ "record": "incidents/inc-4471", "field": "symptoms", "seq": 1, "record_version": 4 }

Evidence is required on every entry. Without it:

HTTP 422
{ "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_on entry for an append field uses that number.
  • seq in the response is not a position in the field. It is a number that increases with every entry written anywhere, so use version when 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.

request
PUT /v1/records/incidents/inc-4471/fields/status
{"value":"resolved","expected_version":1}
HTTP 409
{
  "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_version is required. Without it the write is MALFORMED.

  • 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 and diff is empty. current_version is always present.

  • diff is 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.

request
PUT /v1/records/incidents/inc-4471/fields/affected_service
{"value":"payments-db"}
HTTP 200
{ "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:

HTTP 422
{ "error": "NOT_WRITABLE", "field": "symptoms",
  "message": "put is only implemented for soft and versioned fields", "record": "incidents/inc-4471" }
HTTP 422
{ "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.