Last weekend i finished working on the core features for Sheldon, my personal AI assistant, and decided to test his self managed reminders.

Reminders were honestly one of the main reasons i wanted to build him in the first place. he already has a pretty solid memory system, but then, the obvious approach to agentic assistant reminders (cron + content + replay) felt too primitive, like a glorified remindMeOfThis bot.

I put my thinking caps on, decided i should explore a different approach and in the process of brainstorming stumbled onto a clever idea and to my surprise, i couldn't find any material on the internet about it like i usually do for all other ideas i have thought were genius ideas (yeah, i looked through the OG forums, googled, asked different llms to find anything related, etc). walk with me.

How every other system does reminders

Every AI assistant i've looked at handles reminders the same way. you say "remind me to do xxx at 8pm", and the system stores that exact text in a database. when 8pm hits, it sends you that stored text verbatim or in polished form.

traditional approach:

User: "remind me to <do something> at 8pm"
         │
         ▼
┌─────────────────────────────────────────────┐
│  Reminder Database                          │
│  ┌───────────────────────────────────────┐  │
│  │ id: 1                                 │  │
│  │ time: 20:00                           │  │
│  │ message: "remind xxx to do something" │  │ ← full content stored here
│  │ chat_id: 12345                        │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
         │
         │ [8:00 PM]
         ▼
    "hey it's time to <do something>"  ← robotic reminder

This works. but it's weirdly disconnected from how these assistants actually operate. they have sophisticated memory, they remember your preferences, your context, your life. but reminders? those live in a completely separate box, frozen in time.

What if you later tell the assistant "actually, i need to take my vitamins with my meds"? the memory updates, but the reminder doesn't know about it. two systems that never talk to each other.

Your brain doesn't work this way

Think about how your own brain handles reminders for a second. you don't have a separate "reminder storage" and "memory storage". when you need to remember something at a specific time, your brain associates a trigger with a memory. when that trigger fires, you recall the memory with all its current context and any updates since you first made the mental note.

For example, when you need to take your medication, your brain does something like:

"8pm" ──triggers──► recall("meds") ──► "oh right, meds AND vitamins now"

The trigger is just a pointer. the actual content lives in memory, where it can be updated, enriched, and connected to other things you know.

I wanted my assistant to work the same way.

What i built instead

While building Sheldon, instead of storing reminder content in the cron job, i store just a keyword. when the cron fires, it searches memory for context using that keyword, injects the results into the agent loop, and the agent decides what to do.

I call this pattern SCAMR - self-managed cron-augmented memory retrieval. bit of a mouthful but it captures what's happening: the agent manages its own crons, and those crons are augmented by memory retrieval instead of static text storage.

The cron doesn't know what to remind you about. it just knows when to wake up and what keyword to search for. memory is the source of truth.

How it actually works

Let's walk through a real example. you tell an agent like Sheldon who has SCAMR implemented: "remind me to take my meds every evening for 2 weeks". then the agent does two things:

1. stores the fact in memory (he was going to do this anyway)

domain: "routines"
field: "medication"
value: "take meds every evening"

2. creates a cron with just a keyword

keyword: "meds"           ← just a search term, not the reminder text
schedule: "0 20 * * *"    ← 8pm daily
expires: 2 weeks

That's it. the cron has no idea what "meds" means. it doesn't store "take meds every evening" anywhere. it just knows: at 8pm, search memory for "meds" and give whatever you find to the agent.

So 8pm hits. the cron fires. here's what happens:

recall("meds", domains=[routines, health, goals])
    │
    ▼
  finds: "take meds every evening"
    │
    ▼
  injects into agent loop
    │
    ▼
  agent sends: "⏰ Time for your evening meds!"

After 2 weeks, the cron auto-deletes. no cleanup needed.

ok cool, but so far this looks like a complicated way to do something simple right? here's where it clicks.

The memory updates. the reminder follows.

a week into your reminder, you casually tell Sheldon: "oh btw, the doctor said i should take my vitamins with my meds from now on"

Sheldon stores that as a new fact - "take vitamins with meds every evening". it's a separate fact, not an edit to the original one. you didn't touch the reminder. you didn't edit any cron. you didn't even think about the reminder.

But that evening at 8pm, the cron fires, searches memory for "meds" using semantic search, and now finds both facts:

recall("meds")
    │
    ▼
  finds: "take meds every evening"
  finds: "take vitamins with meds every evening"  ← new related fact!
    │
    ▼
  agent sends: "⏰ Time for your meds and vitamins!"

