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

# KV Store

> Persistent key-value storage with TTL, namespaces and pattern search

The **KV Store** is a persistent key-value database built into the platform. Use it to share state across executions, sessions and agents — shopping carts, feature flags, counters, idempotency keys — without standing up your own Redis or database.

* **Tenant-scoped** — all agents in your workspace read and write the same store
* **Namespaces** — created implicitly on first write, no setup required
* **TTL** — optional per-key expiration in seconds
* **Pattern search** — find keys with glob patterns like `cart:*:items`
* **Any JSON value** — objects, arrays, strings, numbers, booleans (up to 256 KB per entry)

## KV Store vs Memory

The KV Store holds **business state**; [Memory](/core-concepts/memory) holds **conversation context**. A value you `set()` comes back exactly as stored, for as long as you need it — while Memory content is trimmed and summarized as the conversation grows.

| Need                                                                  | Use          |
| --------------------------------------------------------------------- | ------------ |
| Cart contents, feature flags, counters, preferences, idempotency keys | **KV Store** |
| Message history, conversation summaries, session status, follow-ups   | **Memory**   |

If you were persisting data by stuffing it into sessions or message history, the KV Store replaces that workaround. See [Abandoned Cart Recovery](/use-cases/abandoned-cart-recovery) for both working together — cart in KV, dialogue in Memory.

## Quick Start

```typescript theme={null}
import { KV } from '@runflow-ai/sdk';

// Namespace dedicated to a domain (created automatically on first set)
const carts = KV.namespace('carts');

// Set a value with a 1-hour TTL
await carts.set('cart:5511999:items', { items: ['sku-1', 'sku-2'] }, { ttl: 3600 });

// Read it back (null when missing or expired)
const cart = await carts.get('cart:5511999:items');

// Find keys by pattern
const keys = await carts.keys('cart:*:items');

// Delete when done
await carts.delete('cart:5511999:items');
```

Also available as a dedicated entry point: `import { KV } from '@runflow-ai/sdk/kv'`.

## Static Shortcuts

For one-off reads and writes, the static methods use the `default` namespace:

```typescript theme={null}
import { KV } from '@runflow-ai/sdk';

await KV.set('flag:new-checkout', true);
const enabled = await KV.get('flag:new-checkout');

await KV.has('flag:new-checkout');   // true
await KV.delete('flag:new-checkout');
```

## Namespaces

Namespaces isolate keys by domain. They are **implicit**: a namespace exists as soon as its first key is written, and disappears when its last key is deleted.

```typescript theme={null}
const carts = KV.namespace('carts');
const flags = KV.namespace('feature-flags');

// Same key, different namespaces — no collision
await carts.set('config', { maxItems: 50 });
await flags.set('config', { rollout: 0.25 });

// List every namespace in the tenant with key counts
const all = await KV.namespaces();
// [{ namespace: 'carts', keys: 12 }, { namespace: 'feature-flags', keys: 3 }]

// Delete every key in a namespace
const removed = await carts.clear(); // returns number of deleted keys
```

Namespace names accept letters, digits, `.`, `_` and `-` (max 128 characters, must start with a letter or digit).

## TTL and Expiration

Pass `ttl` (in seconds) to make an entry expire automatically. Expired keys behave exactly like missing keys — `get()` returns `null`, `has()` returns `false`, and listings skip them.

```typescript theme={null}
// Expires in 30 minutes
await carts.set('cart:5511999:items', cart, { ttl: 1800 });

// No ttl = persistent
await carts.set('cart:5511999:address', address);
```

<Note>
  Setting a key **without** `ttl` clears any previous TTL — the entry becomes persistent. To keep a key expiring, pass `ttl` on every write.
</Note>

To inspect the expiration of a key, use `getEntry()`:

```typescript theme={null}
const entry = await carts.getEntry('cart:5511999:items');
// { key, value, expiresAt: '2026-07-12T18:30:00.000Z', createdAt, updatedAt }
// null when missing or expired
```

## Pattern Search

`keys()` and `getAll()` accept a glob pattern: `*` matches any sequence of characters, `?` matches exactly one.

```typescript theme={null}
const carts = KV.namespace('carts');

// All item lists, any customer
await carts.keys('cart:*:items');
// ['cart:5511888:items', 'cart:5511999:items']

// Keys and values together
const entries = await carts.getAll('cart:5511999:*');
// [{ key: 'cart:5511999:items', value: {...}, expiresAt, ... }, ...]

// No pattern = everything in the namespace
const everything = await carts.getAll();
```

<Tip>
  Structure your keys with a consistent separator (`entity:id:field`) so patterns stay predictable — `cart:*:items`, `user:*:profile`, `order:2026-07-*`.
</Tip>

## Pagination

Listings return up to 100 items by default (max 1000). For large namespaces, use `listKeys()` / `listEntries()`, which also return the total count:

```typescript theme={null}
const page = await carts.listEntries({ pattern: 'cart:*', limit: 50, offset: 100 });
// { items: KvEntry[], total: 1240 }

const keyPage = await carts.listKeys({ limit: 200 });
// { keys: [{ key, expiresAt, updatedAt }], total: 1240 }
```

