Key-value ("KV") storage provides fast data access for agents. Use it for caching, configuration, rate limiting, and data that needs quick lookups.
When to Use Key-Value Storage
| Storage Type | Best For |
|---|---|
| Key-Value | Fast lookups, caching, configuration, rate limits |
| Vector | Semantic search, embeddings, RAG |
| Object (S3) | Files, images, documents, media |
| Database | Structured data, complex queries, transactions |
| Durable Streams | Large exports, audit logs, real-time data |
Use built-in state (ctx.state, ctx.thread.state, ctx.session.state) for data tied to active requests and conversations. Use KV when you need custom TTL, persistent data across sessions, or shared state across agents.
Access Patterns
| Context | Access | Details |
|---|---|---|
| Agents | ctx.kv | See examples below |
| Routes | c.var.kv | See Using in Routes |
| Standalone | createAgentContext() | See Standalone Usage |
| External backends | HTTP routes | SDK Utilities for External Apps |
| Frontend | Via routes | React Hooks |
The KV API is identical in all contexts. ctx.kv.get() and c.var.kv.get() work the same way. See Accessing Services for the full reference.
Basic Operations
Access key-value storage through ctx.kv in agents or c.var.kv in routes. Buckets are auto-created on first use.
Storing Data
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('CacheManager', {
handler: async (ctx, input) => {
// Store with custom TTL (minimum 60 seconds, maximum 90 days)
await ctx.kv.set('cache', 'api-response', responseData, {
ttl: 3600, // expires in 1 hour
contentType: 'application/json',
});
// Store with default TTL (7 days)
await ctx.kv.set('cache', 'user-prefs', { theme: 'dark' });
// Store with no expiration (persists indefinitely)
await ctx.kv.set('config', 'feature-flags', {
darkMode: true,
betaFeatures: false,
}, {
ttl: null, // never expires (0 also works)
});
return { success: true };
},
});Retrieving Data
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('CacheRetriever', {
handler: async (ctx, input) => {
const result = await ctx.kv.get('cache', 'api-response');
if (result.exists) {
ctx.logger.info('Cache hit', {
contentType: result.contentType,
expiresAt: result.expiresAt, // ISO timestamp when key expires (if TTL set)
});
return { data: result.data };
}
ctx.logger.info('Cache miss');
return { data: null };
},
});Deleting Data
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('SessionCleaner', {
handler: async (ctx, input) => {
await ctx.kv.delete('sessions', input.sessionId);
return { deleted: true };
},
});Type Safety
Use generics for type-safe data access:
import { createAgent } from '@agentuity/runtime';
import { type } from 'arktype';
const UserPreferences = type({
theme: "'light' | 'dark'",
language: 'string',
notifications: 'boolean',
});
type UserPreferences = typeof UserPreferences.infer;
const agent = createAgent('PreferenceLoader', {
handler: async (ctx, input) => {
const result = await ctx.kv.get<UserPreferences>('prefs', input.userId);
if (result.exists) {
// TypeScript knows the shape of result.data
const theme = result.data.theme; // Type: 'light' | 'dark'
return { theme };
}
return { theme: 'light' }; // default
},
});Additional Methods
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('StorageExplorer', {
handler: async (ctx, input) => {
// Search keys by keyword (returns keys with metadata)
const matches = await ctx.kv.search('cache', 'user-');
// List all keys in a namespace
const keys = await ctx.kv.getKeys('cache');
// List all namespaces
const namespaces = await ctx.kv.getNamespaces();
// Get statistics for a namespace
const stats = await ctx.kv.getStats('cache');
// Get statistics for all namespaces
const allStats = await ctx.kv.getAllStats();
return { keys, namespaces, stats, allStats };
},
});Namespace Management
Create and delete namespaces programmatically:
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('NamespaceManager', {
handler: async (ctx, input) => {
// Create a namespace with default TTL for all keys
await ctx.kv.createNamespace('cache', {
defaultTTLSeconds: 3600, // all keys expire in 1 hour by default
});
// Create a namespace with no expiration
await ctx.kv.createNamespace('config', {
defaultTTLSeconds: 0, // keys never expire
});
// Delete a namespace (removes all keys)
await ctx.kv.deleteNamespace('old-cache');
return { success: true };
},
});TTL semantics:
| Value | Behavior |
|---|---|
undefined | Keys expire after 7 days (default) |
null or 0 | Keys never expire |
>= 60 | Custom TTL in seconds (minimum 60 seconds, maximum 90 days) |
The minimum TTL is 60 seconds. The maximum is 90 days (7,776,000 seconds). Values outside this range will be rejected.
When a key is read with less than 50% of its TTL remaining, the expiration is automatically extended. This keeps frequently-accessed data alive without manual renewal.
deleteNamespace() permanently removes the namespace and all its keys. This operation cannot be undone.
TTL Strategy
Keys expire after 7 days by default unless a namespace-level or per-key TTL is set. Use TTL for temporary data:
| Data Type | Suggested TTL |
|---|---|
| API cache | 5-60 minutes (300-3600s) |
| Session data | 24-48 hours (86400-172800s) |
| Rate limit counters | Until period reset |
| Feature flags | No TTL (persistent) |
import { createAgent } from '@agentuity/runtime';
interface UserSession {
userId: string;
email: string;
loginAt: string;
preferences: { theme: string };
}
const agent = createAgent('SessionManager', {
handler: async (ctx, input) => {
const sessionKey = `session:${input.token}`;
// Check for existing session
const existing = await ctx.kv.get<UserSession>('sessions', sessionKey);
if (existing.exists) {
return { session: existing.data };
}
// Create new session with 24-hour TTL
const session: UserSession = {
userId: input.userId,
email: input.email,
loginAt: new Date().toISOString(),
preferences: { theme: 'light' },
};
await ctx.kv.set('sessions', sessionKey, session, {
ttl: 86400, // 24 hours
});
return { session };
},
});Using in Routes
Routes have the same KV access via c.var.kv:
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.get('/session/:id', async (c) => {
const sessionId = c.req.param('id');
const result = await c.var.kv.get('sessions', sessionId);
if (!result.exists) {
return c.json({ error: 'Session not found' }, 404);
}
return c.json({ session: result.data });
});
export default router;Need to access KV from a Next.js backend or other external service? Create authenticated routes that expose storage operations, then call them via HTTP. See SDK Utilities for External Apps.
Standalone Usage
Use KV storage outside of agent handlers with createAgentContext():
import { createApp, createAgentContext } from '@agentuity/runtime';
await createApp();
// Discord bot example
client.on('messageCreate', async (message) => {
const ctx = createAgentContext();
await ctx.invoke(async () => {
// Check user preferences
const prefs = await ctx.kv.get('prefs', message.author.id);
if (!prefs.exists) {
await ctx.kv.set('prefs', message.author.id, {
theme: 'dark',
notifications: true,
});
}
});
});See Running Agents Without HTTP for more patterns including Discord bots, CLI tools, and queue workers.
Best Practices
- Use descriptive keys:
user:{userId}:prefsinstead ofu123 - Set appropriate TTLs: Prevent storage bloat with expiring cache entries
- Handle missing keys: Always check
result.existsbefore accessing data - Keep values small: KV is optimized for small-to-medium values; use Object Storage for large files
Next Steps
- Vector Storage: Semantic search and embeddings
- Object Storage (S3): File and media storage
- Database: Relational data with queries and transactions
- Durable Streams: Large data exports
- State Management: Built-in request/thread/session state