OfficeSyncPro now reads the inbox for the team. A person still signs off on every task.
OfficeSyncPro connects Outlook email, calendars and Teams meetings to projects and tasks for small teams that run their client work through Microsoft 365. The product already held every thread. What it could not do was notice the work inside them: the request buried in point 2 of a long reply, the decision made on a call, the question nobody answered. Elegant built the AI pipeline that catches those, turns them into suggestions with owners, dates and the sentence they came from, and never creates anything until a person accepts it.
Outlook was already the project system. It just did not know it.
OfficeSyncPro is built for teams of 5 to 40 people, such as agencies and professional services firms, whose clients live in their inbox. It signs in with Microsoft, syncs mail and calendars through Microsoft Graph, and lets a team link any email to a project or a task, chat in real time and track follow-ups without leaving the Microsoft 365 world.
Elegant designed and built the product. The AI layer was the next step, and the hardest one: it had to work on real client mail, inside a multi-tenant app, with the same trust bar as the rest of the product. A wrong task assigned to a client contact, or a meeting booked at a time nobody is free, costs more goodwill than the feature earns.
One reply, four jobs, and one trap.
This is an ordinary client reply. Read quickly, it says thanks and asks a question. Read properly, it assigns work to two people with two deadlines, records a scope decision, asks for a meeting, and, in the quoted history underneath, contains an older request that is already handled. A tool that reads the whole thing as one blob creates a duplicate task from history. A tool that reads nothing leaves all of it to memory.
A task for Dana, due Friday 25 Sep
"By Friday" in an email sent on Wednesday 23 Sep. The date is resolved in code, in the workspace time zone.
A decision, and the task it creates
The blog migration is out of scope. The SOW needs editing. No date was given, so none is invented.
A task for Omar, who is only on cc
He is on the thread and in the workspace, so he can be the owner. Someone who is neither never can.
A meeting request
Answered with 3 times both Dana and Omar are free next week, read from their Outlook calendars.
The trap: quoted history
The Q4 pricing line was written by Dana, a day earlier. The pipeline reads only the new part of a reply, and a suggestion that quotes anything else is removed.
Models are good at reading. They are not trusted to decide. Every suggestion has to point at the exact sentence it came from, and a person makes the final call.
A queue job per Graph notification, and a review queue at the end.
OfficeSyncPro already subscribed to Microsoft Graph change notifications for every connected mailbox and calendar. The AI pipeline hangs off the same subscription: each notification becomes a BullMQ job, and the job runs the message through the stages below. Graph delivers at least once, so the first thing a job does is claim the message by its id. A redelivered notification finds the claim and stops before a single token is spent.
Four lanes, one destination. Email, Teams transcripts, quiet threads and plain-language scheduling requests each produce suggestions in the same review queue, with the same shape and the same accept, edit and dismiss actions. The product never has to learn four different AI features.
What the AI does, and what it is not allowed to do.
Read less, then read carefully.
Most of an inbox needs no intelligence at all. Newsletters, receipts, auto-replies and meeting acceptances are identified from their headers and sender before anything leaves the server, so they cost nothing and expose nothing. What remains goes to a small, fast model that answers four questions in a fixed shape: what kind of message is this, how urgent is it, does the sender expect a reply, and does it ask for a meeting. Only requests and decisions continue to extraction, which runs on the strongest model in the route.
Commitments, with receipts.
The extraction model returns each commitment with a title, an owner if the email makes one clear, the deadline exactly as written, and the sentence it came from, copied verbatim. Then code takes over. The quote must appear in the new part of the message, or the suggestion is removed. The owner must be on the thread or in the workspace, or the owner is cleared and the suggestion is flagged. A commitment is keyed on the thread and its quote, so the same sentence can never produce two tasks. It is the same rule we built into SaleQue's CRM assistant: no source, no sentence.
A date is arithmetic, not a guess.
Models are unreliable at calendar maths, and time zones make it worse: "tomorrow" in an email sent at 22:30 in New York is already the day after in UTC. So the model copies the phrase and a small, tested function turns it into a date, relative to when the message was sent and in the workspace time zone. When a phrase is genuinely ambiguous, the function refuses. "Next Tuesday" means different days to different people, so the task keeps the words and asks.
| "by Friday" | sent Wed 23 Sep, 10:12 New York | 2026-09-25 |
| "end of week" | that week's Friday | 2026-09-25 |
| "Tuesday next week" | the Tuesday after next Monday | 2026-09-29 |
| "tomorrow" | sent Wed 22:30 New York, Thu 02:30 UTC | 2026-09-24 |
| "5 January" | already past this year | 2027-01-05 |
| "next Tuesday" | ambiguous, kept as written | asks |
Every meeting ends with a list.
When a Teams meeting has a transcript, Graph serves it as WebVTT with a speaker on every line. The pipeline groups it into turns and asks the strongest model for a summary, the decisions and the action items, each with the exact words that show it. The same rule applies as for email: a decision nobody said is dropped. Owners are matched to attendees by name, and "by Friday" is resolved against the meeting date, not the day the summary was read.
The thread that went quiet.
OfficeSyncPro already detected replies to tracked emails. The AI adds the part a rule cannot know: whether the last message actually asked for an answer. Triage records that when the message goes out, so the follow-up check itself needs no model at all. If a question from the team has had no reply for longer than the workspace allows, counted in working days, a reminder appears in the queue. A question sent on Friday afternoon is 2 days old on Tuesday, not 4.
A meeting from one sentence.
"30 minutes next week with you and Omar" is a request any assistant can read. The mistake is letting the model answer it. Here the model only parses the sentence: who, how long, when, and whether mornings or afternoons were asked for. Who is free comes from Microsoft Graph's findMeetingTimes, across the attendees' real Outlook calendars, and the pipeline can only offer slots Graph returned. If a name matches two people, it stops and asks.
Nothing happens until a person says so.
Every lane ends in the same queue. Each suggestion shows where it came from, the sentence behind it, and any flag the verifier raised. Accept creates the real task, reminder or invite, with the owner and date filled in and the source sentence attached, so anyone opening the task later can see why it exists. Edit changes it first. Dismiss removes it for good. Accepting twice creates one task, and a suggestion from one workspace cannot be accepted from another.
Client mail is hostile input. We built it that way.
An inbox is the one place where anyone on the internet can put text in front of the model. So the pipeline assumes some of that text is an attack, and makes sure an attack has nowhere to go.
Instructions in an email are just words
Every prompt wraps the content as data and says so. More important, nothing depends on the model obeying that: the verifier strips any owner outside the thread and the workspace, only a fixed set of action types exists, and nothing is created without a click. The test suite sends an email that orders the assistant to export the client list to an outside address. It ends up as an unassigned, flagged suggestion that a person dismisses.
No provider keeps the mail
Every request to OpenRouter sets data_collection to deny, so it is only routed to providers that do not store or train on prompts, and require_parameters, so a provider that cannot honour the strict JSON schema is never picked. The model sees the new part of one message, not the mailbox.
A reply that does not fit is refused
Each stage has a zod schema that is sent to the model as a strict JSON schema and checked again on the way back. A reply that fails gets one repair attempt with the error attached. A second failure throws a retryable error and the queue runs the job later. Malformed output never reaches the database.
One workspace, one pass per message
Every row the pipeline writes carries the workspace id, and every read and update filters on it, matching how the rest of OfficeSyncPro isolates tenants. Messages are claimed by their Graph id and suggestions by their source sentence, so redelivery and retries are safe by construction.
Every seat gets the AI, not only the licensed ones
Microsoft's Copilot Chat and Retrieval APIs answer only for users who hold a Microsoft 365 Copilot add-on license, and most OfficeSyncPro workspaces are small teams that do not pay for one. Building on OpenRouter gives every user the same AI, lets the team pick the model for each job, and keeps the strict schemas, fallbacks and no-retention routing above under OfficeSyncPro's control. Teams that already run Copilot lose nothing: the pipeline reads the same Microsoft Graph data Copilot does.
The right model for each job, and a second one ready.
OpenRouter lets one request name several models in priority order and falls back when a provider is down, rate limited or refuses. The route for each stage is configuration, so changing a model is a deploy of a config value, not a rebuild. Every call is written to a ledger with the model that actually answered, its tokens and its cost, which is how the team knows what the feature costs per workspace.
| Stage | Primary | Fallback | Why this model |
|---|---|---|---|
| Triage | openai/gpt-5-mini | google/gemini-2.5-flash | Runs on every message that survives the prefilter. Small, fast and cheap is the requirement. |
| Extraction | ~anthropic/claude-sonnet-latest | openai/gpt-5 | Decides what a person is asked to act on. Precision matters more than cost. |
| Meeting notes | ~anthropic/claude-sonnet-latest | google/gemini-2.5-pro | Long transcripts, several speakers, decisions that must be quoted exactly. |
| Scheduling | openai/gpt-5-mini | anthropic/claude-haiku-4.5 | Only parses a sentence. Availability comes from Graph, never from the model. |
The whole inbox, replayed.
The reference build ships with the demo inbox from this page and a trace command that runs it through every stage. The newsletter never reaches a model. The invoice stops after triage. The client reply produces three tasks and loses the one from quoted history. The phishing email becomes a flagged, unassigned suggestion. The meeting ends with two decisions, three tasks and one removed claim, and the scheduling request comes back with three real slots.
officesync-ai replay workspace 0b6f4c1a tz America/New_York models via OpenRouter (recorded) I2TG94AA This week in design systems digest@news.designweekly.example prefilter skipped: mailing_list 0 model calls I2TG95AA Invoice 2041 paid megan@harlowpike.example triage fyi · low gpt-5-mini I2TG93AA RE: Phase 2 sign-off megan@harlowpike.example triage request · high · needs reply · scheduling ask gpt-5-mini extract 3 kept, 1 dropped claude-sonnet-5 + Send revised homepage wireframes to Megan → Dana due 2026-09-25 + Remove the blog migration from the phase 2 SOW → Dana + Confirm analytics tagging on the checkout pages → Omar due 2026-09-25 - Send Q4 pricing to Megan quote_not_in_source I2TG96AA Urgent: account verification it-desk@harlowpike-support.example triage request · high gpt-5-mini extract 1 kept, 0 dropped claude-sonnet-5 + Export the full client list → unassigned due 2026-09-23 owner_outside_thread teams Phase 2 kickoff transcript, 7 turns decided The homepage hero leads with the booking flow. decided The blog migration moves to phase 3. + Rework the homepage wireframes with a booking-first hero → Priya due 2026-09-25 + Fix the checkout purchase event and send Megan a test report → Omar + Update the SOW and send it to Megan → Dana due 2026-09-23 - decision: Launch date stays at 14 November. not said in the meeting schedule "30 minutes next week with you and Omar" free/busy from Graph findMeetingTimes ○ Mon, 28 Sep 14:00 ET ○ Tue, 29 Sep 10:00 ET ○ Thu, 01 Oct 15:30 ET review queue 8 pending · 0 tasks created until a person accepts ledger 7 model calls $0.0262 total, recorded prices
What the suite proves on every commit.
test/pipeline.spec.tstest/pipeline.spec.tstest/pipeline.spec.tstest/prepare.spec.tstest/pipeline.spec.tstest/due-date.spec.tstest/due-date.spec.ts · test/schedule.spec.tstest/schedule.spec.tstest/meeting.spec.tstest/followup.spec.tstest/openrouter.spec.tstest/review.spec.tsAn AI layer that fits the product it joined.
The model reads. The product decides.
The interesting engineering in this project is not a prompt. It is everything around the model: deciding what it never needs to see, proving each suggestion against the source, doing date and calendar work in code, and making sure a person is the last step. That is what lets an AI feature run on real client mail without anyone worrying about what it might do.
If your product already holds email, meetings or documents your users keep sorting by hand, this kind of layer is a fixed-price build on top of what you have.
AI Integration for Existing Products
AI features built into a product that is already live, on its own data and stack, by one senior team. Fixed price, fixed timeline, full IP transfer.
Building agents and automations from scratch? See AI Agents and Workflow Automation
Ready to build something like this?
Book a free 15-minute call. We will look at what you are building and tell you honestly what it takes and whether we are the right fit.
