Reminders Now Understand Perspective: Notify vs Action

Eric Brasher February 20, 2026 at 9:55 AM 7 min read

When an agent sets a reminder, who is it for? Is the agent reminding itself to do something, or reminding the user about something? Until today, OpenCaddis didn't distinguish between the two. The reminder fired, the agent got a generic message, and it had to guess what to do with it. We just shipped a change that fixes this — reminders now carry explicit perspective, and the agent behaves differently depending on whether it's acting or notifying.

The Problem

The old RegisterReminder API looked like this:

Before
public async Task<string> RegisterReminder(
    string reminderName,
    string messageType,       // generic — agent had to guess intent
    string? message,
    double dueTimeMinutes,
    double periodMinutes)      // always recurring

The LLM had to fill in a raw messageType string and decide on a period — even when the user just wanted a one-time reminder. And when the reminder fired, the AssistantAgent treated it like any other message. The agent didn't know whether to take action or relay a notification. This led to inconsistent behavior: sometimes the agent would try to do work when it should have just notified the user, and sometimes it would just report back when it should have been executing.

The Solution: Two Perspectives

We replaced the generic messageType parameter with a clear reminderType that has exactly two values:

notify

The reminder is for the user. When it fires, the agent's job is to relay the message to the user — nothing more.

"Remind me to check the deployment in 30 minutes"

action

The reminder is for the agent. When it fires, the agent should autonomously execute the task using its available tools.

"Every hour, check the API health endpoint and alert me if it's down"

The new API makes the intent explicit:

After
public async Task<string> RegisterReminder(
    string reminderName,
    string reminderType,       // "notify" or "action"
    string message,
    double dueTimeMinutes,
    bool recurring = false,   // one-shot by default
    double periodMinutes = 1)  // only used when recurring

How It Works Inside the AssistantAgent

When a reminder fires, the AssistantAgent now formats the message differently based on the reminder type. This is the key change — the agent receives a contextual prompt that tells it exactly what perspective to take:

AssistantAgent.cs — FormatReminderMessage
private static string? FormatReminderMessage(AgentMessage message)
{
    if (message.MessageType?.Contains(":action") == true)
        return
            "[REMINDER TRIGGERED] You have a scheduled task to "
            + "execute autonomously. Use your available tools to "
            + "carry out the following instructions:\n"
            + content;

    return
        "[REMINDER TRIGGERED] You have a scheduled reminder. "
        + "Notify the user with the following message using "
        + "the SendNotification tool:\n\""
        + content + "\"";
}

With an action reminder, the agent is told to use its tools and execute autonomously. With a notify reminder, it's told to relay the message to the user. The LLM doesn't have to guess — the framing is built into the system.

One-Shot by Default

The other major change is that reminders are now one-shot by default. Most reminders ("remind me in 30 minutes", "check on this in an hour") should fire once and go away. The old API always required a period and always recurred.

Now, recurring defaults to false. One-shot reminders auto-unregister after the first tick:

Auto-cleanup for one-shot reminders
// After processing the reminder response...
if (message.MessageType?.Contains(":oneshot") == true)
{
    var reminderName = message.Args?.GetValueOrDefault("reminderName");
    if (reminderName is not null)
        await fabrAgentHost.UnregisterReminder(reminderName);
}

Orleans requires a minimum period of 1 minute for all reminders, so one-shot reminders are technically registered with a 1-minute period — but the agent auto-unregisters them after the first tick, so they never fire twice.

Message type encoding

The reminder type and one-shot flag are encoded in the message type string: reminder:notify, reminder:action, reminder:notify:oneshot, reminder:action:oneshot. This keeps the information in the message itself so the AssistantAgent can route without external lookups.

Fixing Reminder Delivery to the UI

There was a subtle bug with how reminder responses reached the user. Reminders in FabrCore are self-sent — the agent sends a message to itself on a timer. When OnMessage returns a response, that response goes back to the sender, which is... the agent. The user never sees it.

The fix: after processing a reminder, the AssistantAgent now explicitly sends the response to the connected client:

Explicit client delivery
if (ThinkingNotifier.TryGetClientHandle(myHandle, out var clientHandle))
{
    await fabrAgentHost.SendMessage(new AgentMessage
    {
        ToHandle = clientHandle,
        FromHandle = myHandle,
        Kind = MessageKind.OneWay,
        Message = response.Message
    });
}

We also added a keep-alive mechanism in the Chat UI. Orleans observer subscriptions can expire during long idle periods, which means reminder messages would have nowhere to go. The chat component now pings agent health every 2 minutes, which triggers RefreshObserverIfNeeded inside the client context and keeps the subscription alive.

What It Looks Like in Practice

User SaysReminder TypeRecurringAgent Behavior
"Remind me to submit the report in 2 hours" notify No (one-shot) Fires once, sends notification to user, auto-unregisters
"Check the /health endpoint every 15 minutes and tell me if it fails" action Yes Fires every 15 min, agent calls the endpoint using WebBrowser, reports result
"In 30 minutes, summarize my unread emails" action No (one-shot) Fires once, agent uses Microsoft365Email plugin to read and summarize, auto-unregisters
"Every morning at 9am, remind me to check the build" notify Yes Fires daily, sends a notification to the user each time

Summary of Changes

BeforeAfter
Generic messageType stringExplicit reminderType: "notify" or "action"
Always recurringOne-shot by default, opt-in to recurring
Agent guesses intent from contextAgent receives a framed prompt matching the perspective
Reminder response went back to agent (lost)Response explicitly sent to connected client
Observer subscription could expireKeep-alive pings every 2 minutes
One-shot reminders kept firingAuto-unregistered after first tick

These are the kind of changes that look small in a diff but make a real difference in how the system behaves. Reminders went from "sometimes useful, sometimes confusing" to predictable and intentional. The agent knows what perspective to take, the user gets responses reliably, and one-shot reminders clean up after themselves.


Eric Brasher

Builder of OpenCaddis and the FabrCore framework.