SoftSages Technology company logo
LinkedIn professional network iconShare on LinkedInFacebook social media iconShare on Facebook

Mizo: A Conversational Agent Inside a Live Email Marketing Platform

September 04, 2026 25 mins read SoftSages Team AI and ML Development
Six-layer architecture diagram of the Mizo AI agent inside Mailzzy: request safety, turn orchestration, agent runtime, deterministic send chain, state and observability, and the existing campaign pipeline

1. The Desired Outcome


2. Architecture at a Glance


3. The Components


4. Challenges and How We Solved Them


5. Summary

Today, building a campaign email in Mailzzy means picking an authoring surface and working through it: a drag-and-drop block builder, a rich-text editor, or raw HTML. After that come more screens for audience, sender, sending domain and schedule. It works, and it is what most of the market offers. But it is slow, it asks for a lot of decisions before anyone sees a draft, and every path assumes the user wants to sit inside a builder.
Mizo is the path that is not a builder. Describe the campaign in chat, get the finished campaign back.
That sounds like a text-generation feature. It is not. A campaign is not a document. It is a structured object with a large validation surface, and almost none of that surface is about language:
Now put a system that is approximately right by design in front of that surface. A send is irreversible, outward-facing and large. One wrong audience is not a bad paragraph. It is thousands of emails to the wrong people, with no undo.
So the question was never whether a model can write marketing copy. It is where the model's authority ends, and what enforces that boundary.
  • The subject line has a length limit and has to avoid deliverability traps.
  • The body has to be HTML that renders correctly in every major inbox client. In practice that means table-based layout, fully inlined styles and fluid widths.
  • The audience has to resolve to contact groups that exist in this account and no other, with a real recipient count behind them.
  • The sender has to be a verified identity the user is allowed to send from, on an active, authenticated domain.
  • The schedule has to parse, resolve against the account's timezone, and land in the future.
  • Personalisation can only use merge fields that will actually substitute at send time.

The Desired Outcome

  • A conversational front door, not a new product. The user describes what they want and gets back a complete, valid campaign: drafted, styled, audienced, sender-resolved, schedule-checked.
  • The agent never sends. It assembles a campaign and hands it to the existing pipeline, which still owns quota validation, review, queueing, dispatch, deliverability and per-recipient substitution. None of that is rebuilt.
  • The agent can only do what the platform can already do. Account data is read live, using the user's own credentials. Tenant isolation is inherited, not rebuilt in the AI layer.
  • Every irreversible step is gated in code. The model can propose. Only code decides.
  • Room to change your mind. People revise, backtrack and ask side questions. The system has to absorb that without a rigid workflow.
  • Predictable cost per turn, and visible failure. When a dependency breaks, the system says so instead of quietly running in a weaker mode.
Non-goals: autonomous sending, replacing any existing pipeline stage, being a general-purpose assistant.

Architecture at a Glance

The system breaks into six layers. Each does one job, and only one of them can move money or trigger a send:
Layered guardrails keeping AI-assisted email campaign sending safe, reversible, and gated in code before every send
#ComponentWhat it doesWhat it will not do
1Request Edge & Input SafetyChecks who is asking, limits how often they can ask, and screens the message before any spendTrust a credential it decoded itself; let a hard rule match reach a model
2Turn OrchestrationMakes a turn durable: save it, pause it, survive a restart, resume it. Stops two requests overlapping on one conversationStore a credential in saved state; let a crashed turn keep its lock
3Agent Runtime & ToolbeltUnderstands the user, picks tools, assembles the campaignSend; write long-term user facts; answer a state question from chat history
4Deterministic Send ChainGets human confirmation, caps atomically, claims idempotently, validates the renderTake anything from the model beyond "this user wants to send"
5State, Policy & ObservabilityHolds the truth, prices the work, records what actually happened each turnLet policy move money directly; let a tracing failure break a request
6Existing Campaign PipelineEverything from a valid campaign object onwardChange for this feature at all

The Components

The system is a thin conversational layer over a product we did not change. Three rules hold it together:
  • The model picks tools. Code enforces order. No router, no intent classifier, no phase machine.
  • The tool layer is the product surface. If it is not callable, the agent cannot claim it.
  • Anything irreversible sits behind gates the model cannot reach.

Request Edge and Input Safety

