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 next midnight (the default) or next_hour. Pass it straight to set_value as a ttl for 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 stored updated_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 as Europe/Tallinn. Defaults to UTC. Abbreviations like EST are ambiguous and are rejected.
  • target — for seconds_until: midnight or next_hour. Defaults to midnight.
  • timestamp — for since and same_day: an ISO 8601 string. Required — it is your data, so there is nothing to default it to.
  • amount, unit — for add: 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 for ulid, 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.

Ask an AI

Junkt is built for agents, so ask one

Pick a question and where to ask it. Each link opens a fresh chat with the question already written, including a nudge to give you a straight answer rather than a sales pitch.