Draft technical appendix. This document is the implementation-facing companion to the Copilot Concierge scope study. Signatures, scopes, and schemas shown here are the design contract for Phase 1 build starting the week of April 27, 2026. The surface-facing narrative is in the scope study.
Daniel Lepel | M365 & Azure Architect
Copilot Concierge: Technical Appendix
The implementation contract behind the scope study: scopes, tools, schemas, prompts.
The scope study is the product. This document is the build. Every tool signature, every Graph permission, every Dataverse table, every JSON schema the Phase 1 implementation needs to stand up a working single-tenant demo. Written for the technical interviewer, the tenant admin reviewing consent, and the developer on the other side of the handoff. No marketing narrative. No em-dashes. Just the contract.
Companion document
Phase 1 implementation scope
Runtime: Azure AI Foundry | Python 3.11
Surface: Copilot Studio agent
Appendix A | Graph API scopes
Least-privilege permission set, staged by phase.
Phase 1 requests the minimum. Each later phase adds only what it needs.
Every scope below is a delegated permission on the signed-in user. No application-level Graph permissions are requested in the single-tenant demo. Delegated scopes are easier for a tenant admin to reason about during consent, and they keep the blast radius bounded to the operator's own mailbox and chats. Application scopes become relevant in the multi-tenant packaging work, which is out of scope for this document.
Mail.ReadWrite
Phase 1
Read mail for classification. Stage drafts. Move to Noise folder on auto-archive. The minimum required scope for the Outlook triage pipeline.
Mail.Send
Phase 1
Auto-send for categories where the user opted in. Gated by the per-category toggle plus sender override plus confidence threshold. Never invoked when the toggle is off.
MailboxSettings.Read
Phase 1
Read the user's timezone, working hours, and automatic-reply state. Feeds the calendar-adjacency classifier feature and suppresses drafts when the user is out of office.
Calendars.Read
Phase 1
Read the next 48 hours of calendar events. Used as a feature in the classifier: a sender named in a meeting starting within two hours escalates the category.
User.Read
Phase 1
Read the signed-in user's basic profile. Populates the settings page header and ties audit rows to the correct user identity.
Chat.ReadWrite
Phase 2
Read Teams one-on-one chats and at-mentions for the unified triage queue. Stage draft replies in the same chat. No channel write access under this scope.
ChannelMessage.Read.All
Phase 3
Read-only across followed channels for the morning digest and on-demand summaries. Intentionally scoped without a matching write permission. Channels stay read-only by design.
Tasks.ReadWrite
Phase 4
Create and update Microsoft To-Do tasks extracted from Outlook mail, Teams chats, and at-mentions. Write-back is required; no broader task-list permission is requested.
Files.ReadWrite.AppFolder
Phase 1
Read and write the voice profile JSON in the agent's app folder in the user's OneDrive. Limited to the app folder only; no broader OneDrive access requested.
Consent posture
All scopes above are delegated and request admin consent at tenant enrollment. Mail.Send is flagged for the admin review because it permits programmatic sending; the agent never sends without the per-category toggle explicitly on. ChannelMessage.Read.All is the broadest scope and is deferred to Phase 3 so Phase 1 and Phase 2 can ship with a narrower permission set.
Appendix C | Dataverse data model
The four tables the settings page reads and writes.
Dataverse is the config store. Copilot Studio uses it natively; the Foundry agent reads it via Dataverse connector.
Four tables cover Phase 1. Two more are added for Phase 3 (channel subscriptions) and Phase 4 (To-Do list binding). All tables carry a composite key of tenant_id plus user_upn so the schema is ready for multi-tenant packaging without a migration.
Table | concierge_user_settings
CREATE TABLE concierge_user_settings (
tenant_id string -- composite key, tenant identifier
user_upn string -- composite key, user principal name
cat_critical_toggle enum -- draft_only | auto_on
cat_urgent_toggle enum -- draft_only | auto_on
cat_important_toggle enum -- draft_only | auto_on
cat_normal_toggle enum -- draft_only | auto_on
cat_fyi_toggle enum -- draft_only | auto_on (mark_read is auto_on)
cat_noise_toggle enum -- archive_on | archive_off
cat_threshold json -- {category: float 0..1} confidence floor per category
digest_time_utc time -- morning digest delivery time
teams_channel_digest_on bool
todo_integration_on bool
voice_profile_active bool
created_at datetime
updated_at datetime
);
Table | concierge_sender_override
CREATE TABLE concierge_sender_override (
tenant_id string -- composite key
user_upn string -- composite key
sender_pattern string -- exact email, or domain suffix (@contoso.com)
rule_type enum -- vip | blacklist
category_floor enum | null -- vip only; min category to classify at
force_draft_only bool -- if true, never auto-send regardless of toggle
note string -- user-entered justification, shown in settings
created_at datetime
);
Table | concierge_channel_subscription (Phase 3)
CREATE TABLE concierge_channel_subscription (
tenant_id string
user_upn string
team_id string
channel_id string
include_in_digestbool
last_summarized datetime
);
Table | concierge_todo_binding (Phase 4)
CREATE TABLE concierge_todo_binding (
tenant_id string
user_upn string
todo_list_id string -- bound To-Do list; defaults to "Tasks"
tag_prefix string -- default "concierge:"; user can override
);
Why Dataverse, not Azure SQL
Copilot Studio already reads and writes Dataverse natively; adding a separate SQL backing store would pull the tenant admin into a second data residency review for no added capability. Dataverse also handles the row-level security by default: the signed-in user only ever reads their own rows without extra filter logic in the agent. The tradeoff is query flexibility; for the four tables above, Dataverse filtering is sufficient.
Appendix D | Voice profile schema
The per-user profile, as a single JSON document.
Lives in OneDrive. Editable by the user. Regenerated on demand from the 15 most recent Sent Items.
The voice profile is one JSON document per user, stored in the agent's OneDrive app folder at /approot/voice_profile.json. The user can open it, read it, and hand-edit it. Every field has a default so a missing or unparseable profile falls back gracefully to the universal filter alone.
Schema | voice_profile.json
{
"schema_version": "1.0",
"user_upn": "jane.doe@contoso.example",
"generated_at": "2026-04-28T09:14:00Z",
"sample_size": 15, // Sent Items count sampled
"tone": {
"register": "plain-direct", // plain-direct | warm-collegial | formal-executive
"avg_sentence_length_words": 14,
"hedging_frequency": "low", // low | medium | high
"contraction_usage": "natural", // avoid | natural | heavy
"humor": "dry" // none | dry | warm
},
"structure": {
"opens_with_greeting": true,
"typical_greeting": "Hi {firstname},",
"typical_signoff": "Daniel",
"uses_bullet_lists": "when-useful", // rarely | when-useful | often
"paragraph_density": "short" // short | medium | dense
},
"vocabulary": {
"preferred_phrases": [
"happy to", "quick note", "let me know", "checking on"
],
"banned_phrases": [
"seamless", "leverage", "robust", "ensure", "utilize",
"spearhead", "innovative", "transformative"
],
"punctuation_rules": {
"no_em_dashes": true,
"no_double_hyphens": true,
"serial_comma": true
}
},
"forbidden_patterns": [
"X isn't just about Y", // negation-contrast
"From X to Y", // scope framing
"Whether X or Y" // parallel padding
],
"examples": [
{
"context": "short_ack",
"sample": "Got it, will circle back by EOD Thursday."
},
{
"context": "meeting_confirm",
"sample": "Works for me. I'll send an invite for 2:00."
},
{
"context": "polite_decline",
"sample": "Thanks for thinking of me. Not a fit right now, but please keep me on the list."
}
],
"overrides": {
"user_edited": false, // true once the user hand-edits any field
"last_edited_at": null
}
}
The profile_trainer.py service regenerates the profile on demand. Regeneration reads the 15 most recent items from Sent Items, strips quoted reply content and signatures, and calls Claude via Foundry with a dedicated profile-extraction prompt that returns this JSON shape. If the user has manually edited the profile (overrides.user_edited = true), regeneration preserves the edited fields and only refreshes the computed fields the user hasn't touched.
Appendix E | Audit log schema
One custom event per tool call.
Application Insights is the store. Kusto is the query surface. The settings page reads from the same workspace.
Every tool call in the agent pipeline emits one custom event to the concierge Application Insights workspace. Events carry a correlation ID so all tool calls for one inbound conversation thread under the same trace. The user-facing audit view runs a Kusto query against the same workspace; there is no separate audit table.
event_name
string
Tool call name (e.g. classify_conversation, stage_draft). Always present.
correlation_id
string
UUID issued at the webhook arrival step. All tool calls for one inbound message share the same correlation ID.
tenant_id
string
Tenant identifier from the signed-in user context.
user_upn
string
User principal name of the signed-in user. Audit view filters on this field.
conversation_id
string
Graph message ID or chat ID for the inbound item under triage.
category
string | null
One of Critical, Urgent, Important, Normal, FYI, Noise. Null for events that run before classification.
confidence
float | null
0 to 1. Present on classify_conversation events and on any downstream event that consumed the score.
reason
string
One-line model-generated reason. Shown in the user audit view.
action_decision
string
stage_draft, auto_send, mark_read, archive, no_action, blocked. Logged by the rule engine.
action_path
string
How the decision was reached: category_toggle, sender_override, confidence_floor, lint_block, or preflight_cancel.
revocable
bool
True if the user can revoke the action from the audit view. False for actions the agent cannot undo (e.g. sends delivered to external recipients).
draft_hash
string | null
SHA-256 of the draft body. Lets the audit view detect drafts the user manually edited before sending.
latency_ms
int
Elapsed time for the tool call, in milliseconds. Populates the performance dashboard.
error_code
string | null
Populated on failure paths only. See the full error taxonomy in rule_engine.py.
ts_utc
datetime
Event timestamp in UTC. Audit view converts to the user's timezone from mailbox settings.
Sample Kusto | recent actions for one user
customEvents
| where name in ("stage_draft", "send_reply", "archive")
| where customDimensions.user_upn == "jane.doe@contoso.example"
| where timestamp > ago(24h)
| project
ts = timestamp,
event = name,
category = customDimensions.category,
conf = customDimensions.confidence,
path = customDimensions.action_path,
rev = customDimensions.revocable,
reason = customDimensions.reason
| order by ts desc
Appendix F | Classification prompt structure
The template behind classify_conversation.
One system prompt, one user prompt, one strict JSON response format.
The classification call to Claude follows a fixed template. The system prompt pins the six categories, their definitions, and the output format. The user prompt packs the conversation context, calendar adjacency, and sender rules. The model returns strict JSON only. Any non-JSON response is a hard error and the conversation falls back to a manual-review category with the user notified.
System prompt | classify_conversation
You are Copilot Concierge, a triage classifier for inbound M365 messages.
Your only output is one JSON object matching the schema below. No prose.
Categories, in priority order:
- CRITICAL: Explicit deadline today, escalation language, blocking someone,
or named in a meeting starting within 2 hours.
- URGENT: Deadline this week, repeated follow-ups, or strong role signals.
- IMPORTANT: Substantive request needing thought, no immediate pressure.
- NORMAL: Standard correspondence. Answer when convenient.
- FYI: Informational. No response expected.
- NOISE: Newsletters, promos, automated. No action.
Signals to weight:
1. Explicit deadlines in the thread body.
2. Sender role, history, and whether the sender is in the user's VIP list.
3. Calendar adjacency: sender named in a meeting within 2 hours escalates.
4. Thread position: reply to a long thread escalates when the user is the
last non-sender party.
5. Language features: escalation words, blocking phrasing, at-mentions.
Respond with:
{
"category": "CRITICAL|URGENT|IMPORTANT|NORMAL|FYI|NOISE",
"confidence": 0.0-1.0,
"reason": "<= 140 chars, one sentence, user-facing"
}
User prompt | classify_conversation (template)
CONVERSATION
From: {sender_name} <{sender_email}>
Subject: {subject}
Arrived: {arrived_utc}
Thread length: {thread_count}
{body_truncated_to_4000_chars}
SENDER CONTEXT
VIP list: {vip_membership}
Blacklist: {blacklist_membership}
Recent threads with user: {recent_thread_count}
Last user reply: {last_user_reply_utc | "never"}
CALENDAR ADJACENCY
Next 2h events with sender: {adjacent_event_count}
Adjacent titles: {adjacent_titles[:3]}
USER STATE
Timezone: {tz}
Working hours: {working_hours}
Out of office: {oof_state}
Classify.
Why strict JSON
The model output feeds a deterministic rule engine. Prose responses break the pipeline. Strict JSON with a fixed schema means every tool downstream has a typed contract, and any drift from the schema surfaces as a hard error rather than a silent misread. Claude on Foundry supports structured output natively; the prompt above pairs with the structured-output API to enforce the shape at the model boundary.
Appendix G | Security posture
What tenant admins and security reviewers should ask.
The answers are written for the security review, not the marketing deck.
Every Phase 1 design decision was made with a tenant admin's security review in mind. The posture below is the set of answers the review will want, stated plainly. Any item a reviewer flags as insufficient is a Phase 1 build gate.
Data residency
All customer data stays inside the tenant: Graph calls hit the user's mailbox, Dataverse is the tenant's Dataverse, Application Insights is the customer's workspace, OneDrive profile is the user's OneDrive. The Foundry agent runtime runs in an Azure subscription the customer owns.
Model provider isolation
Claude runs via Azure AI Foundry's model catalog. Requests stay inside the Azure boundary. No data leaves the customer's Azure subscription for model inference. This is the deployment option the security review needs to see for Anthropic models inside enterprise tenants.
Scope minimization
Delegated scopes only. No application permissions in the single-tenant demo. Each phase requests the minimum permissions it requires. The two scopes a reviewer will focus on (Mail.Send and ChannelMessage.Read.All) are gated behind opt-in toggles.
Write-action gating
Every write action (send, archive, To-Do create) passes through the rule engine with the per-category toggle, sender override, and confidence threshold. Default is draft-only. The user controls every auto-action and can revoke any of them from the audit view.
Audit completeness
Every tool call emits one event. Correlation IDs thread events per conversation. The audit view reads the same workspace the pipeline writes. No parallel audit table; no risk of drift between what the agent did and what the audit shows.
Prompt injection exposure
Classifier and drafter run on user-supplied text. Untrusted input is marked as such in the user prompt; the system prompt restates that tool outputs (not conversation body) are the source of truth for actions. The rule engine runs downstream of the model and will not execute instructions embedded in message bodies.
Revocability
Draft creation and archive actions are fully revocable from the audit view. Send actions to external recipients are not revocable after delivery; the audit view marks them as such. Revocability is written into the audit event, not inferred.
Data retention
Application Insights retention matches the customer's existing workspace policy. Voice profile JSON is editable and deletable by the user. Dataverse rows follow standard Dataverse retention. No secondary copies of customer data are kept by the agent.
Appendix H | Dependencies and handoff
What a developer needs to pick this up.
Runtime versions, repos, libraries, and the handoff checklist.
Azure AI Foundry
GA channel
Agent service. Hosts the Python orchestration code and brokers Claude calls.
Anthropic Claude
Sonnet 4.6 (Foundry catalog)
Reasoning model. Model choice is configurable in settings; Sonnet is the default for Phase 1 on cost-quality balance.
Python
3.11
Runtime in Foundry. Pinned to 3.11 for stability; 3.12 upgrade tracked as Phase 1.5.
Microsoft Graph SDK
msgraph-sdk-python 1.x
Official SDK. Covers mail, calendar, chat, drive, todo endpoints in a single client.
Copilot Studio
Power Platform current
Surface layer. Publishes the Concierge agent into M365 Copilot chat. Dataverse is the backing store Studio uses natively.
Application Insights
Log Analytics workspace
Audit log target. Customer's existing workspace or a dedicated one per the reviewer's preference.
Dataverse
Tenant default
Config store. Four tables in Phase 1, six by Phase 4. All composite-keyed on tenant plus user.
Handoff checklist
At Phase 1 ship, the handoff artifact is: one GitHub repo with the Python orchestration, one Bicep template for the Foundry workspace and App Insights, one Dataverse solution file for the tables, one Copilot Studio solution file for the agent, one admin-consent URL for the delegated scopes, and this technical appendix versioned alongside the code. The scope study ships as the user-facing README; this appendix ships as the developer-facing one.
Both documents are versioned and updated as the build advances. The scope study version tracks user-facing capability; the appendix version tracks the implementation contract. A reader comparing the two at any point can see exactly which promises have been built and which are still ahead.
The short version of the appendix.
Phase 1 needs five delegated Graph scopes, fourteen typed tool functions, four Dataverse tables, one JSON voice profile, one Application Insights workspace, and one Foundry agent with Claude as the reasoning model. The scope study is the why and the shape. This document is the contract.
If you're reviewing this for a technical interview, a security assessment, or a developer handoff, the fields and signatures above are the design surface. Any question the document does not answer is a gap worth calling out; I'd rather find it now than during the build.
Daniel Lepel
Principal Microsoft Cloud Architect
daniel@lepel.us
212-252-9200
Albany, NY Capital District