Identity is checked against the platform's own identity service rather than by decoding a credential locally and trusting it. If the check fails, the turn is refused. There is no fallback identity and no assumed account.
The user's credential is passed in per request. It is never written into saved state and never reaches a model. A resumed turn re-authenticates using the credentials of the request that resumed it. Per-user rate limiting also lives here.
Input safety is two stages that fail in opposite directions: a deterministic rule rail runs before any model call, so it costs no latency, no spend and no network. It scores prompt-injection shapes, sensitive-data shapes and harmful-intent shapes, and blocks past a threshold. It fails closed: a hard match stops the turn and the second stage never runs. That second stage is an ML content-safety classifier for the things patterns miss, such as unsafe copy phrased in a way nobody wrote a rule for. It fails open: a timeout or error logs loudly and falls back to the rule-rail verdict instead of breaking the turn.

Turn Orchestration

This layer adds two things the agent runtime does not have: durable checkpoints and a real mid-turn pause. State is saved at each step, so a turn can stop, survive a process restart, and pick up where it left off.
One advisory lock per conversation stops two overlapping requests from interleaving their checkpoints. A second concurrent request fails fast with its own status rather than hanging. The lock is held by the database connection, so if a turn crashes the database reclaims the lock. No cleanup process needed.

Agent Runtime and Toolbelt

One agent holds the whole conversation. It decides each turn which tools to call, in what order, and how many times. Dependencies such as authenticated context and platform clients are injected per run rather than read from global state. That is what makes "the credential is never saved" a rule the code enforces instead of a promise.
Every turn starts with a fact sheet built from the campaign record itself: subject, status, whether a schedule is noted or confirmed, audience counts, sender and domain state, content outline, plus the real current time in the user's timezone. Questions about campaign state are answered from the record, never from the chat log. Chat history records what was said. It does not record what is true.
Tools return a small set of typed results rather than prose:
  • ok — it happened; compact result (ids, counts, never the artifact)
  • needs_input — a specific question; the turn becomes a normal ask-and-wait
  • needs_confirm — pause and show the user a summary
  • error — a stable code, so the UI behaves consistently
Keeping ok results compact matters. Artifacts travel by reference. Putting rendered email into message history fills the context window in a few turns and makes everything after it slower, more expensive and less accurate.
  • Content tools. Drafting returns a typed structure, not an email: a length-bounded subject plus an ordered list of semantic blocks. A malformed generation becomes a validation error with a retry attached rather than a problem further downstream. Blocks carry stable ids, which is what makes targeted editing possible: one instruction against one block instead of regenerating the whole email. A separate pass normalises personalisation fields and rejects invented ones.
  • Styling tools. Turn the semantic structure into inbox-safe HTML while keeping the block ids. Output has to pass a deterministic lint before it is saved: table layout, fully inline styles, fluid widths, readable body size, ids intact. A lint failure triggers a retry with the failure attached. It is not a warning in a log.
  • Media tool. Image generation is slow and expensive, so it never blocks the conversation. The tool returns a placeholder reference right away and resolves it later. That placeholder is load-bearing: pre-send validation rejects any payload that still contains one, so a half-rendered email cannot go out.
  • Platform read tools. Contact groups, sending identities and domains are read live from the platform using the user's credentials. Never cached, never shared across accounts.
  • Campaign state tools. Version history, rollback, fork, switch, and semantic search over past campaigns.
Call sites ask for a role, such as conversation, content or styling. They never name a model. Each role gets its own model on its own cost logic. The conversational loop replays full history plus the tool schemas every turn, so it runs the cheapest model that handles the toolbelt reliably. Content and styling run stronger models and only fire when someone actually asks for an email. Every role has an automatic fallback for throttling and outages, and prefix caching is configured per model family, because families disagree about cache markers in ways that break easily.

Deterministic Send Chain

When a user asks to send, the model contributes exactly one thing: this user wants to send, in this mode, at this time. Everything after that is a fixed chain in code:
  • Explicit human confirmation. Not a prompt instruction. The turn genuinely pauses: state is checkpointed mid-turn, the user is shown exactly what is about to happen, and execution resumes only when their answer arrives. The model cannot talk its way past this gate because it is nowhere near it.
  • Atomic send caps, per user and per account, written as single-statement conditional upserts. The check and the increment are one operation, so two concurrent sends cannot both see "under the limit".
  • Idempotency claim on the outbound call, so a network retry, a double-click or a replayed resume cannot submit the same campaign twice. The claim is two-phase: claim first, record the result second, so a replay arriving mid-flight backs off instead of racing.
  • Render and compliance validation, including the unresolved-placeholder check and the unsubscribe mechanism, which was never a model decision anyway.
Schedule times are re-checked against the clock at confirmation. People take real seconds to answer, and a time that was safely in the future when the prompt appeared can be in the past by the time "yes" comes back.

