What is ctx.memory?
ctx.memory is a per-session key-value store backed by Redis. It is automatically available when the request comes from an active conversation (has a socketId) and the company has a workflow API key configured.
Typical use cases:
- Multi-step flows — remember which step the user is on
- Shopping carts — accumulate products during the conversation
- Counters — limit login attempts, retry tracking
- Temporary preferences — language, format, filters during the session
Quick start
import { define, z } from "@jelou/functions";
export default define({
name: "registration-flow",
description: "Multi-step registration with session memory",
input: z.object({
answer: z.string().optional(),
}),
handler: async (input, ctx) => {
if (!ctx.memory.available) {
return { error: "Memory not available outside of a conversation" };
}
const step = await ctx.memory.get("step", "start");
if (step === "start") {
await ctx.memory.set("step", "name", 3600);
return { question: "What is your name?" };
}
if (step === "name") {
await ctx.memory.set("name", input.answer || "", 3600);
await ctx.memory.set("step", "email", 3600);
return { question: "What is your email?" };
}
const name = await ctx.memory.get("name", "");
await ctx.memory.delete("step");
await ctx.memory.delete("name");
return { completed: true, name, email: input.answer };
},
});
Check availability
if (!ctx.memory.available) {
ctx.log("Memory not available — no socketId or workflow API key");
return { error: "memory_unavailable" };
}
ctx.memory.available is false when the socketId is missing (requests outside a conversation) or the workflow API key is not configured. Calling methods on an unavailable client throws an Error.
Primitives vs JSON
Use set()/get() for simple values and setJson()/getJson() for objects:
await ctx.memory.set("step", "confirmation", 3600);
const step = await ctx.memory.get("step", "start");
await ctx.memory.setJson("cart", { items: [], total: 0 }, 86400);
const cart = await ctx.memory.getJson("cart", { items: [], total: 0 });
The return type of get() matches the type of the default value:
const name = await ctx.memory.get("name", "anonymous"); // string
const attempts = await ctx.memory.get("attempts", 0); // number
const verified = await ctx.memory.get("verified", false); // boolean
TTL (time to live)
All values expire automatically. TTL is specified in seconds.
| Method | TTL | Maximum |
|---|
set() | Optional | 86,400 (24h) |
setJson() | Required | 86,400 (24h) |
await ctx.memory.set("step", "payment"); // no explicit TTL
await ctx.memory.set("step", "payment", 1800); // expires in 30 min
await ctx.memory.setJson("cart", cart, 86400); // expires in 24h (maximum)
Limits
| Restriction | Value |
|---|
Max length of set() | 255 characters |
| Max TTL | 86,400 seconds (24h) |
| Scope | Per session (socketId) |
Values from set() that exceed 255 characters throw an Error. For larger data, use setJson().
Common patterns
Multi-step flow
Shopping cart
Attempt limit
handler: async (input, ctx) => {
const step = await ctx.memory.get("step", "start");
if (step === "start") {
await ctx.memory.set("step", "data", 3600);
return { next: "data" };
}
if (step === "data") {
await ctx.memory.set("name", input.name, 3600);
await ctx.memory.set("step", "confirm", 3600);
return { next: "confirm", name: input.name };
}
const name = await ctx.memory.get("name", "");
await ctx.memory.delete("step");
await ctx.memory.delete("name");
return { completed: true, name };
}
interface Cart {
items: Array<{ id: string; name: string; price: number; quantity: number }>;
total: number;
}
handler: async (input, ctx) => {
const empty: Cart = { items: [], total: 0 };
const cart = await ctx.memory.getJson<Cart>("cart", empty);
cart.items.push({
id: input.productId,
name: input.name,
price: input.price,
quantity: input.quantity,
});
cart.total = cart.items.reduce(
(sum, i) => sum + i.price * i.quantity, 0
);
await ctx.memory.setJson("cart", cart, 86400);
return { cart };
}
handler: async (input, ctx) => {
const attempts = await ctx.memory.get("pin_attempts", 0);
if (attempts >= 3) {
return { blocked: true, message: "Too many attempts" };
}
const valid = input.pin === "1234";
if (!valid) {
await ctx.memory.set("pin_attempts", attempts + 1, 1800);
return { blocked: false, error: "Incorrect PIN" };
}
await ctx.memory.delete("pin_attempts");
return { blocked: false, verified: true };
}
Error handling
import { MemoryApiError } from "@jelou/functions";
try {
await ctx.memory.set("step", "payment", 3600);
} catch (err) {
if (err instanceof MemoryApiError) {
ctx.log("Memory API failed", { status: err.status, code: err.code });
if (err.isRateLimit()) {
return { error: "rate_limit", retryAfter: 2 };
}
}
throw err;
}
When to use ctx.memory vs a database?
| ctx.memory | External database |
|---|
| Scope | Per session (socketId) | Global |
| Persistence | Temporary (max 24h) | Permanent |
| Setup | Zero — included | Requires connection string in secrets |
| Ideal for | Conversation state, temporary cache | User data, history, config |