Claims and leases
A task field tracks which agent is working on something and until when.
You write it with claim, and only with claim. kslate stores the claim
in Postgres, so if the agent dies the claim stays until its lease runs
out. Then another agent can take the task over.
A class declares a task field like this:
fields:
remediation: { type: task, lease: 300s }
roles:
remediator: { identities: [sa:kagent/remediator], read: ["*"], write: [status, actions_taken], claim: [remediation] }claim is its own permission, separate from write. A role can be
allowed to claim a task without being allowed to put anything.
It is not separate from read. A role that claims a task field must
also be able to read it, and a class where one cannot is refused when it
loads. The fence below is why. A claimer proves it still holds the lease
by declaring the task field’s version as a premise, and declaring a
premise is a read. Without read scope, every write is rejected after the
agent has already run.
Granting it gives nothing away. A task field has no value, since you
cannot put to one. It holds only its owner, lease and version, which is
what claim and HELD already return to a role that may claim it.
Claim a task
Send claim. You can leave the body empty to use the class’s lease.
PUT /v1/records/incidents/inc-4471/fields/remediation/claim
{}{
"record": "incidents/inc-4471",
"field": "remediation",
"owner": "apikey:remediator",
"lease_expires_at": "2026-10-01T22:05:36.923106-07:00",
"version": 1,
"record_version": 15
}You now own the task until lease_expires_at. Pick a lease two to three
times longer than one step of your work, and renew it at a third of the
lease. The Go and Python clients renew at a third for you, measured from
the expiry the gateway granted rather than from the lease you asked for.
When someone else holds it
If another agent holds the task and its lease has not expired, you get
HELD with the owner and the expiry. Nothing waits and nothing queues.
{
"error": "HELD",
"field": "remediation",
"owner": "apikey:remediator",
"lease_expires_at": "2026-10-01T22:05:36.923106-07:00",
"version": 1
}If several agents claim the same task at once, exactly one wins. The rest
get HELD. Five workers claiming one task through a barrier:
worker-1: HELD owner=apikey:remediator/worker-5
worker-2: HELD owner=apikey:remediator/worker-5
worker-3: HELD owner=apikey:remediator/worker-5
worker-4: HELD owner=apikey:remediator/worker-5
worker-5: GOT IT owner=apikey:remediator/worker-5If you claim a task you already hold, that is fine: you get your claim back.
One identity, not one process
That last rule has a consequence worth stating on its own. A claim from the identity that already owns the task is a re-claim, not contention, so it succeeds.
Exclusion is per identity, not per process. Run ten copies of an agent that all present the same API key or the same ServiceAccount token, and all ten claim successfully, all ten believe they own the task, and all ten act on the record.
Give each copy its own identity. A role’s identities list takes a glob
in its final segment, so one role covers a fleet:
roles:
remediator: { identities: ["apikey:remediator/*"], read: ["*"], write: [actions_taken], claim: [remediation] }Workers then present remediator/worker-1, remediator/worker-2 and so
on, and the claim separates them. On Kubernetes the same applies to
ServiceAccounts: every pod of one Deployment shares one, so replicas of a
single Deployment do not compete for a task — see the kagent
guide.
Renew and release
To renew, send claim with renew: true. Your lease is extended from now.
PUT /v1/records/incidents/inc-4471/fields/remediation/claim
{"lease_seconds":300,"renew":true}{ "record": "incidents/inc-4471", "field": "remediation", "owner": "apikey:remediator",
"lease_expires_at": "2026-10-01T22:05:36.954272-07:00", "version": 2, "record_version": 16 }To release, send lease_seconds: 0. The task becomes free.
{ "record": "incidents/inc-4471", "field": "remediation", "version": 3, "record_version": 17 }If a renew comes back HELD, another agent took the task over while you
were working. Stop and throw away the result.
The sidecar and the Python run() loop both do three things:
- renew while your agent runs
- stop renewing before they write
- name the claim’s version in
based_onas a fence
A write after a lost lease is then refused with STALE_PREMISE.
Taking over a dead agent’s task
kslate checks the lease when someone claims, not on a timer. The moment a lease expires, the next claim by any allowed agent succeeds. The response says who was displaced.
PUT /v1/records/incidents/inc-4471/fields/remediation/claim
{"lease_seconds":300}{
"record": "incidents/inc-4471",
"field": "remediation",
"owner": "apikey:remediator",
"lease_expires_at": "2026-10-01T22:05:39.012433-07:00",
"version": 5,
"record_version": 19,
"took_over_from": "apikey:remediator2"
}History records this as a takeover. After taking over, load the
previous holder’s checkpoint before doing
anything, so you do not repeat work it already did.
See who holds what
get on a task field includes the owner and expiry:
"remediation": {
"version": 5,
"author": "apikey:remediator",
"updated_at": "2026-10-01T22:00:39.012433-07:00",
"owner": "apikey:remediator",
"lease_expires_at": "2026-10-01T22:05:39.012433-07:00"
}Or from the command line, with the connection flags left out as on the blame page:
$ kslate claims incidents/inc-4471
remediation apikey:remediator lease_expires_at=2026-10-01T22:05:39-07:00 v5 (held)Details
lease_secondsdefaults to the class’sleasefor the field. If the class has none, you must send it. The maximum is 86400 (one day).- Expiry is computed on the database’s clock inside the write transaction, so gateway replicas with different clocks agree.
- Claiming a record that does not exist creates it.
- A release by someone other than the holder, after the lease expired, is recorded as a takeover.
claimon a field that is not a task field isNOT_TASK_FIELD. A role withoutclaimpermission getsSCOPE_DENIED.- A writer can prove it still holds the claim when it writes another
field. It puts the task field and the version its claim returned in
based_onwithkind: fence. If the lease has moved, the gateway refuses the write withSTALE_PREMISE.kslate-reconcilerand the Pythonrun()do this on every write. See based_on.
What a claim does not do
- It does not assign work. An agent finds out a task is free by claiming it.
- It does not notify anyone when a lease expires.
- It does not lock the record. Other agents keep writing other fields while a task is held.
- It does not recover unwritten work. If an agent dies holding a task, anything it did after its last checkpoint is unknown, unless it wrote it to an append field.