State, Policy and Observability

State store. Conversation checkpoints, campaign records under optimistic concurrency, idempotency claims and atomic counters, a vector index for campaign recall, and a small set of read-only cross-session facts per user (brand voice, tone, preferred sender). The agent reads those facts. A turn cannot write them. Auto-write is deferred until we have governance for it, because letting a conversation save long-term facts means a pasted instruction could quietly poison someone's context.
Business policy. Entitlements, feature gating and credit weighting live as versioned policy, with the numbers supplied as a generated data file built from a single upstream cost model. Repricing is one edit and a regeneration, not a code change. Three things matter here: policy decides but never moves money; the ledger debit and the send caps are atomic database transactions, because those are concurrency problems and a cacheable policy decision is the wrong tool for a race; and if the policy engine is down, an in-process copy behind the same interface takes over and logs loudly the first time. Entitlement checks run at the tool boundary, before any paid external call.
Observability. One trace per turn, with every model call, tool call, platform call, policy decision and gate verdict nested inside it. Three rules: sensitive data is redacted going into a span and masked again on export; tracing can never break a request, so a span failure just means an untraced turn; and with no credentials configured, the tracing layer is a real no-op.

Downstream: the Existing Campaign Pipeline

Takes a completed campaign object and owns everything after that: quota validation, review, registration, queueing, dispatch, scheduling, send-time optimisation, per-recipient substitution and deliverability. We did not modify it for this feature.

Challenges and How We Solved Them

The Multi-Agent Architecture We Built First

Challenge. The first design was an orchestrator routing between specialists: a generator, an audience resolver, a reformatter, a dispatcher, with phase rules deciding which specialist you could reach from which state. It demoed well, which should have been the warning.
People do not move through phases. They revise the subject three steps after you decided they were done with it, ask to see their contact groups mid-draft, then change their mind and change it back. Every one of those is a routing decision, and every routing decision is a chance to pick the wrong specialist or hand off without the context that made the request make sense.
Resolution. We rebuilt it flat: one agent, one toolbelt, no router, no phase graph. The specialist models did not disappear. They became tools instead of participants, called and validated rather than joining the conversation. The lesson travels: models are good at picking tools and bad at sequencing an approval workflow. Give them the first job and keep the second in code.

Every Edit Regenerated the Whole Email

Challenge. Asking for a green button returned a green button attached to completely new copy.
Resolution. Stop letting the model hand us an email. Drafting returns a typed semantic structure, and styling is a separate step that has to preserve block ids. That gives us a semantic view and a rendered view that stay linked, so an edit can target one block. Editing became its own path: current state plus one instruction, change only what was asked, subject and body targeted separately. Vague refinements like "shorter" or "punchier" name no part of the email, so the system remembers what the last edit touched and defaults to that. Structure is what makes editing possible. A model that hands you a finished artifact leaves you nothing to aim at.

Email HTML Is a Compatibility Problem, Not a Taste Problem

Challenge. Major inbox clients still ignore stylesheet blocks. Fixed pixel widths overflow phones. Models follow explicit style directives most of the time, and "most" is not a specification.
Resolution. A deterministic lint gate between generation and saving. Failing it triggers a retry with the specific failure attached, not a logged warning. Explicit directives from the brief are checked against the rendered output instead of trusted. If code can check a requirement, code should check it. An instruction in a prompt is a preference, not a constraint.

Correcting the Wrong Component

Challenge. When a generated draft failed validation, our first design sent the rejection up to the conversational agent to retry. It looked tidy and did not work at all. That agent only controls the brief, so it could not tell the generator what was actually wrong. Each retry was independent of the last and produced the same kind of defect until the retry budget ran out.
Resolution. Send the correction to the component that made the error, naming the specific problems and what a correct version looks like. This is the most portable lesson here: escalating to a supervisor who does not hold the relevant lever produces motion, not repair.

An Irreversible Action Behind a Probabilistic Planner

Challenge. A model told to confirm before sending will do it almost every time. "Almost" is the whole problem.
Resolution. Confirmation is a real pause in execution, not a prompt rule. State is checkpointed mid-turn and resumes only on an explicit answer, then the fixed gate chain from the deterministic send chain runs. Caps are atomic single statements rather than read-then-write. The model cannot skip a link because it never holds the sequence. The reliable way to stop a model doing something is to put it out of reach, not to ask nicely.

Resuming a Turn Replays Work