A traditional system would still be saying "take your meds" because the reminder was frozen the moment you created it. SCAMR doesn't have this problem because the cron never stored content in the first place - it searches memory every time it fires, and semantic search naturally surfaces related facts.

It gets even better with connected facts

Memory isn't flat. facts are connected to other facts, entities, and domains. so recall("meds") doesn't just return one thing - it can surface related context the agent thinks is relevant:

recall("meds")
    │
    ▼
  primary: "take meds and vitamins every evening"
  related: "prescription refill due March 15"
  related: "Dr. Smith - primary care physician, (030) 555-0147"
    │
    ▼
  agent sends: "⏰ Time for your meds and vitamins!
                Heads up - your prescription refill is due in 3 days.
                Dr. Smith's office: (030) 555-0147"

You asked for a medication reminder. you got a medication reminder that also tells you your prescription is running out and gives you your doctor's number. because the agent searched memory instead of replaying a frozen string.

It's not just reminders - it's scheduled agent triggers

This is the part that really makes it click. for an agent like Sheldon, the cron doesn't just fire a notification. it wakes the full agent with context. the agent then decides what to do. could be a reminder, could be a check-in, could be kicking off an entire coding task. same pattern.

This eliminates the need for a "heartbeat" system since all you have to do is tell sheldon when you want him to check-in on something and then he creates a new cron with a keyword (could be anything or any time, or any interval) and only when the cron fires will he retrieve context about what he should be coming to your dms to talk about. this way the agent is always fully aware even if the crons were created ages ago.

You: "check on me every 6 hours while I'm grinding on this project"
  keyword: "project"
  schedule: "0 */6 * * *"

6hours later:
  cron job: triggers recall with keyword "project" 
  memory recalls: "working on API refactor", "deadline Friday"

Sheldon: "Hey, how's the refactor going? You've been at it
          for a while. Friday's coming up quick."
You: "tomorrow at 3pm, build me a weather dashboard
      and deploy it to weather.mysite.com"

  keyword: "weather dashboard"
  schedule: one-shot, tomorrow 3pm

3pm tomorrow:
  cron job: triggers recall with keyword "weather dashboard" 
  memory recalls: "preferred stack: Go", "deployment target: docker"

Sheldon: "spins up a sandboxed container, writes the code,
          deploys it, and messages you when it's done"

The cron fires, memory is recalled, context is injected into the agent loop, and the agent takes action. the LLM converts natural language to cron expressions too - "every morning at 9" becomes 0 9 * * *, "weekdays at 2pm" becomes 0 14 * * 1-5. no cron syntax knowledge needed from the user.

How this compares to existing systems

I looked at how other AI assistants handle this and they all do the same thing - store the full message in the cron, keep memory separate, replay the stored text when it fires.

OpenClaw has a pretty solid cron system. you can just ask your claw bot to remind you about something just like sheldon and it will. it supports isolated sessions, model overrides, heartbeat integration, the works. but the cron still stores the full message or system event text. its memory lives in separate markdown files that the cron system never queries. when a cron fires, it replays what you stored or runs an agent turn with the message you wrote at creation time.

NanoBot takes the same approach in a lighter package. nanobot cron add --name "daily" --message "Good morning!" stores the full text. its memory module is a separate persistent storage system that the cron scheduler never touches. when a cron fires, it sends the stored message. that's it.

both are great projects and i learned a lot from studying them. but the pattern is the same across all of them:

The difference isn't that their cron systems are bad. they're actually more feature-rich than mine in some ways. the difference is that none of them connect their crons to their memory system. the cron and memory are two separate things that never interact.

Also, you might be thinking "sure, OpenClaw stores the full text, but it runs an agent turn when the cron fires, so the LLM can still make the message sound nice." well yeah, that's true. the agent can rephrase "Good morning!" into something more natural. but the LLM is still only working with what the cron gave it. it doesn't go search memory for related context. it doesn't know your prescription is running out or that you added vitamins last week. it can polish the words, but it can't enrich the content.

Implementation

I'll keep the code minimal but enough to get the idea across. the whole thing is in Go.

Memory store (sheldonmem)

Sheldonmem is a lightweight memory package i built for sheldon. facts are extracted during conversations and stored in SQLite with domain tags, entity relationships, and vector embeddings.

type Fact struct {
    ID         int64
    EntityID   *int64     // who this fact is about
    DomainID   int        // which life domain (health, routines, etc.)
    Field      string     // category
    Value      string     // the actual content
    Confidence float64
    CreatedAt  time.Time
}

Cron table

Minimal schema. just timing and a keyword:

