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:
| Contract | Answers | Off Kubernetes | On Kubernetes |
|---|---|---|---|
| identity provider | Given this connection, who is calling, or reject it | apikey, oidc, mtls | k8s-sa, through the TokenReview API |
| policy source | What classes exist, and tell me when that changes | a YAML file, re-read when it changes | the 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.
GET /v1/records/incidents/inc-4471{ "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.
PUT /v1/records/incidents/inc-4471/fields/status
{"value":"open","expected_version":0}{ "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=.
GET /v1/records/incidents/inc-4471?fields=status,symptoms{
"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=100The six operations
Everything an agent does is one of these.
| Operation | What it does |
|---|---|
get | Read a record’s fields and their versions. |
put | Write one versioned or soft field. |
append | Add one entry to an append field. |
claim | Take, renew or release a lease on a task field. |
checkpoint | Save or load an agent’s progress on a record. |
query | Find 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
taskfield: is it free, or held by this caller? - Is evidence present where the class requires it?
- For a
versionedfield: doesexpected_versionmatch 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.