When we first built Emily, one of the things we liked most about the idea was that there was very little to learn.
If you know how to email a colleague, you know how to use her.
Forward something and ask Emily to deal with it. Ask her to arrange a meeting. Tell her to follow up if nobody replies. Send her a document and ask what matters. Set a reminder, research something, or simply ask what your week looks like.
Email made sense because it already carries a surprising amount of useful context. Threads tell us who is involved. Attachments arrive with the conversation they belong to. Delegating something by forwarding it feels familiar, and, perhaps most importantly, email is asynchronous. You can hand work over and get on with something else.
We still think that is one of Emily’s strengths. But “email first” started to become confused with “email only”.
The more we used Emily ourselves, and the more we watched other people use her, the more obvious the awkward moments became. You remember something while walking to a meeting, but opening email feels unnecessary. You are already working through a problem in ChatGPT and realise the next step is to actually email someone. You are deep into a conversation in Claude and need to know whether Emily already has a rule, contact or note that changes what you should do.
In each case, the assistant is useful. The journey to reach her is not.
That led us to a slightly different product question. Instead of asking where people should go to use Emily, we started asking where they already are when they need her.
The answer has turned Emily into something more deliberately multi-channel. You can still email her, but you can now text her from a verified UK mobile number, use Emily directly from ChatGPT, or connect the same account to Claude through MCP. The web dashboard and Emily’s own chat remain there too.
The interesting part was not adding more interfaces. It was making sure we did not accidentally build several different Emilys.