CREATE TABLE crons (
    id INTEGER PRIMARY KEY,
    keyword TEXT NOT NULL,      -- search term for memory
    schedule TEXT NOT NULL,     -- cron expression
    chat_id INTEGER NOT NULL,   -- where to send
    expires_at DATETIME,        -- auto-delete (nullable)
    next_run DATETIME NOT NULL, -- pre-computed next fire
    created_at DATETIME
);

Cron runner

Background process that checks every minute:

func (r *CronRunner) fireCron(ctx context.Context, c cron.Cron) {
	// search memory for keyword
	result, _ := r.memory.Recall(ctx, c.Keyword, nil, 10) // nil = all domains

	// build context from recalled facts
	var factsContext strings.Builder
	for _, f := range result.Facts {
		fmt.Fprintf(&factsContext, "- %s: %s\n", f.Field, f.Value)
	}

	// inject into agent loop - NOT just a notification
	prompt := fmt.Sprintf(`[SCHEDULED TRIGGER]
  Keyword: %s
  Current time: %s

  Recalled context:
  %s`, c.Keyword, currentTime, factsContext.String())

	response, _ := r.trigger(c.ChatID, sessionID, prompt)
	r.notify(c.ChatID, response)

	// schedule next run
	nextRun, _ := r.crons.ComputeNextRun(c.Schedule)
	r.crons.UpdateNextRun(c.ID, nextRun)
}

Agent tools

The agent gets five tools for cron management:

set_cron(keyword, schedule, expires_in)
  → creates a new scheduled trigger

list_crons()
  → shows all active crons for this chat

delete_cron(keyword)
  → removes a cron by keyword

pause_crons()
  → temporrily deactivates (eg don't send the next check-in, i'll be busy)

resume_cron(keyword)
  → resumes a previously paused cron

the agent decides the keyword, the schedule, the expiry. you just speak naturally.

Where SCAMR makes sense

This approach works well when your agent already has a structured memory system and you want reminders to stay in sync with it. if you want context-aware notifications instead of parroting stored text. if you want crons that wake the full agent, not just send a string. if you want the agent to self-manage its own schedule.

It's probably overkill if you just need simple static reminders, or your agent doesn't have persistent memory, or you want users to edit reminder text directly.

Hello, I'm
Collins Enebeli.

avatar

I solve problems. These days, you'll find me neck-deep in design and product engineering. However, building, testing, and deploying solutions transcends field, architecture stack, or computer programming language. let's talk →

Last weekend i finished working on the core features for Sheldon, my personal AI assistant, and decided to test his self managed reminders.

Reminders were honestly one of the main reasons i wanted to build him in the first place. he already has a pretty solid memory system, but then, the obvious approach to agentic assistant reminders (cron + content + replay) felt too primitive, like a glorified remindMeOfThis bot.

I put my thinking caps on, decided i should explore a different approach and in the process of brainstorming stumbled onto a clever idea and to my surprise, i couldn't find any material on the internet about it like i usually do for all other ideas i have thought were genius ideas (yeah, i looked through the OG forums, googled, asked different llms to find anything related, etc). walk with me.

How every other system does reminders

Every AI assistant i've looked at handles reminders the same way. you say "remind me to do xxx at 8pm", and the system stores that exact text in a database. when 8pm hits, it sends you that stored text verbatim or in polished form.

traditional approach:

User: "remind me to <do something> at 8pm"
         │
         ▼
┌─────────────────────────────────────────────┐
│  Reminder Database                          │
│  ┌───────────────────────────────────────┐  │
│  │ id: 1                                 │  │
│  │ time: 20:00                           │  │
│  │ message: "remind xxx to do something" │  │ ← full content stored here
│  │ chat_id: 12345                        │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
         │
         │ [8:00 PM]
         ▼
    "hey it's time to <do something>"  ← robotic reminder

This works. but it's weirdly disconnected from how these assistants actually operate. they have sophisticated memory, they remember your preferences, your context, your life. but reminders? those live in a completely separate box, frozen in time.

What if you later tell the assistant "actually, i need to take my vitamins with my meds"? the memory updates, but the reminder doesn't know about it. two systems that never talk to each other.

Your brain doesn't work this way

Think about how your own brain handles reminders for a second. you don't have a separate "reminder storage" and "memory storage". when you need to remember something at a specific time, your brain associates a trigger with a memory. when that trigger fires, you recall the memory with all its current context and any updates since you first made the mental note.

For example, when you need to take your medication, your brain does something like:

"8pm" ──triggers──► recall("meds") ──► "oh right, meds AND vitamins now"

The trigger is just a pointer. the actual content lives in memory, where it can be updated, enriched, and connected to other things you know.

