> ## Documentation Index
> Fetch the complete documentation index at: https://empirical.run/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# KV Store

> Share state across tests, shards, test runs and agent sessions

Empirical gives every project a key-value store. Tests, shards of the same
run, later test runs and Empirical's agents all read and write the same
entries, so you can hand data from one place to another without a database of
your own.

Every entry expires, so the store cleans up after itself. Conditional writes
let parallel shards coordinate safely: only one of them can create a key, and
a write can be made to apply only if nothing changed since you read it.

All test repositories managed by Empirical have this built-in — no additional
setup is required.

## When to use it

* **Verify background work in a later run.** A test that triggers a slow job
  (an analytics import, a nightly export) can record what it started and exit.
  A test in the next run reads the key and checks the result, instead of
  waiting minutes inside one test.
* **Share data between tests and shards.** Store an ID, a token or a URL
  created by one test and read it from another, even on a different shard.
* **Coordinate parallel work.** Playwright's `lock` option holds across shards
  on Empirical. For anything else, use a conditional write to make sure only one
  shard sets something up, or to hold a short lease on a shared resource.
* **Track resources to clean up.** Record environments or accounts your tests
  create, with an expiry, so a later test or agent can find and remove them.

## Usage

### Read and write values

Use the `kv` fixture in tests, or `KVClient` anywhere else. `ttl` is required
and is in seconds.

```ts theme={null}
import { test } from "./fixtures";

test("starts an export", async ({ page, kv }) => {
  const exportId = await startExport(page);
  await kv.set(`exports:pending:${exportId}`, { startedAt: Date.now() }, 86400);
});
```

```ts theme={null}
import { KVClient } from "@empiricalrun/playwright-utils";

const kv = new KVClient();
const pending = await kv.get<{ startedAt: number }>("exports:pending:42");
// null if the key does not exist or has expired
```

### Check on work from a previous run

`entries` iterates every entry under a key prefix, across pages.

```ts theme={null}
test("exports started earlier have completed", async ({ kv }) => {
  for await (const entry of kv.entries("exports:pending:")) {
    const exportId = entry.key.split(":").at(-1)!;
    await expect.poll(() => getExportStatus(exportId)).toBe("done");
    await kv.delete(entry.key);
  }
});
```

### Conditional writes

`set` takes `ifAbsent` to write only when the key does not exist, or
`ifVersion` to write only when the entry has not changed since you read it.
When the condition fails, `set` throws `KvPreconditionFailedError`, and its
`current` property holds the entry as it is now.

For example, to let only one shard seed shared data:

```ts theme={null}
import { KvPreconditionFailedError } from "@empiricalrun/playwright-utils";

const claimed = await kv
  .set("setup:shared-org", { claimedBy: testInfo.testId }, 3600, {
    ifAbsent: true,
  })
  .then(() => true)
  .catch((error) => {
    if (error instanceof KvPreconditionFailedError) {
      return false;
    }
    throw error;
  });

if (claimed) {
  await seedSharedOrg();
}
```

Every write increases the entry's `version`, and every write returns the new
entry. Pass its `version` to `set`, `refresh` or `delete` to act only if no one
else has written since:

```ts theme={null}
const lease = await kv.set("lease:payments-sandbox", testInfo.testId, 60, {
  ifAbsent: true,
});
const renewed = await kv.refresh("lease:payments-sandbox", 60, {
  ifVersion: lease.version,
});
await kv.delete("lease:payments-sandbox", { ifVersion: renewed!.version });
```

### Methods

| Method | Returns | Description |
| - | - | - |
| `get(key)` | value or `null` | Reads a value |
| `getEntry(key)` | entry or `null` | Reads a value with its `version`, timestamps and who wrote it |
| `set(key, value, ttl, options?)` | entry | Creates or replaces an entry; `ifAbsent` or `ifVersion` make it conditional |
| `refresh(key, ttl, options?)` | entry or `null` | Resets the expiry without resending the value |
| `delete(key, options?)` | `boolean` | Deletes an entry; `false` if it did not exist |
| `list({ prefix, cursor, limit })` | page | Lists entry metadata in key order |
| `entries(prefix)` | async iterator | Iterates every entry under a prefix, with values |

## Locks across shards

Playwright's [test locks](https://playwright.dev/docs/test-parallel#test-locks)
keep tests that share a lock name from running at the same time, but only
within one shard. On Empirical, the same `lock` option also holds across every
shard of a test run, with no code changes:

```ts theme={null}
test("updates billing settings", { lock: "billing-account" }, async ({ page }) => {
  // never runs at the same time as another test with the "billing-account"
  // lock, on any shard of this test run
});
```

Each locked test holds a short lease in the KV store while it runs, renews it
until the test finishes, and releases it afterwards. If a shard dies, its lease
expires within a minute. Time a test spent waiting shows up in the test report
as a `lock` annotation.

Locks are scoped to one test run: two separate runs do not wait for each other.
Cross-shard locks need Playwright 1.63 or later and the
`@empiricalrun/playwright-utils` fixtures.

## Keys, values and limits

| | Limit |
| - | - |
| Key | 1-512 characters from `A-Z a-z 0-9 . _ : -` |
| Value | Any JSON value, up to 1 MB serialized |
| TTL | Required, 1 second to 30 days |

Use `:` to namespace keys, for example `exports:pending:42`, so related entries
share a prefix you can list. Entries are scoped to your project; values are
visible to anyone with access to it, so do not store secrets.

## API

The store is also available over the [Empirical API](https://empirical.run/docs/api-reference/introduction.md)
under `/api/kv`, for scripts, CI jobs and agents: list, get, put, refresh and
delete, with `If-None-Match: *` and `If-Match: <version>` for conditional
writes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.