## Common Patterns

### Cart / conversation state with TTL

```typescript theme={null}
const carts = KV.namespace('carts');

// Inside a tool: persist partial state that survives the session
await carts.set(`cart:${phone}:items`, items, { ttl: 24 * 3600 });

// Next conversation, even days later on a new session:
const items = await carts.get(`cart:${phone}:items`) ?? [];
```

### Feature flags

```typescript theme={null}
const flags = KV.namespace('feature-flags');

await flags.set('new-payment-flow', { enabled: true, rollout: 0.5 });

const flag = await flags.get('new-payment-flow');
if (flag?.enabled) {
  // ...
}
```

### Idempotency / deduplication

```typescript theme={null}
const processed = KV.namespace('processed-events');

if (await processed.has(`webhook:${eventId}`)) {
  return { skipped: true }; // already handled
}
await processed.set(`webhook:${eventId}`, { at: new Date().toISOString() }, { ttl: 86400 });
```

<Tip>
  See [Abandoned Cart Recovery](/use-cases/abandoned-cart-recovery) for a complete working project combining these patterns — KV cart state, TTL as cleanup policy, idempotent reminders, and Memory-powered follow-ups.
</Tip>

## Managing Data in the Dashboard

Every namespace is browsable in the platform under **KV Store** in the sidebar:

* Browse namespaces with live key counts
* Filter keys with the same glob patterns (`cart:*:items`)
* Inspect full JSON values and TTLs
* Delete individual keys or clear a whole namespace

## Local Development

Outside the platform, the SDK follows the same convention as [Memory](/core-concepts/memory): with `RUNFLOW_ENV=development` (or `RUNFLOW_LOCAL_MEMORY=true`) values are stored as JSON files under `.runflow/kv/` in your project — no API required.

```bash theme={null}
# Force a specific provider (optional)
RUNFLOW_KV_PROVIDER=file   # local JSON files
RUNFLOW_KV_PROVIDER=api    # Runflow API
```

You can also pass a provider explicitly — useful in tests:

```typescript theme={null}
import { KV, FileKvProvider } from '@runflow-ai/sdk';

const kv = KV.namespace('test', { provider: new FileKvProvider('/tmp/kv-test') });
```

To back the KV store with your own storage, implement the `KvProvider` interface (same pattern as [custom memory providers](/advanced/custom-memory-provider)).

## Limits and Validation

| Constraint                  | Value                                          |
| --------------------------- | ---------------------------------------------- |
| Value size                  | 256 KB per entry (JSON-serialized)             |
| Key length                  | 1–512 characters, no control characters        |
| Namespace                   | Letters, digits, `.`, `_`, `-` — max 128 chars |
| TTL                         | Integer ≥ 1 (seconds); omit for persistent     |
| List page size              | Default 100, max 1000                          |
| `null` / `undefined` values | Rejected — use `delete()` to remove a key      |

## Method Reference

| Method                      | Returns                         | Description                                      |
| --------------------------- | ------------------------------- | ------------------------------------------------ |
| `get(key)`                  | `T \| null`                     | Value, or `null` when missing/expired            |
| `getEntry(key)`             | `KvEntry \| null`               | Value plus `expiresAt`, `createdAt`, `updatedAt` |
| `set(key, value, { ttl? })` | `{ key, namespace, expiresAt }` | Create or overwrite a key                        |
| `has(key)`                  | `boolean`                       | Whether the key exists and is not expired        |
| `delete(key)`               | `boolean`                       | `true` if the key existed                        |
| `keys(pattern?)`            | `string[]`                      | Key names, optionally filtered by glob           |
| `getAll(pattern?)`          | `KvEntry[]`                     | Entries with values, optionally filtered         |
| `listKeys(options?)`        | `{ keys, total }`               | Paginated key metadata                           |
| `listEntries(options?)`     | `{ items, total }`              | Paginated entries                                |
| `clear()`                   | `number`                        | Delete all keys in the namespace                 |
| `listNamespaces()`          | `KvNamespaceSummary[]`          | All namespaces with key counts                   |

All methods are also available as statics on `KV` (operating on the `default` namespace), plus `KV.namespace(name)` to get a scoped instance and `KV.namespaces()` to list namespaces.

## REST API

Everything above is also exposed as authenticated REST endpoints under `/api/v1/runtime/v1/kv` — see the [Runtime REST API reference](/api-reference/introduction) if you're integrating without the SDK.

## Next Steps

<CardGroup cols={2}>
  <Card title="Memory" icon="brain" href="/core-concepts/memory">
    Conversation history — use KV for state, Memory for dialogue
  </Card>

  <Card title="Tools" icon="wrench" href="/core-concepts/tools">
    Read and write KV state from custom tools
  </Card>

  <Card title="Abandoned Cart Recovery" icon="cart-shopping" href="/use-cases/abandoned-cart-recovery">
    Complete project: KV cart state + Memory follow-ups
  </Card>

  <Card title="Schedule" icon="clock" href="/core-concepts/schedule">
    Combine KV state with scheduled follow-ups
  </Card>
</CardGroup>