One assistant, several ways in
The architectural principle underneath the new channels is fairly simple. Email, SMS, ChatGPT, Claude and Emily’s own dashboard are entry points. They are not separate assistants. Once identity has been established, the request eventually reaches the same underlying Emily engine. The same rules apply. The same contacts and memory notes are available. The same calendar connections sit underneath it. The same activity history records what happened.
That matters more than it first appears.
Imagine telling Emily by email that you prefer meetings after 10am, then having to explain the same preference again when you text her. Or asking her in ChatGPT what she remembers about a contact and getting a different answer from the one she would give in her own dashboard.
More channels would actually make the product worse if each one accumulated its own version of you. So we treat the channel as context around a request rather than the home of the assistant.
A simplified version of that architecture looks something like this:
type EmilyRequest = {
userId: string;
channel: "email" | "sms" | "dashboard" | "connected_assistant";
message: string;
metadata?: Record<string, unknown>;
};
async function handleRequest(request: EmilyRequest) {
const context = await loadEmilyContext(request.userId);
const result = await runEmily({
message: request.message,
context,
source: request.channel
});
return deliverForChannel(request.channel, result);
}That is not production code, and quite a lot has been removed from it, but it shows the important separation.
The channel tells us where the request came from.
The Emily account tells us who the assistant is working for.
The core engine decides what to do.
Delivery then adapts the result to wherever the conversation is happening.
Keeping those responsibilities separate has made adding new channels considerably less painful than building a new assistant every time.
Texting Emily sounded simpler than it was
SMS was the most obvious new channel. A surprising number of assistant requests are tiny.
“Remind me at four to call the dentist.”
“What does tomorrow look like?”
“Email Priya and say I’m running ten minutes late.”
“Find me half an hour with Sam next week.”
None of those needs a rich interface. They barely need an interface at all.
Once a UK mobile number is linked to an Emily account, the user can send those instructions through the ordinary messaging app on their phone. Emily replies in the same conversation, and a text from that verified number has the same authority as an instruction sent by the account holder through email.
The first complication was obvious: a phone number is not automatically an identity. We could not simply accept an incoming text and assume whoever sent it was allowed to tell Emily to send emails, change meetings or cancel something. So linking the number became part of the security boundary.
The account holder enters their mobile number inside Emily, receives a six digit verification code, and completes the link there. Verification codes expire after ten minutes. Until the number has actually been verified, texts from it are not handed to Emily for processing or stored in the user’s conversation history. Messages themselves are transported by Twilio.
The core check is conceptually quite boring:
async function receiveSms(from: string, body: string) {
const owner = await findVerifiedMobile(from);
if (!owner) {
await sendLinkingInstructions(from);
return;
}
await handleRequest({
userId: owner.userId,
channel: "sms",
message: body
});
}There is more around the real handler, of course. Provider validation, error handling, opt-out behaviour and delivery state do not fit neatly into five lines. But the important rule is there. The message does not decide who the user is. The verified channel does.
That principle becomes much more important as an assistant gains the ability to take actions rather than merely answer questions.
The channel should shape the response, not the capability
SMS created another useful product problem. Email can comfortably contain a detailed research brief, several meeting options or a long draft. A text message should not.
Emily currently keeps SMS responses to roughly 450 characters, spread across up to three linked texts. When an answer needs more room, the text contains the useful short version and Emily sends the full response by email as well. The full exchange still appears in Emily’s dashboard history. Images and attachments are not currently processed through SMS, so those still belong in email. Emily by White Rabbit Foundry
We could have responded by making “Text Emily” a reduced version of the product. We preferred a different approach. The capability remains the same. The presentation changes.
In very simplified form:
async function deliverForChannel(
channel: EmilyRequest["channel"],
result: EmilyResult
) {
if (channel === "sms" && result.text.length > SMS_LIMIT) {
await sendSms(result.userId, summarise(result.text));
await sendEmail(result.userId, result.text);
return;
}
return deliverNormally(channel, result);
}There is something we like about that. A channel does not need to contain the whole experience to be useful. It only needs to carry the right part of the interaction. SMS is excellent for short instructions and quick answers. Email is better for depth. A dashboard is better for managing settings. Trying to force every interaction into every channel would give us consistency at the expense of usefulness. The better version of consistency is that the same Emily understands the request wherever it arrived.
ChatGPT changed the direction of travel
SMS removes friction when you are away from a computer. ChatGPT solves a different problem. Increasingly, the work begins inside an AI conversation. Someone might be researching an organisation, thinking through a proposal, planning their week or drafting a difficult email. ChatGPT can help move the thinking forward, but eventually many conversations reach the same boundary.
The answer is finished.
Now somebody has to do something with it.
That is exactly the boundary Emily was originally designed to cross.
Emily is now available through ChatGPT’s plugin system. Once an Emily account has been connected, ChatGPT can read supported Emily information such as rules, automations, recent activity, contacts, memory notes, stored file details and usage, or ask Emily to carry out work such as sending an email or arranging a meeting.
This makes for a more interesting interaction than simply placing two chatbots beside one another.
You might be working through next week in ChatGPT and ask: Ask Emily what my calendar looks like on Thursday.
Or:
What does Emily remember about Sam?
Then:
Ask Emily to suggest Thursday afternoon and send him an email.
ChatGPT remains the place where the broader conversation is happening. Emily becomes the connection to your actual assistant context and actions.
OpenAI’s current plugin architecture is explicitly designed around this model, where plugins can contain connected tools and external account access, while the underlying provider still controls authentication and permissions. For authenticated MCP services with customer data or write actions, OpenAI’s technical guidance expects server-side authorisation rather than relying on the model to decide who should have access. That principle shaped the Emily integration too.
Never trust the assistant to tell you which account it belongs to
This is one of the less glamorous lessons from connected AI products. A tool call might contain a user identifier. We do not trust it.
The account Emily exposes is derived from the authorisation established when the user signs into Emily’s own consent flow. The connected assistant never gets to choose which Emily account it would like to access. Emily’s public documentation makes that explicit: the account is determined by the sign-in, not by something the external assistant sends.
The simplified pattern is closer to:
async function handleToolCall(
accessToken: string,
input: ToolInput
) {
const auth = await verifyAccessToken(accessToken);
// Identity comes from the verified authorisation.
const userId = auth.subject;
return runTool({
userId,
input
});
}Not this:
// Don't do this.
const userId = input.userId;It seems almost comically obvious when written down. Unfortunately, a lot of security decisions are obvious only after someone has made the opposite choice. Identity should come from the credential, not from the model.
Read and act are different permissions
We also did not want “connect Emily” to become one enormous permission. Looking up information and causing something to happen are different levels of trust.
For ChatGPT, Emily currently exposes separate permissions for reading Emily information and asking Emily to answer or act. ChatGPT can additionally request the standard identity permissions it needs to tie the connection to the verified account. Claude uses the two Emily specific permissions rather than the additional ChatGPT identity scopes.
Conceptually:
const SCOPES = {
READ: "emily:read",
CHAT: "emily:chat"
};
function requireScope(auth: Authorisation, scope: string) {
if (!auth.scopes.includes(scope)) {
throw new ForbiddenError();
}
}The point is not the string names. It is that permissions should describe what the connected system is actually allowed to do.
emily:readcan expose supported account context such as rules, automations, contacts and memory notes.emily:chatcan hand work to the assistant engine.
Some operations remain outside connected assistants completely. Purchases, billing changes, paid address changes and deleting the Emily account stay on Emily’s own site. File contents are also not exposed directly to connected assistants, even though supported metadata about saved files can be viewed.
We could technically expose more. That is not the same thing as having a reason to.
Preparing an action is not the same as carrying it out
The most important product decision came once connected assistants could do real work.
If someone says in ChatGPT or Claude: Ask Emily to email Sam and suggest Thursday. Should the email immediately leave the building? We decided no.
For consequential actions, Emily first prepares what she intends to do. The connected assistant presents that plan to the user, and execution only happens after confirmation. The resulting action then appears in Emily’s Activity log as work originating through a connected assistant.
This created a two stage pattern throughout the integration:
async function prepareAction(
userId: string,
instruction: string
) {
const plan = await emily.prepare(userId, instruction);
return {
planId: plan.id,
summary: plan.summary,
requiresConfirmation: true
};
}
async function confirmAction(
userId: string,
planId: string
) {
const plan = await loadPreparedPlan(userId, planId);
return executeOnce(plan.id, () =>
emily.execute(userId, plan)
);
}Again, this is illustrative rather than verbatim production code. The pattern is the useful bit. Prepare. Show. Confirm. Execute. There is a temptation in AI products to treat every confirmation as friction that needs to be engineered away. We do not think that is always right.
Asking an assistant what meetings are tomorrow and asking it to email a client are different things. It is perfectly reasonable for the first to feel almost invisible and the second to have a clear moment where the user sees what is about to happen.
Good friction can create trust. Then we had to make confirmation safe to retry Connected systems have another irritating habit. They retry things. A network request times out. The calling platform is not sure whether the operation completed, so it sends the same request again.
That behaviour is perfectly sensible for reading information. It becomes much less charming when the action is “send this email”. Emily therefore treats prepared actions as idempotent. An exact retry after a timeout does not execute the same confirmed action twice.
The simplified idea is:
async function executeOnce(
actionId: string,
operation: () => Promise<unknown>
) {
const existing = await findExecution(actionId);
if (existing) {
return existing.result;
}
const result = await operation();
await saveExecution(actionId, result);
return result;
}The real implementation needs to handle concurrency rather more carefully than that snippet suggests, but the principle is worth sharing. Any external action that might be retried should have a stable identity. “Probably only runs once” is not a particularly reassuring property for an assistant with permission to send things.
Claude showed us why a standard matters
ChatGPT gave Emily a distribution channel through its plugin directory. Claude made the architectural benefit of MCP much clearer.
Rather than building a completely separate “Emily for Claude” backend, Claude connects to Emily’s existing remote MCP server as a custom connector. The user authenticates on Emily’s own site, reviews the permissions and then works with Emily from inside Claude. Claude never receives the Emily password.
MCP, the Model Context Protocol, is an open standard for connecting AI applications to external tools and data. Anthropic describes it as a standard interface between AI applications and the systems around them, while its remote connector support allows web services to expose those capabilities without requiring a local server on the user’s machine.
For us, the benefit is fairly practical.
Instead of thinking: How do we build Emily for every new assistant?
We can think: How do we expose Emily’s capabilities properly once?
That is a much healthier architecture.
Claude becomes another client of the same capabilities rather than a fork of the product. It also means the security model can stay familiar. The user signs into Emily, sees the permissions, and can revoke Claude separately later. For actions that send, book or change something, Emily still prepares a plan and waits for confirmation. Emily by White Rabbit Foundry
Anthropic’s own guidance for remote MCP connectors recommends exactly the kinds of boundaries we care about: connect only to trusted servers, review OAuth permissions, pay particular attention to tools that can modify external systems, and keep approval around consequential actions. Claude Help Center
Standards do not remove the need for product decisions. They do mean we do not have to reinvent the plumbing every time.
The hardest part was keeping context coherent
The technical work around authentication and permissions is visible because it has obvious security implications. The less visible challenge is context. Emily already has a growing set of things that can shape a response: memory notes, saved contacts, user preferences, rules, automations, connected calendars, activity and the conversational context around a particular request. Adding channels creates a risk that each one develops its own partial version of that world.
We deliberately avoided that.
An SMS request does not get “SMS memory”.
A connected assistant does not get “ChatGPT memory”.
A request from Claude does not start a parallel profile.
Once the account is identified, Emily loads the account context she needs from the same underlying sources.
Conceptually:
async function loadEmilyContext(userId: string) {
const [
profile,
memories,
contacts,
rules,
calendars,
recentActivity
] = await Promise.all([
getProfile(userId),
getMemoryNotes(userId),
getContacts(userId),
getRules(userId),
getCalendarContext(userId),
getRecentActivity(userId)
]);
return {
profile,
memories,
contacts,
rules,
calendars,
recentActivity
};
}Not every request needs all of that information, and the production system is more selective than this example. The principle remains the same… context belongs to Emily, not to the channel. That is the part that makes multi-channel feel like one product rather than a collection of integrations. The interface can disappear without accountability disappearing with it There is an interesting tension in all of this.
The better an assistant becomes at appearing wherever you need it, the less visible the underlying product becomes. That is useful. It is also potentially dangerous. Traditional applications make context obvious. You opened a particular app. You signed into a particular account. You pressed a particular button.
With a multi-channel assistant, the transition between thinking and acting can become much less visible. That is why we have actually added more accountability underneath the simpler experience.
SMS numbers have to be verified before they carry authority. Connected assistants authenticate through Emily’s own consent flow rather than receiving a password. Authorisations can be viewed and revoked independently. Credentials are checked on every request. Emily’s documentation says issued credentials are short lived, only one way hashes are stored, and replay of a renewal credential causes the authorisation to be revoked.
Activity is recorded too. If a meeting was booked because somebody asked through Claude, it should not become a mystery later why the meeting exists. The interface can become lighter. The audit trail should not. Not every channel needs every feature
Another thing we learned was to resist the desire for absolute parity. It sounds appealing to say every Emily channel can do everything. It would also make some of them worse.
SMS currently does not handle image or file attachments. Longer responses sensibly move to email. Billing and account deletion stay on Emily’s website rather than being exposed to connected assistants. Claude and ChatGPT can see supported metadata about saved files, but not simply pull the contents of every file through the connector.
These are constraints, but we do not think of them as missing features. They are part of deciding what a channel is for. A text message is brilliant when you remember something on the Tube. It is not where we would choose to review a twenty page proposal. ChatGPT is useful when the work has already begun there and Emily can provide action or account context.
Emily’s own settings remain the right place for managing things that affect the account itself. The goal is not identical surfaces. It is continuity.
Multi-channel has changed how Emily feels
This is probably the part that matters most. Before the new channels, there was sometimes a small piece of mental bookkeeping involved in using Emily. You had to remember to go to her. That could mean opening email, finding the address, moving information across or changing tools halfway through another task. Those moments sound trivial. Most product friction does when described individually. The cumulative effect is larger.
Texting Emily means a reminder can be delegated at the moment you remember it. The ChatGPT plugin means a conversation about a client can turn into an actual email or meeting without re-establishing everything somewhere else. Claude can hand work into the same Emily account while retaining the rules, contacts and memory that make her useful in the first place. The product has not become dramatically more capable because we added three interfaces. It has become easier to reach at the moment the capability matters. That is a different kind of improvement.
We increasingly think assistants will be infrastructure
When Emily began, we described her as an AI assistant that lives in your inbox. We still like that description because email remains an unusually natural way to delegate work. But it is becoming incomplete. Emily increasingly behaves less like a destination and more like infrastructure sitting behind several destinations.
The visible interface could be an email thread.
It could be a text.
It could be ChatGPT.
It could be Claude.
In the future, it could be something else entirely.
What matters is that the assistant underneath remains recognisable. It should know the same user, respect the same boundaries, retain the same context and leave the same trail of what it has done. This is one reason MCP is interesting beyond the technical implementation. It points towards a world where useful capabilities do not necessarily need to own the interface through which they are invoked.
Software spent years encouraging us to accumulate applications. AI may push some of that in the opposite direction. We may increasingly spend time in a smaller number of conversational environments while calling specialist products when we need them. In that world, a product like Emily does not become less important because you see less of its interface. Quite possibly the opposite.
A final thought
Adding channels initially sounded like a distribution project. It became an architecture and trust project instead.
SMS forced us to think carefully about when a phone number becomes an authorised identity and what belongs in a text message.
ChatGPT forced a clean distinction between reading information and carrying out work, then between preparing an action and actually executing it.
Claude reinforced the value of building on an open protocol rather than writing a bespoke integration for every assistant that appears.
All three reminded us that memory and context need to belong to the assistant itself, otherwise every new interface fragments the experience it was supposed to improve.
The end result is, hopefully, much less dramatic for the person using it. You think of something. You ask Emily. The particular box you happen to type into matters a little less than it used to. That feels like the direction assistants should be moving in. Not another application demanding a place in the workflow. Something useful that can meet you inside the workflow you already have.
Emily still lives in your inbox. She just does not expect you to be there every time you need her.