Challenge. When a paused turn resumes, the orchestration node re-runs from the top. Everything before the pause happens again: the model is called again, plans are rebuilt, external calls repeat. We confirmed this behaviour before designing around it.
Resolution. Two independent layers. Turn-level memoisation: work done before the pause is claimed and recorded durably, so a later call, including one from a different process after a restart, reads the recorded value instead of re-running. Outbound idempotency: the claim key is built from a canonical serialisation that strips volatile fields and includes the id assigned on first submission, so harmless field reordering maps to the same key and a replay is recognised instead of re-sent. The invariant is the part worth carrying elsewhere: a turn can be replayed any number of times. A send happens once.

Guardrails That Punish Legitimate Users

Challenge. In a marketing tool, classic content-safety concerns are real but rare. The frequent victim is an ordinary user whose perfectly legitimate copy gets blocked by a rail tuned for the rare case.
Resolution. Pick a failure direction per layer, on purpose. The deterministic rail fails closed, because it is cheap and precise. The ML classifier fails open, because a classifier hiccup should not take down a customer's afternoon when several more gates sit behind it. An unreadable verdict defaults to safe; blocking requires an explicit unsafe verdict. The whole layer sits behind an injectable seam so CI runs offline and deterministically, and a deployment missing its classifier config logs a loud one-time warning instead of quietly shipping with a stub. Fail visibly, never quietly. The failure that hurts most is the one nobody chose.

Scope Enforcement Judged the Wrong Thing

Challenge. We first judged the user's message: is this campaign work? Against real conversations, it got ordinary non-adversarial turns wrong at a rate we would not ship. The cause was structural, not a tuning problem. At the moment a user speaks, "is this campaign work?" often has no answer yet. A lot of mid-conversation speech is a fragment that only means something in context: a one-word answer to a question just asked, "never mind, undo that", a good brief phrased casually. Meanwhile, requests wrapped in a plausible pretext got through anyway.
Resolution. Move the judgment to the reply, one step later, where it is cheap and easy. A prefilter means ordinary campaign work never pays for the rail at all, and a wrong verdict replaces a reply nobody asked for instead of blocking a request somebody did. The general form: judge at the point where the evidence exists, not the first point you could. And the hard rule that came out of it: scope enforcement must never refuse a greeting. A message with no request in it is small talk, in any language or spelling, and unfamiliar wording is never on its own a reason to turn someone away. The first message a user sends is the worst one to get wrong.

Defending the Instruction Layer

Challenge. A red-team pass showed that literal attack phrasings were blocked while indirect ones were not: the same request restructured, reframed, or written in another language.
Resolution. Two changes. Detection became family-based rather than phrase-based, matching the shape of a request instead of its wording, across multiple languages. And an output-side backstop inspects the reply rather than the request: even a novel technique still has to produce the protected content, and that content has recognisable fingerprints. You can always argue your way around a prompt. You cannot argue your way around an outbound check. One corollary from the same pass shaped the final design: when refused, a model will sometimes invent a plausible answer instead, and users reasonably believe what they are shown. Declining to reveal is not the same as declining to invent, and making something up is as harmful as leaking it.

The Model Reasoning From the Conversation Instead of the Record

Challenge. Our longest-running class of apparent hallucination was not hallucination. Asked "what time is it scheduled for?", the model answered from chat history, which contains times that were proposed, times that were rejected and times that failed to apply. None of those are facts about a campaign. Separately, the model does not know what day it is and proceeds as though it does: it resolves "tomorrow at 10" against a stale sense of today, lands in the past, then invents a plausible replacement when rejected.
Resolution. Every turn opens with a fact sheet built from the record, carrying the real current time in the user's timezone and an explicit note that the model's own sense of today is unreliable. The fact sheet also encodes distinctions chat history blurs, such as a noted schedule versus a confirmed one, because confusing those means telling someone their campaign is scheduled when nothing will ever be sent. Give the model facts, not recollections. Nearly everything that looked like hallucination was the model reasoning from the conversation when a record was available.

Claiming Work That Never Happened

Challenge. A model can produce a fluent, warm, specific description of work it did not do. Catching that and replacing it with an apology is not enough. A user told "sorry, I didn't actually do that" is still holding nothing.
Resolution. The recovery does the work, rebuilt from the user's own brief so nothing is invented. But acting automatically on a text match is exactly the kind of cleverness that becomes its own bug, so it is fenced on every side. It requires that no record exists, that the claim pattern matched, that the turn trace confirms no relevant tool ran, and that a per-conversation cap has not been hit. The third condition carries the weight, and it exists only because tool calls are traced: a pattern can tell you a claim looks false, but only the trace can tell you nothing happened. If a tool did run and failed, the honest answer is that failure, not a second silent attempt. Repair beats apology, with corroboration and a hard cap.