I wanted my assistant to work the same way.

What i built instead

While building Sheldon, instead of storing reminder content in the cron job, i store just a keyword. when the cron fires, it searches memory for context using that keyword, injects the results into the agent loop, and the agent decides what to do.

I call this pattern SCAMR - self-managed cron-augmented memory retrieval. bit of a mouthful but it captures what's happening: the agent manages its own crons, and those crons are augmented by memory retrieval instead of static text storage.

The cron doesn't know what to remind you about. it just knows when to wake up and what keyword to search for. memory is the source of truth.

How it actually works

Let's walk through a real example. you tell an agent like Sheldon who has SCAMR implemented: "remind me to take my meds every evening for 2 weeks". then the agent does two things:

1. stores the fact in memory (he was going to do this anyway)

domain: "routines"
field: "medication"
value: "take meds every evening"

2. creates a cron with just a keyword

keyword: "meds"           ← just a search term, not the reminder text
schedule: "0 20 * * *"    ← 8pm daily
expires: 2 weeks

That's it. the cron has no idea what "meds" means. it doesn't store "take meds every evening" anywhere. it just knows: at 8pm, search memory for "meds" and give whatever you find to the agent.

So 8pm hits. the cron fires. here's what happens:

recall("meds", domains=[routines, health, goals])
    │
    ▼
  finds: "take meds every evening"
    │
    ▼
  injects into agent loop
    │
    ▼
  agent sends: "⏰ Time for your evening meds!"

After 2 weeks, the cron auto-deletes. no cleanup needed.

ok cool, but so far this looks like a complicated way to do something simple right? here's where it clicks.

The memory updates. the reminder follows.

a week into your reminder, you casually tell Sheldon: "oh btw, the doctor said i should take my vitamins with my meds from now on"

Sheldon stores that as a new fact - "take vitamins with meds every evening". it's a separate fact, not an edit to the original one. you didn't touch the reminder. you didn't edit any cron. you didn't even think about the reminder.

But that evening at 8pm, the cron fires, searches memory for "meds" using semantic search, and now finds both facts:

recall("meds")
    │
    ▼
  finds: "take meds every evening"
  finds: "take vitamins with meds every evening"  ← new related fact!
    │
    ▼
  agent sends: "⏰ Time for your meds and vitamins!"

A traditional system would still be saying "take your meds" because the reminder was frozen the moment you created it. SCAMR doesn't have this problem because the cron never stored content in the first place - it searches memory every time it fires, and semantic search naturally surfaces related facts.

It gets even better with connected facts

Memory isn't flat. facts are connected to other facts, entities, and domains. so recall("meds") doesn't just return one thing - it can surface related context the agent thinks is relevant:

recall("meds")
    │
    ▼
  primary: "take meds and vitamins every evening"
  related: "prescription refill due March 15"
  related: "Dr. Smith - primary care physician, (030) 555-0147"
    │
    ▼
  agent sends: "⏰ Time for your meds and vitamins!
                Heads up - your prescription refill is due in 3 days.
                Dr. Smith's office: (030) 555-0147"

You asked for a medication reminder. you got a medication reminder that also tells you your prescription is running out and gives you your doctor's number. because the agent searched memory instead of replaying a frozen string.

It's not just reminders - it's scheduled agent triggers

This is the part that really makes it click. for an agent like Sheldon, the cron doesn't just fire a notification. it wakes the full agent with context. the agent then decides what to do. could be a reminder, could be a check-in, could be kicking off an entire coding task. same pattern.

This eliminates the need for a "heartbeat" system since all you have to do is tell sheldon when you want him to check-in on something and then he creates a new cron with a keyword (could be anything or any time, or any interval) and only when the cron fires will he retrieve context about what he should be coming to your dms to talk about. this way the agent is always fully aware even if the crons were created ages ago.

You: "check on me every 6 hours while I'm grinding on this project"
  keyword: "project"
  schedule: "0 */6 * * *"

6hours later:
  cron job: triggers recall with keyword "project" 
  memory recalls: "working on API refactor", "deadline Friday"

Sheldon: "Hey, how's the refactor going? You've been at it
          for a while. Friday's coming up quick."
You: "tomorrow at 3pm, build me a weather dashboard
      and deploy it to weather.mysite.com"

  keyword: "weather dashboard"
  schedule: one-shot, tomorrow 3pm

3pm tomorrow:
  cron job: triggers recall with keyword "weather dashboard" 
  memory recalls: "preferred stack: Go", "deployment target: docker"

