Skip to content
The shared record

The shared record

A record is the one place a group of agents keeps what they know about a task. An incident, a support ticket, a pull request: each gets one record. The record has a fixed set of named fields. Each agent reads the fields it needs and writes the fields it owns. No agent talks to another agent, and no process hands out work.

kslate stores records in Postgres and puts a gateway in front of them. The gateway checks every write before it lands. It rejects a write in two cases: the write would overwrite something the writer never saw, or it acts on a decision that has since changed. The rejection says which.

    flowchart LR
  subgraph agents["your agents"]
    direction TB
    a1["diagnoser"]
    a2["remediator"]
    cli["kslate CLI"]
  end
  gw["gateway<br/>checks every write:<br/>identity · role · version · lease · evidence · based_on"]
  subgraph pg["Postgres"]
    direction TB
    rec["records: fields with versions"]
    hist["history: every write and rejection"]
    cp["checkpoints"]
  end
  cls["class (YAML or SharedContext)<br/>fields, modes, roles, TTL"]
  a1 -->|"HTTP · gRPC · MCP"| gw
  a2 -->|"HTTP · gRPC · MCP"| gw
  cli --> gw
  cls -.->|"defines"| gw
  gw --> rec
  gw --> hist
  gw --> cp
  wk["worker<br/>TTL · archive · lapsed leases · metrics"]
  wk --> rec
  

Your agents call the gateway. It checks each write against the class and the record’s current state, then stores the result. The class is a YAML file or a SharedContext resource.

Two processes touch Postgres and nothing else does. The gateway serves every request. The worker expires records, archives them, marks lapsed leases and reports metrics. They share no state beyond the database, and the worker is never in the path of a request: a lapsed lease is noticed when someone claims, not when the worker gets to it.

Everything kslate enforces is enforced in the gateway. The only checks in Postgres are the two compare-and-swaps, one on a field’s version and one on a lease. See Storage.

The gateway reaches identity and policy through two small contracts, so running on or off Kubernetes changes the plugin and nothing else:

ContractAnswersOff KubernetesOn Kubernetes
identity providerGiven this connection, who is calling, or reject itapikey, oidc, mtlsk8s-sa, through the TokenReview API
policy sourceWhat classes exist, and tell me when that changesa YAML file, re-read when it changesthe SharedContext resources, through a watch

Kubernetes adds to that picture rather than replacing it. The same binary serves the same six operations either way.

Classes, records and fields

A class is the template for a kind of record. It lists the fields, who may read and write each one, how long records live, and a few rules. You write a class as a YAML file. On Kubernetes it is a SharedContext resource with the same content. The incident fleet walks through a full class.

A record is one instance of a class, named <class>/<id>. For example, incidents/inc-4471. You choose the id. It usually comes from the system the record is about, such as an incident number.

A field is a named slot in the record. Every field has a mode that says how it may be written: versioned, soft, append or task. Field modes explains each one. Every field value also carries who wrote it, when, what evidence they cited, and what they based the decision on.

Every field has a version, a counter that goes up by one on each write. The record as a whole has a record_version that goes up on any write to any field. Agents use field versions to say which value they read. kslate uses record_version to work out what changed since a checkpoint.

Creating a record

There is no create call. The first write to a record that does not exist creates it. Reading a record that does not exist returns 404.

request
GET /v1/records/incidents/inc-4471
HTTP 404
{ "error": "NOT_FOUND", "record": "incidents/inc-4471" }

Write to it and it exists. The response is 201 instead of 200 to tell you the write created the record.

request
PUT /v1/records/incidents/inc-4471/fields/status
{"value":"open","expected_version":0}
HTTP 201
{ "record": "incidents/inc-4471", "field": "status", "version": 1, "record_version": 1 }

expected_version: 0 means “this field must not exist yet”. If two agents both try to create the same record, one gets 201 and the other gets a STALE rejection. Neither has to check first.

Reading a record

get returns the fields you are allowed to read, with their versions and metadata. Ask for specific fields with ?fields=.

request
GET /v1/records/incidents/inc-4471?fields=status,symptoms
HTTP 200
{
  "record": "incidents/inc-4471",
  "record_version": 5,
  "fields": {
    "status": {
      "value": "mitigating",
      "version": 2,
      "author": "apikey:detector",
      "updated_at": "2026-10-01T22:00:36.628631-07:00"
    },
    "symptoms": {
      "version": 1,
      "entries": [
        {
          "seq": 1,
          "value": "5xx rate on payments-api 12% since 14:00",
          "author": "apikey:detector",
          "evidence": "alertmanager PaymentsErrorBudgetBurn firing",
          "updated_at": "2026-10-01T22:00:36.531433-07:00"
        }
      ]
    }
  }
}

status is a versioned field: one value, one version. symptoms is an append field: a list of entries, and its version is the number of entries. The author on each value is the identity kslate verified on the connection. The writer cannot set it.

To find records, use query. It filters a class by field values and returns each matching record in the same shape.

GET /v1/records/incidents?status=open&limit=100

The six operations

Everything an agent does is one of these.

OperationWhat it does
getRead a record’s fields and their versions.
putWrite one versioned or soft field.
appendAdd one entry to an append field.
claimTake, renew or release a lease on a task field.
checkpointSave or load an agent’s progress on a record.
queryFind records in a class by field values.

The same six operations are available over HTTP/JSON, gRPC and MCP. The HTTP API reference has every route, body and error code.

What the gateway checks

Every write goes through the same checks:

  • Is the credential valid, and does this identity have a role in the class?
  • Does the field exist, and may this role write it?
  • Does the operation match the field’s mode?
  • For a task field: is it free, or held by this caller?
  • Is evidence present where the class requires it?
  • For a versioned field: does expected_version match the current version?
  • For any write with based_on: is every entry still current?

If any check fails, kslate rejects the write and returns an error that names the check. The order the gateway runs them in, with every error code, is on the HTTP API page. Scope and identity covers identity and role. Field modes covers the version check. based_on covers premises.

Why there is no leader

Nothing in kslate decides which agent runs or in what order. An agent acts when a field it cares about changes. It learns that by reading the record again, either itself or through the sidecar.

This works because every write is checked against the current state of the record inside one database transaction. Two agents cannot both update the same field from the same starting version. An agent cannot act on a decision another agent has already changed without being told.

What you get from this:

  • To add an agent, you add its fields and role to the class. Existing agents do not change.
  • If an agent dies, nothing it wrote is lost. Its replacement reads the record, claims the task when the lease runs out, and resumes from the last checkpoint.
  • Every write and every rejected write is recorded with the identity that made it. See blame.

What you do not get: kslate does not decide which of two agents is right. If their writes conflict, kslate reports the conflict to the second writer with the current value and who wrote it. The agent reads again and decides again. kslate also does not know whether a value is true. It only knows what was written.

Storage

kslate uses Postgres. Each write is one transaction: lock the record row, apply the write if the checks pass, append a history row. There is no cache in front of Postgres and no queue behind it. If Postgres is down, the gateway returns UNAVAILABLE and agents wait.