Concurrency Inside a Single Turn

Challenge. Campaign records use optimistic concurrency, which surfaced a race we would not have predicted: one turn firing several tool calls in parallel, such as set the sender, set the domain, set the reply-to, each reading and writing the same record and each losing to the others.
Resolution. Run a turn's tool calls in sequence. Slightly slower, categorically correct. Overlapping requests on the same conversation are a separate problem, handled by the per-conversation lock. Parallelism inside a turn is a performance choice. Correctness on a shared record is not a choice.

Context Growth and Cost

Challenge. The conversational loop replays history every turn, so context size drives cost, latency and accuracy directly. Naive trimming makes it worse. Windowing by message count can cut between a tool call and its result, and stricter model APIs reject unpaired tool blocks outright: a hard failure caused by pure bookkeeping, mid-conversation, in front of a user.
Resolution. Four things. Artifacts travel by reference and never enter history. The window is repaired on both read and write, dropping orphaned tool blocks and then trimming forward to a real user turn, with a second bound by approximate token volume; anything that falls out is folded into a short running summary of durable facts, never the artifact itself. Models are split by role so the expensive ones only fire on demand. And the stable prefix is cached, configured per model family. Context management is a data-structure problem, not a trimming heuristic. History has pairing rules, and breaking them fails at the worst possible moment.

Debugging an Agent

Challenge. One turn fans out into a model call, several tool calls, multiple platform calls, several gate decisions and possibly a pause. Reading that from interleaved log lines is close to impossible.
Resolution. One trace per turn with everything nested inside it. "It said it scheduled my campaign but nothing happened" is answered by opening the trace and reading which tool ran, what it returned, and which gate stopped the chain. Gate decisions also get their own structured logs, so "how often does this rail fire?" is a query rather than an excavation. Worth saying plainly: at least four mechanisms in this document exist only because tool calls are traced, including the false-claim repair, the scope prefilter, the correction routing and the gate analytics. Instrument before you need it. The instrumentation is what makes the next four features possible.

Summary

The version of this project that sounded exciting was put an agent in the product. The version that shipped was a long exercise in deciding, over and over, which half of the system is allowed to be uncertain.
That is why the architecture diagram is six boxes with one line drawn through the middle of them. A model is genuinely good at understanding someone who says "we're launching a summer collection and I want to tell our customers." It should never be the thing that decides whether thousands of emails go out. The architecture above is what taking both halves of that sentence seriously looks like.

Table of contents

The Desired Outcome


Architecture at a Glance


The Components


Challenges and How We Solved Them


Summary

Join Our Newsletter

Get the latest tech trends, tutorials and expert analysis delivered straight to your inbox.

FAQs about Engineering Mizo

Mizo is a conversational AI agent that SoftSages built inside Mailzzy. Users describe a campaign in plain language, and Mizo drafts, styles, targets, and schedules it, while every irreversible step, like sending, stays gated in code rather than left to the model's judgment.

Because a send is irreversible and outward-facing. Mizo can only propose a send; a fixed chain in code, not a prompt instruction, requires explicit human confirmation, atomic sending caps, an idempotency claim, and a render and compliance check before anything actually goes out.

SoftSages' team tried a multi-agent, phase-based design first. It demoed well but broke down against real conversations, where people revise, backtrack, and change their minds mid-draft. The team rebuilt it as one agent with one toolbelt and no router, keeping specialist models as callable tools instead of independent participants in the conversation.

An idempotency claim sits on the outbound send call, so a network retry, a double-click, or a replayed resume can't submit the same campaign twice. Sending caps are also written as atomic single-statement operations, so two concurrent sends can't both pass the same limit check.

No. Contact groups, sending identities, and domains are read live from the platform using the user's own credentials each time, never cached and never shared across accounts. If Mizo hasn't looked something up, it asks instead of guessing.

Detection is matched to the shape of a request rather than specific wording, across languages, and an output-side check inspects what the model is about to say rather than only the incoming request. The system is also built so a refusal never becomes an invented answer in its place, since users reasonably trust what they're shown.

Each layer is built to fail in a deliberate direction. A cheap, precise rule-based check fails closed and stops the turn. A slower classifier fails open, logging loudly and falling back to the earlier check, so a single dependency outage doesn't block a customer's whole afternoon.

Yes, Mizo is live inside Mailzzy. This article covers the engineering architecture behind it; the companion piece, Why We Built Mizo, covers what it's like to use.