Sheldon: "spins up a sandboxed container, writes the code,
          deploys it, and messages you when it's done"

The cron fires, memory is recalled, context is injected into the agent loop, and the agent takes action. the LLM converts natural language to cron expressions too - "every morning at 9" becomes 0 9 * * *, "weekdays at 2pm" becomes 0 14 * * 1-5. no cron syntax knowledge needed from the user.

How this compares to existing systems

I looked at how other AI assistants handle this and they all do the same thing - store the full message in the cron, keep memory separate, replay the stored text when it fires.

OpenClaw has a pretty solid cron system. you can just ask your claw bot to remind you about something just like sheldon and it will. it supports isolated sessions, model overrides, heartbeat integration, the works. but the cron still stores the full message or system event text. its memory lives in separate markdown files that the cron system never queries. when a cron fires, it replays what you stored or runs an agent turn with the message you wrote at creation time.

NanoBot takes the same approach in a lighter package. nanobot cron add --name "daily" --message "Good morning!" stores the full text. its memory module is a separate persistent storage system that the cron scheduler never touches. when a cron fires, it sends the stored message. that's it.

both are great projects and i learned a lot from studying them. but the pattern is the same across all of them:

The difference isn't that their cron systems are bad. they're actually more feature-rich than mine in some ways. the difference is that none of them connect their crons to their memory system. the cron and memory are two separate things that never interact.

Also, you might be thinking "sure, OpenClaw stores the full text, but it runs an agent turn when the cron fires, so the LLM can still make the message sound nice." well yeah, that's true. the agent can rephrase "Good morning!" into something more natural. but the LLM is still only working with what the cron gave it. it doesn't go search memory for related context. it doesn't know your prescription is running out or that you added vitamins last week. it can polish the words, but it can't enrich the content.

Implementation

I'll keep the code minimal but enough to get the idea across. the whole thing is in Go.

Memory store (sheldonmem)

Sheldonmem is a lightweight memory package i built for sheldon. facts are extracted during conversations and stored in SQLite with domain tags, entity relationships, and vector embeddings.

type Fact struct {
    ID         int64
    EntityID   *int64     // who this fact is about
    DomainID   int        // which life domain (health, routines, etc.)
    Field      string     // category
    Value      string     // the actual content
    Confidence float64
    CreatedAt  time.Time
}

Cron table

Minimal schema. just timing and a keyword:

CREATE TABLE crons (
    id INTEGER PRIMARY KEY,
    keyword TEXT NOT NULL,      -- search term for memory
    schedule TEXT NOT NULL,     -- cron expression
    chat_id INTEGER NOT NULL,   -- where to send
    expires_at DATETIME,        -- auto-delete (nullable)
    next_run DATETIME NOT NULL, -- pre-computed next fire
    created_at DATETIME
);

Cron runner

Background process that checks every minute:

func (r *CronRunner) fireCron(ctx context.Context, c cron.Cron) {
	// search memory for keyword
	result, _ := r.memory.Recall(ctx, c.Keyword, nil, 10) // nil = all domains

	// build context from recalled facts
	var factsContext strings.Builder
	for _, f := range result.Facts {
		fmt.Fprintf(&factsContext, "- %s: %s\n", f.Field, f.Value)
	}

	// inject into agent loop - NOT just a notification
	prompt := fmt.Sprintf(`[SCHEDULED TRIGGER]
  Keyword: %s
  Current time: %s

  Recalled context:
  %s`, c.Keyword, currentTime, factsContext.String())

	response, _ := r.trigger(c.ChatID, sessionID, prompt)
	r.notify(c.ChatID, response)

	// schedule next run
	nextRun, _ := r.crons.ComputeNextRun(c.Schedule)
	r.crons.UpdateNextRun(c.ID, nextRun)
}

Agent tools

The agent gets five tools for cron management:

set_cron(keyword, schedule, expires_in)
  → creates a new scheduled trigger

list_crons()
  → shows all active crons for this chat

delete_cron(keyword)
  → removes a cron by keyword

pause_crons()
  → temporrily deactivates (eg don't send the next check-in, i'll be busy)

resume_cron(keyword)
  → resumes a previously paused cron

the agent decides the keyword, the schedule, the expiry. you just speak naturally.

Where SCAMR makes sense

This approach works well when your agent already has a structured memory system and you want reminders to stay in sync with it. if you want context-aware notifications instead of parroting stored text. if you want crons that wake the full agent, not just send a string. if you want the agent to self-manage its own schedule.

It's probably overkill if you just need simple static reminders, or your agent doesn't have persistent memory, or you want users to edit reminder text directly.