Every notification system I've seen works the same way: code triggers, template fills. User signs up? Fire the welcome sequence. Order ships? Send the tracking email. Status changes to X? Template Y.
The triggers are deterministic. The content is a template with some variables swapped in. Maybe you A/B test the subject line. Maybe you personalise with {{first_name}}. But fundamentally, you wrote the email months ago and now it just... fires.
This works fine for transactional stuff. But what if you're building something where the context actually matters? Where every situation is different enough that a template feels robotic?
The Template Problem
Traditional notification systems couple two things together:
- When to send: hardcoded triggers in your application code
- What to say: templates with variable interpolation
This coupling makes sense when your notification types are fixed and predictable. You know exactly what "order shipped" means, so you can write that email once.
But I'm building an AI agent that handles messy, real-world cases. Disputing bills, negotiating refunds, dealing with bureaucracy. Every case is different. The "what happened" varies wildly. A template that says "We've received a response from {{company}}" tells the user almost nothing.
What they actually want to know: Was it good news or bad news? What did the company say? What happens next? Do I need to do anything?
You can't template that. The content needs to be generated from context.
The Insight
Here's what I realised: if the agent is already reasoning about the situation, it should also generate the notification content. Not fill in a template. Actually write the email, based on what just happened.
The agent decides when to notify (guided by rules) and generates what to say (based on context). No templates.
I call this "semi-deterministic" because the timing follows explicit natural language rules, but the content is fully generated. Every notification is contextual and personalised. Not because we're doing fancy segmentation, but because an LLM is writing it fresh each time.
What This Looks Like
Two things are happening here:
- Timing is rule-governed. The "should I notify?" check isn't
if (event.type === 'inbound_email'). It's natural language rules that the agent interprets. - Content is generated. There's no template. The agent writes the actual email based on what just happened and the full context of the case.
Rules for Timing
"But wait," I hear you saying, "if the rules are in natural language, isn't the agent just deciding arbitrarily?"
No. There's a crucial difference between giving an agent a vague tool and giving it explicit heuristics.
Compare:
Vague: "Use the send_notification tool when appropriate."
Explicit: "Send a notification when: (1) an action requires more information from the user, or (2) something substantive happens that changes the state of their case. Ignore automated replies unless they contain critical information like a reference number."
The second version constrains behaviour. The agent still interprets "substantive" (that's what makes it semi-deterministic rather than fully deterministic) but it has a framework. It knows what categories of events matter. It knows to filter out noise.
This is the same reason you write explicit acceptance criteria instead of telling your team "do what feels right." Constraints enable good judgment.
Why "Substantive" Works
You might think vague words like "substantive" are a liability. They're actually the point.
Try writing a deterministic rule for "should we notify the user about this inbound email?" You'll end up with something like:
if (
email.from !== 'noreply@' &&
!email.subject.includes('Auto-Reply') &&
!email.body.match(/thank you for contacting/i) &&
(email.body.includes('case number') ||
email.body.length > 500 ||
email.hasAttachment)
) {
sendNotification();
}
This is a mess. And it'll still miss edge cases. What about the auto-reply that happens to contain a case number? What about the short email that says "Your refund has been processed"? What about the long email that's just a legal disclaimer?
The word "substantive" encodes human judgment that would take hundreds of conditional branches to approximate. And it generalises. When a new type of email shows up, "substantive" still works. Your regex doesn't.
Generated, Not Templated
This is where it gets interesting. Once the main agent decides to notify, it doesn't slot values into a template. It passes a summary to a specialised email agent that writes the notification from scratch:
const result = await emailAgent.stream({
prompt: `
Draft a notification based on this summary.
Greet the user by name. Be concise.
Summary: ${summary}
`,
context: situationalContext,
});
await sendEmail({
to: user.email,
subject: result.subject,
body: result.body,
});
The situationalContext includes everything relevant: the user's name, what company they're dealing with, the history of the case, what actions have been taken. The email agent synthesises all of this into a coherent message.
Compare this to a template:
// Traditional approach
sendEmail({
subject: `Update on your ${case.type} case`,
body: `Hi ${user.firstName},\n\nWe've received a response from ${company.name}. Log in to view the details.`
});
The template tells the user almost nothing. "Log in to view the details" is a cop-out. The generated version actually summarises what happened, what it means, and what comes next. It has access to the full context and can reason about it.
Why use a separate agent for this? Bounded context. The main agent is orchestrating a complex workflow with dozens of tools. Asking it to also write polished prose overloads its attention. The email agent receives a curated prompt (just the summary and situational context) and focuses entirely on communication.
Scheduling
Sometimes the right time to notify isn't now. It's later.
scheduleTask({
datetime: "2025-01-30T10:00:00Z",
task: {
type: "send-notification",
summary: "Following up, no response from business yet"
}
});
The agent decides: "I'll check back in 48 hours if we haven't heard back."
Notice what's happening here. The scheduling is deterministic: it will fire at that exact time. But the decision to schedule was a judgment call. The agent looked at the context, decided a follow-up made sense, and picked an appropriate delay.
This is semi-determinism in action. The infrastructure is predictable. The choices using that infrastructure are intelligent.
Real Examples
Let me show you what this looks like in practice.
Agent needs more info
The agent tries to log into a customer portal on behalf of the user. It hits a 2FA wall.
Rule check: "Action requires more information from the user." → Notify.
What a template would say:
Hi Sarah,
We need additional information to proceed with your case. Please log in to continue.
What the agent actually writes:
Hi Sarah,
I tried to access your Telstra account but the system is asking for a verification code sent to your phone. Could you share that code when you get a chance?
The generated version tells the user exactly what's happening and exactly what they need to do. No "log in to continue" mystery box.
Business responds
The company replies to the complaint email. It's not an auto-reply. There's a case number and next steps.
Rule check: "Substantive update that changes the case state." → Notify.
What a template would say:
Hi Sarah,
We've received a response from Telstra. Log in to view the details.
What the agent actually writes:
Hi Sarah,
Good news: Telstra has responded. They've assigned case number TLS-2024-78234 and said a specialist will review within 3-5 business days.
I'd suggest we wait for their follow-up. Want me to send a reminder if we don't hear back by Friday?
The agent synthesised the inbound email, extracted what matters, assessed the sentiment ("good news"), and proposed a concrete next step. This isn't a forwarded message. It's a briefing.
Auto-reply
The business sends: "Thank you for contacting us. Your message has been received and will be reviewed shortly."
Rule check: "Ignore automated replies unless they contain critical information." → Don't notify.
A traditional system would probably fire a "New response received" email here. The user clicks through, sees it's nothing, and learns to ignore your notifications. Our agent recognises there's nothing substantive and stays quiet.
This is the notification that didn't happen. And that's just as important as the ones that did.
Why Users Like This
After a few interactions, users develop a mental model: "I get notified when something meaningful happens."
They stop checking the app compulsively because they trust that silence means nothing's changed. They stop ignoring emails because they know each one contains something worth reading.
This is what deterministic template systems can't achieve. They're predictable but not meaningful.
The Tradeoffs
I'm not going to pretend this is free. You're trading:
Auditability. You can't point to a line of code and say "this is why the notification fired." The agent interpreted a rule. If you need to debug, you're reading reasoning traces, not stepping through conditionals.
Testing complexity. "Did the agent correctly interpret substantive?" is fuzzier than assertEquals(status, "received"). You end up writing more eval-style tests with example cases.
Prompt sensitivity. Poorly written rules lead to inconsistent behaviour. If your rule says "notify when something important happens," you've given the agent nothing. The specificity of your rules directly determines the consistency of your notifications.
When to Use This
Semi-deterministic notifications make sense when:
- The events you're responding to vary in importance, and that importance is hard to codify
- Users need to trust that notifications are worth reading
- The content of notifications needs to be contextual, not templated
- You're already running an agent that can interpret natural language rules
They don't make sense when:
- You have a small, fixed set of notification types
- The trigger conditions are genuinely simple
- You need perfect auditability for compliance reasons
- You're not running an agent anyway
Don't over-engineer. If if (order.shipped) sendEmail() works for your use case, do that.
Semi-deterministic notifications are about matching the architecture to the problem. Timing is a constraint problem: you want consistency and predictability. Content is a generation problem: you want context and nuance. Templates can't give you nuance. Pure autonomy can't give you consistency.
Split them apart. Rules for when. Intelligence for what. Your users will notice the difference.