Documentation
Tool reference
Five tools for your keys, one for the controls you set, four more for what a model cannot work out on its own. Each one is scoped to the authenticated account automatically — there is no store or namespace parameter to pass.
list_keys
List the keys stored for this account. Read-only. Returns metadata, never values, so an agent can discover what state exists before deciding what to read.
Parameters
prefix— optional string. Only return keys starting with it. Omit to list everything.
Returns
{
"keys": [
{ "key": "cowork:daily-digest:last_run",
"type": "value",
"updated_at": "2026-05-31T09:00:00+00:00",
"expires_at": null,
"size": 2 }
],
"total": 1,
"controls": {
"keys": ["control:paused"],
"total": 1,
"read_with": "list_controls"
}
}
Expired keys are excluded, and the total counts only what you store, so it can be read straight against your key limit. An empty keyspace returns an empty array and a total of zero, not an error.
The controls block names any controls you
have set. It is here because a control an agent never learns about does nothing, and
an agent that only ever calls list_keys is the likeliest way for that to
happen. Names only, matching what this tool promises about keys —
list_controls returns the values.
get_value
Read a single key. Read-only. A missing key is not an error — it comes back with
exists: false so the model can branch cleanly instead of handling a
failure.
Parameters
key— required string.
Returns
{
"exists": true,
"key": "cowork:daily-digest:last_run",
"value": "ok",
"type": "value",
"created_at": "2026-05-30T09:00:00+00:00",
"updated_at": "2026-05-31T09:00:00+00:00",
"expires_at": null
}
// or
{ "exists": false, "key": "cowork:daily-digest:last_run" }
Worth knowing: for “when did this last run” checks you do
not need to store a timestamp at all. Call set_value on the key each run
and read updated_at back. Junkt keeps the time for you.
set_value
Create or overwrite a key. If it already exists as a value, it is replaced. Not idempotent, and marked as such so clients can ask before calling it.
Parameters
key— required string, up to 128 characters.value— required string, up to 2048 bytes. Store JSON as a string if you need structure.ttl— optional integer, seconds, minimum 1. Omit for a key that never expires.
Returns
{
"key": "deploys:staging:cooldown",
"value": "held",
"created": true,
"updated_at": "2026-05-31T09:00:00+00:00",
"expires_at": "2026-05-31T10:00:00+00:00"
}
created distinguishes a new key from an overwrite. Passing ttl
on an existing key rewrites its expiry; omitting it clears any expiry the key had.
increment
Atomically add to a counter and return its new total. Creates the counter starting from zero if it does not exist. This is the tool to reach for whenever you would otherwise read a number, add one, and write it back — that sequence races between concurrent runs and quietly loses counts.
Parameters
key— required string.amount— optional integer, defaults to 1. Negative values decrement.
Returns
{
"key": "cowork:daily-digest:runs",
"value": 7,
"created": false,
"updated_at": "2026-05-31T09:00:00+00:00"
}
A counter created here can only be changed here. set_value will not
overwrite it.
delete_key
Remove a key and its value. Idempotent — deleting something that does not exist is
fine and reports deleted: false.
Parameters
key— required string.
Returns
{ "key": "demo:hello", "deleted": true }
list_controls
Read the controls you have set. Read-only, and the only tool that touches them —
there is no set_control, on purpose. Every other tool here writes what
your agent chose to remember. This one reads what you decided.
It takes no parameters. An account has at most ten controls and they are tiny, so values and descriptions come back with the list rather than costing a call each. Making an agent pay per control would only discourage it from looking, and a control nothing checks does nothing.
Returns
{
"controls": [
{ "key": "control:paused",
"type": "boolean",
"value": false,
"options": null,
"description": "If true, stop and do nothing this run." },
{ "key": "control:mode",
"type": "enum",
"value": "conservative",
"options": ["conservative", "normal", "aggressive"],
"description": "How much to attempt without asking me first." }
],
"total": 2
}
Values arrive cast to their type, so false is a boolean and not the
string "false" — which is truthy, and is exactly the sort of thing
that turns a pause into a no-op. An enum reports its options so the agent
knows the closed set it is choosing between.
description is the field that does the work. A flag named
paused tells an agent nothing on its own; the description is where you
say what to do about it, and it is read as an instruction rather than as data.
Worth knowing: list_keys names your controls alongside
your keys, so an agent that only ever calls that one still discovers they exist. It
reports names only — the values come from here.
time
Everything that turns on what time it actually is. A model has no clock, so a date it
infers from context is a guess — and a wrong one is invisible to everything
downstream. Set kind to choose a calculation.
now— the current moment, with the calendar date and weekday already worked out.seconds_until— whole seconds to the nextmidnight(the default) ornext_hour. Pass it straight toset_valueas attlfor a key that clears itself at the boundary.since— elapsed time from a timestamp you supply, for “how long ago did this last run?” against a storedupdated_at. Negative if the timestamp is in the future.same_day— whether a timestamp falls on today’s date in a given timezone. This is the “has this already run today?” check.add— the time an interval from now, handling month lengths and daylight saving.
Parameters
kind— required string, one of the kinds above.timezone— optional IANA identifier such asEurope/Tallinn. Defaults to UTC. Abbreviations likeESTare ambiguous and are rejected.target— forseconds_until:midnightornext_hour. Defaults tomidnight.timestamp— forsinceandsame_day: an ISO 8601 string. Required — it is your data, so there is nothing to default it to.amount,unit— foradd: a whole number (negative goes backwards) and one of seconds, minutes, hours, days, weeks, months, years. The unit defaults to days; the amount is required.
So time with nothing but kind: "now" is a complete call, and
so is kind: "seconds_until".
Returns
{
"kind": "now",
"arguments": { "timezone": "Europe/Tallinn" },
"iso": "2026-08-17T00:30:00+03:00",
"date": "2026-08-17",
"time": "00:30:00",
"weekday": "Monday",
"offset": "+03:00",
"unix": 1786915800,
"utc": "2026-08-16T21:30:00+00:00"
}
The arithmetic is the point. A day is not always 86,400 seconds — on a daylight-saving change it is an hour shorter or longer — one month after 31 January is 28 February rather than 3 March, and two timestamps sharing a UTC date can fall on different local days. Each of those is somewhere hand-rolled date maths goes quietly wrong.
random_number, random_token, random_choice
Randomness generated server-side, because
a model cannot produce it. Each tool takes a
kind naming the generator to run, plus the arguments that generator needs.
The full list of kinds is in each tool's description, which your client already has.
random_number— int, float, bool, percent, normal, jitter.random_token— uuid, ulid, nanoid, hex, pin, slug, passphrase, color, emoji.random_choice— pick, shuffle, sample, weighted.
Parameters
kind— required string, one of the kinds above.seed— optional string. The same seed and arguments always return the same result, which is how you bucket an account or assign consistently between runs. Not available forulid, which is time-based and so can never be reproduced.times— optional integer. Return that many results as an array instead of one, so a batch costs a single request.
Every generator argument that can have a sensible default has one, and each tool's
description states it: int rolls 1–100, hex returns 16
bytes, pin six digits, jitter spreads over a minute. Only the
arguments that are your own data — the list to pick from, its weights — are ever
required, so most calls are a kind and nothing else.
Returns
{ "kind": "int", "arguments": { "min": 1, "max": 100 }, "seed": null, "value": 42 }
A generated value is not a key. It is never stored and cannot be read back, so pass it
to set_value if you want to keep it.
Errors
Every error message is written to be read by a model rather than by a log parser, so the agent can usually recover without asking you. These are the exact strings:
| When | Message |
|---|---|
| Key too long | The key is too long. Keys may be at most 128 characters. |
| Value too large | The value is too large. Values may be at most 2048 bytes. Store large data elsewhere and keep only a reference here. |
| Key limit reached | This account has reached its limit of 25 keys. Delete an existing key before creating a new one. |
| Overwriting a counter | The key “…” is a counter. Use the increment tool to change it; it cannot be overwritten with set_value. |
| Incrementing a value | The key “…” holds a value, not a counter, so it cannot be incremented. Delete it first if you want to use it as a counter. |
Writing to control: |
The “control” namespace holds controls, which are set by the account owner and are read-only to you. Read them with list_controls; you cannot create, change or delete them. |
The reserved namespace
One prefix is not your agent’s to write. control: is where your
controls live, and set_value,
increment and delete_key all refuse it. That refusal is the
entire mechanism: a pause flag an agent can clear is not a pause flag.
It is the same prefix an agent reads a control under, which is deliberate. In a flat
keyspace, control:paused is legible at a glance as something the account
owner put there — and it is exactly the name the error message tells the agent it
cannot write.
The refusal message names list_controls, because an agent that has just
tried to change a control is one step from doing the right thing with it instead.