Agent Memory and Schedules
Learn about Agent Memory and Schedules in WaymakerOS.
Two things separate an Agent from an Ambassador: it remembers, and it can wake itself up. This covers both.
Memory: ctx.state
Every agent instance gets its own private store. Write to it and the value is still there on the next call — an hour later, a month later, after a redeploy.
await ctx.state.put('stage', 'awaiting-signature')
const stage = await ctx.state.get<string>('stage') // 'awaiting-signature'
| Method | What it does |
|---|---|
get(key) | Read a value. Returns undefined if it was never set. |
put(key, value) | Write a value. Objects and arrays are fine — no manual serialising. |
delete(key) | Remove a value. Returns whether it existed. |
list({ prefix, limit }) | Read many keys at once, as an object. Prefix is the useful part. |
Prefixes give you collections
There's no separate "list" type — use key prefixes and list():
await ctx.state.put(`msg:${Date.now()}`, { from: 'ana', text: 'can we move Tuesday?' })
const messages = await ctx.state.list({ prefix: 'msg:' })
// { 'msg:1721...': {...}, 'msg:1721...': {...} }
Memory is per tenant, always
The isolation is structural, not something you implement. Agent acme writing stage cannot see or overwrite agent globex's stage. You never write a tenant id into a key to keep them apart — ctx.tenantId tells you who you're serving, but the store is already separate.
What to keep in state
Keep the things that make the next call smarter: conversation history, workflow position, counters, cached lookups, the queue of pending work. Keep business records — customers, invoices, orders — in your tables or your app's database, where the rest of your organisation can see them.
Schedules: ctx.schedule
An agent can set an alarm for itself. When it fires, your onAlarm handler runs.
export default defineAgent({
async onRequest(request, ctx) {
await ctx.state.put('pending', 'follow-up')
await ctx.schedule.in(24 * 60 * 60 * 1000) // wake me tomorrow
return new Response('I will check back in a day')
},
async onAlarm(ctx) {
const pending = await ctx.state.get('pending')
if (!pending) return
// ...send the follow-up
await ctx.state.delete('pending')
}
})
| Method | What it does |
|---|---|
in(ms) | Wake after a delay, in milliseconds |
at(when) | Wake at a specific time — a Date or a timestamp |
cancel() | Cancel the pending wake-up |
next() | When the next wake-up is due, or null if none is set |
One alarm at a time
Each agent instance has a single alarm slot. Setting a new time replaces the previous one — it doesn't queue a second.
To manage several logical timers, keep them in state and set the alarm for whichever is soonest:
async onAlarm(ctx) {
const timers = await ctx.state.list<number>({ prefix: 'due:' })
const now = Date.now()
for (const [key, dueAt] of Object.entries(timers)) {
if (dueAt <= now) {
// ...handle this one
await ctx.state.delete(key)
}
}
// Re-arm for the next one still outstanding
const remaining = Object.values(await ctx.state.list<number>({ prefix: 'due:' }))
if (remaining.length > 0) {
await ctx.schedule.at(Math.min(...remaining))
}
}
Schedules vs Ambassador cron
They solve different problems:
| Use | |
|---|---|
| Ambassador schedule | Fixed, recurring, same for everyone — "run the nightly sync at 2am" |
| Agent schedule | Per tenant, set dynamically in response to something — "chase this client in three days" |
If the timing is the same for every customer, it's a cron job on an Ambassador. If each tenant is on its own clock, it's an Agent.
A worked example: a follow-up chaser
export default defineAgent({
async onRequest(request, ctx) {
const { action } = await request.json()
if (action === 'sent') {
await ctx.state.put('sent_at', Date.now())
await ctx.state.put('chases', 0)
await ctx.schedule.in(3 * 24 * 60 * 60 * 1000) // chase in 3 days
return new Response('tracking')
}
if (action === 'replied') {
await ctx.schedule.cancel()
await ctx.state.delete('sent_at')
return new Response('done — no more chasing')
}
return new Response('unknown action', { status: 400 })
},
async onAlarm(ctx) {
const chases = (await ctx.state.get<number>('chases')) ?? 0
if (chases >= 3) return // give up politely
const note = await ctx.ai.generate({
prompt: `Write a short, warm follow-up. This is chase number ${chases + 1}.`
})
// ...send it
await ctx.state.put('chases', chases + 1)
await ctx.schedule.in(3 * 24 * 60 * 60 * 1000)
}
})
One agent, one instance per client, each on its own clock, each remembering how many times it has already chased. That shape is hard to build any other way — and it's the reason Agents exist.
Next Steps
- What is an Agent? — concepts and billing
- Creating Your First Agent — scaffold to deployed