Email Generator Automation
The design for Step 5 of the nightly reporting pipeline — where the email is built, approved, and sent. It replaces the Email Generator workbook's 19 hard-coded worksheet tabs with a Power Automate flow driven entirely by SharePoint. Two build-and-send scenarios are on the table — a simpler out-of-the-box path and an editable-draft path via Microsoft Graph — and which one we build turns on the review needs and attachment size set out below.
What we've confirmed
Power Automate and SharePoint stay
Power Automate remains the orchestrator, and SharePoint stays the full interface: every configuration value and every file lives there, nothing hardcoded in a Function or Flow. The one real change from the original design is that every report now sends from a single shared mailbox with a single shared footer. The mailbox —
reporting@trailmark.us — is the one everyone already uses today; what's new is that the footer is held centrally, not pulled from whoever's personal Outlook signature. Both the mailbox address and the footer are themselves SharePoint-configured, not hardcoded — see the lists below.
This step never calculated anything
The program-wide "calculations stay in Excel" decision doesn't apply here. The Email Generator never calculated anything itself — it only read an already-computed NAV value and attached files other workbooks had produced, unmodified. There is no calculation logic in this step to preserve or port. This flow doesn't touch Excel at all: the night's share price is read from the
NAV Values SharePoint list — a dated history appended nightly from the end-of-day file — not from a workbook.
What this replaces
Today, one generic VBA routine (BuildTradeEmailFromSheet) runs against whichever of 19 worksheet tabs is active. Each tab hard-codes To/CC/BCC, a subject formula, attachment paths, and an optional delayed-send time in fixed cells. The macro displays the draft and never sends — so a live desktop Outlook session merges the user's signature, and a person then clicks Send by hand.
Retired
- 19 worksheet tabs — one per client/report, each with its own fixed cell layout for recipients, subject, and attachments. Becomes one row per report in a SharePoint list.
- Live desktop signature merge — Outlook merged a personal signature before the macro wrote the body. Only works in a live desktop session, so it's replaced by one shared footer held centrally.
- Excel subject formulas — CONCATENATE/XLOOKUP built the subject line, including a per-share NAV for 3 clients. Replaced by placeholder substitution in the flow, reading the price from a list.
- "Display, then a person clicks Send" — replaced by a draft built in the shared mailbox via Graph, then an explicit approval step (Manual) or an immediate send (Automatic).
Unchanged
- A person approves by default. Nothing sends for a Manual client until someone approves it — the same human checkpoint as today. Clients can opt into Automatic send per the Send Mode setting.
- Who gets what. The same recipients, subjects, and attachments per report — now edited in a list instead of worksheet cells.
- Delayed delivery. Still honored, set from config exactly as today's worksheet cell specified.
- The shared mailbox. Reports keep sending from reporting@trailmark.us — the same mailbox everyone already uses today.
Where this sits in the pipeline
This design covers Steps 5–7 of the nightly run — the email being drafted, approved, and sent & archived. Steps 1–4 (files arriving over SFTP, renamed & sorted, workbooks refreshed, reports created) are covered elsewhere; they hand this step a finished report sitting in the Working library.
1
Reports generated
Fund accounting, delivered via SFTP
2
Renamed & sorted
Into the Working library, a folder per client
3
Workbooks updated
Same formulas, refreshed automatically
4
Reports created
Clean PDF / Excel saved to Working
5
Email built
From the config lists (two scenarios)
6
Approved
A person approves — when set to Manual
7
Sent & archived
Sent on approval or automatically; then archived
Steps 5–7 are this document. A person still approves before anything sends, unless a client is set to Automatic.
The email flow, step by step
One Power Automate flow per report row, triggered independently — a stall on one client's report doesn't block another's draft. These are the sub-steps inside pipeline Step 5.
1Trigger — the report's files land. The flow for a given report wakes up when that report's expected file(s) appear in the Working library, matched by the Report Type + Reporting Date metadata written during Step 2 — not filename guessing. If the files don't appear within the processing window, the flow logs it and does not create a partial draft.
2Resolve recipients & template. One lookup against the Notification Assignments list, keyed by Report Key, reads To/CC/BCC, the subject/body templates, attachment types, delayed-send time, Send Mode, and the Approver — replacing 6+ fixed worksheet cells per tab.
3Resolve the night's share price (funds that want it). For funds that show a price in the email (about 3 of 19), the flow reads it from the NAV Values list with a SharePoint "Get items" action, keyed by fund ticker for that night's date — not from the Excel workbook. The list is a dated history: a new row per fund is appended each night from the end-of-day file, and the flow reads the row matching tonight.
4Build subject & body, append the shared footer. Placeholder substitution — {{date}}, {{nav}}, {{fund}} — runs against the stored template strings, same power as today's CONCATENATE formulas but editable in a list field. The one shared footer from Email Sender Settings is appended here, because Graph has no live Outlook client to merge a signature automatically.
5Create the draft in the shared mailbox. Microsoft Graph's Create message action — POST /users/reporting@trailmark.us/messages — builds the message as a draft in the mailbox's Drafts folder and returns its id and webLink (the link a reviewer opens to see or edit it in Outlook Online).
6Attach the finished report. Attachments are the clean client report pulled from the Working library — the renamed PDF and Excel with internal columns already removed, not the raw trigger files — added to the draft via /attachments. Anything over 3MB uses a Graph upload session (createUploadSession); the nightly PDF and Excel together often exceed 3MB, so this path is expected.
7Branch on Send Mode, then log. Automatic sends the draft immediately; Manual (the default) routes it to an approval task. Either way, once it sends, the run is recorded in the Run Log list. The next section walks the branch in full.
How a report gets approved and sent
The Send Mode branch is the same either way — Manual (approval required, the default) or Automatic (sends without approval). What differs is how the email is built and sent, and there are two scenarios. Pick per the decision below.
Which scenario — one question for the client
Does the reviewer only
approve, or do they need to
edit the email before it goes out?
Scenario A (compose & send via the Outlook connector) if approval is enough and attachments stay under 25 MB — simpler and fully out-of-the-box.
Scenario B (editable draft via Graph) if the reviewer must edit the outbound email, or attachments can exceed 25 MB. To confirm with Matt: (1) approve vs. edit; (2) typical nightly PDF+Excel size.
Scenario A — compose & send via the Outlook connector simpler, fully OOTB
The flow reads the config lists, gets the finished report from Working, and (for Manual clients) raises an approval showing a preview. On approval it composes and sends the email with the Office 365 Outlook Send an email from a shared mailbox (V2) action — recipients, subject, body and attachments in one step. No Graph and no Azure Function; sending from reporting@trailmark.us still leaves a proper Sent Items record. Trade-off: the reviewer approves a preview, not the actual outbound email, and attachments are capped at 25 MB.
| Step | Tool / action | Endpoint or notes |
| Get config | SharePoint — Get items | Notification Assignments, Email Sender Settings, NAV Values |
| Get report | SharePoint — Get file content | Finished report from Working / {client} |
| Branch | Condition | Send Mode: Manual / Automatic |
| Approval (Manual) | Approvals — Start and wait for an approval | Preview: subject, recipients, body, attachment names |
| Send | Office 365 Outlook — Send an email from a shared mailbox (V2) | To/Cc, subject, body, attachments (≤25 MB); leaves a Sent Items record |
| Run Log | SharePoint — Create item | Status + detailed log per run |
Scenario B — editable draft via Graph reviewer can edit; up to 150 MB
Why a draft, and why Graph
Building the email as a real draft in reporting@trailmark.us first gives two things Scenario A can't:
(a) a real, openable email — the approver sees it in Outlook exactly as the client will, HTML rendered with attachments, not a flow preview; and
(b) editability before send — fix a typo in Outlook Online and that edited version is what goes out, because the system sends that same draft by id. The standard Outlook connector can only
send a message; it can't build a draft into a shared mailbox — so the draft is created with Microsoft Graph. (Both scenarios leave a correct Sent Items record, so that isn't a differentiator.) The rest of this section details Scenario B.
Build
Working libraryFinished report, internal columns removed
→
Graph: Create messagePOST /users/reporting@trailmark.us/messages — draft in Drafts, returns id + webLink
→
Add attachments/attachments; over 3MB via upload session
Automatic
Draft readySend Mode = Automatic
→
Graph: send nowPOST /messages/{id}/send
→
Sent & loggedNo manual step
Manual
Approval taskStart and wait for an approval — preview + link to the draft
→
ApproveGraph sends the draft by id — approver edits carry through
→
Sent & loggedReject → delete the draft, log, stop
Manual is the default. The approval task shows the subject, recipients, and a body snippet, plus a link to open the draft in Outlook Online. The approver can edit the draft there and clicks Approve — the system then sends that same draft by id, so any edits are included. On Reject, the draft is deleted and the outcome logged; nothing is sent.
Large attachments use an upload session
Attachments go on the draft two ways, by size.
Up to 3MB: a single
POST /messages/{id}/attachments with the file content base64-encoded.
From 3MB up to 150MB: a Graph attachment
upload session (createUploadSession), after which the bytes are PUT in sequential ranges of up to 4MB each until complete. The nightly PDF and Excel together often exceed 3MB, so the upload session is a real requirement, not an edge case — the tenant's default message-size limit is 35MB. Building the draft and running this chunked upload is what the one Azure Function (see below) handles.
Known issue to validate — large files in a shared mailbox
Microsoft documents a
known issue (#13644) attaching large files to a message in a shared or delegated mailbox via the upload session. Because our draft is created in the shared mailbox reporting@trailmark.us, this path has to be validated during build against a real >3MB nightly report. It's called out here as a
risk to confirm, not assumed working.
Permissions this needs
Mail.ReadWrite to create the draft in the shared mailbox, plus
Mail.Send /
Send-As on reporting@trailmark.us to send it. These are granted on the shared mailbox specifically. Confirming exactly how they're scoped with whoever administers the tenant is an open item below.
Every run is logged
After the email sends — on approval or automatically — the workflow appends a row to the
Run Log SharePoint list: the client/run, when files arrived, when the report was generated, when it was approved/sent, success or failure, and a detailed log field for anything that needs a closer look. One row per run, so each report's outcome is isolated from every other's.
The resulting draft — held for approval (USVC)
From
reporting@trailmark.us (shared mailbox)
To
erik@angellist.com; jun.yoon@…; usvc@pineadvisorsolutions.com
Cc
USVCSHARED@trailmark.us; fundaccounting@trailmark.us
Subject
USVCX Nightly Reports - 7/14/2026 - $20.46
Attached
USVC nightly reports (PDF), USVC nightly reports (Excel), USVC trade FMV daily file
Body
Good evening,
Please find attached the USVC nightly reports for 7/14/2026. The fund's closing NAV per share is $20.46.
Please let us know if you have any questions.
Thank you,
Trailmark Fund Solutions
[shared footer]
Status
Built in Drafts — waiting for a person to approve, then it sends
This is the draft as it sits in the shared mailbox before approval. The $20.46 is an example figure; the real subject and body use that night's actual NAV and date, formatted exactly as they are now. Recipients and body reflect USVC's current setup — please correct the body wording if it doesn't match what you send today.
Workflow diagram — Scenario B (Steps 5–7)
The implementation-level view of Scenario B (editable draft via Graph): build the draft, branch on Send Mode, log the run. Each row is one pipeline step. Scenario A's flow is the simpler Outlook path shown above.
Step 5
Read configNotification Assignments (To/CC, subject & body templates), Email Sender Settings (shared footer), NAV Values (tonight's price)
→
Graph: Create messageDraft in reporting@trailmark.us — returns id + webLink
→
Attach finished reportFrom Working — small: POST /attachments; large: Azure Function + upload session
Step 6
Branch on Send ModeAutomatic vs Manual (default)
→
Automatic: send nowGraph POST /messages/{id}/send
→
Manual: approvalStart and wait for an approval — preview (subject, recipients, body snippet) + link to the editable draft. Approve → send by id; Reject → delete draft, log
Step 7
Run LogOne row per run: arrived, generated, approved/rejected, sent, success/failure + detailed log
Manual is the default: nothing sends until a person approves the finished draft, and their Outlook edits carry through because the system sends that same draft by id. Automatic sends immediately. Either outcome writes one Run Log row.
Action set — Scenario B
The concrete actions the Scenario B flow runs, in order — which tool performs each, and the endpoint or note that pins it down. (Scenario A's action set is in the section above.)
| Step | Tool / action | Endpoint or notes |
| Get config | SharePoint — Get items | Notification Assignments, Email Sender Settings, NAV Values |
| Resolve NAV | SharePoint — Get items | NAV Values row for tonight (by fund ticker + as-of date) |
| Build draft + attach report | Azure Function (Graph SDK) | Create message + LargeFileUploadTask; returns draft id + webLink |
| Branch | Condition | On Send Mode (Manual / Automatic) |
| Approval | Approvals — Start and wait for an approval | Approve / Reject; approver link to the draft |
| Send | Microsoft Graph | POST /users/reporting@trailmark.us/messages/{id}/send |
| Reject cleanup | Microsoft Graph | Delete message (the draft) |
| Run Log | SharePoint — Create item | Status + detailed log per run |
Power Automate vs. one Azure Function
Orchestration stays in Power Automate. Exactly one small Azure Function is introduced — and only where Power Automate genuinely can't do the job.
Power Automate orchestrates
- Triggers when the report's file(s) land in Working.
- Reads the config lists — Notification Assignments, Email Sender Settings, NAV Values.
- Branches on Send Mode.
- Runs the approval task.
- Sends on approval, then writes the Run Log.
One Azure Function, where PA can't
- Assembles the draft and uploads large attachments — .NET + the Graph SDK, using LargeFileUploadTask.
- Needed because raw Power Automate can't reliably do chunked byte-range uploads: no native binary slicing, manual Content-Range math, and no resumability.
- Returns the draft id and webLink back to the flow.
Why a Function fits the security model
The Function runs on
Managed Identity with the application permissions
Mail.ReadWrite (create the draft + attachments) and
Mail.Send (send it), scoped to just reporting@trailmark.us via
RBAC for Applications in Exchange Online (App RBAC; the older ApplicationAccessPolicy does the same but is now legacy) — no stored credentials, and no reach into any other mailbox. A pure Power Automate route (the "HTTP with Microsoft Entra ID" connector) would be
delegated and would need
FullAccess on the shared mailbox for the service account. The Function keeps to the project's principle: Power Automate orchestrates; Functions appear only where genuinely needed. (The standard Office 365 Outlook connector's "Send an email from a shared mailbox (V2)" can send from the shared mailbox, but cannot create a draft in it — which is why the draft is built through Graph.)
The lists you'll manage
Everything client-configurable lives in SharePoint: one row per report in Notification Assignments, one shared row in Email Sender Settings, a dated history in NAV Values, and a row per run in the Run Log. Editing any of them is a list edit — no workbook, no code deploy.
Notification Assignments one row per report
| Field | Type | Replaces (today) |
| Report Key | Text (unique) | Sheet name / tab identity, e.g. USVC, CV01, IDX |
| To / CC / BCC | Text (semicolon-delimited) | Rows 5 / 6 / 7 on each worksheet |
| Subject Template | Text, with {{placeholders}} | Row 9 subject formula |
| Body Template | Multiline text (Rich Text column) | Row 15 CONCATENATE formula |
| Attachment Report Types | Text / choice (multi) | Row 13 attachment path columns C, D, E… |
| Delayed-Send Time | Time | Row 11 Delay Delivery Time |
| Send Mode | Choice: Manual / Automatic | NEW. Manual (default) = build draft + approval; Automatic = send without approval. Per-client, one edit to flip. |
| Approver | Person | NEW. Who receives the approval task for this client when Send Mode = Manual. |
| Active | Yes/No | Whether this report currently runs — was implicit in the tab existing |
Editing who a report goes to, its subject line, or whether it needs approval is a one-row edit here — no workbook, no code.
Email Sender Settings one shared row
| Field | Type | Replaces (today) |
| Sender Mailbox | Text (single shared address) — reporting@trailmark.us | The mailbox everyone already sends from today — now named in one place instead of assumed. |
| Footer / Signature HTML | Multi-line text (single shared value) | The live Outlook desktop signature merge — no longer automatic under Graph, so one shared footer is stored here and appended to every report. |
One shared footer for every report, kept current in one place instead of relying on whoever's personal signature happened to be on the email.
NAV Values a new dated row per fund each night
| Field | Type | Purpose |
| Fund Ticker | Text | Row key — matches the ticker used in today's XLOOKUP against NAV Listing |
| Share Price / NAV Value | Currency / Number | The per-share value today's subject-line formula reads via XLOOKUP |
| As-of Date | Date | Which night this value is for — the flow reads the row matching tonight, and the history stays queryable |
Not a single overwritten "current price". Each night a new dated row per fund is appended from the end-of-day file, so a full price history builds up, and that night's value is read and merged into the subject/body. Populated by the Step 3 workbook refresh (see open items).
Run Log one row per run
| Field | Type | Purpose |
| Client / Run | Text | Which report this run is for, and the night |
| Arrived | Date/Time | When the report's files landed in Working |
| Generated | Date/Time | When the draft was built |
| Approved / Sent | Date/Time | When it was approved (Manual) and sent |
| Outcome | Choice: Success / Failure | Whether the run completed |
| Detailed Log | Multiline text | Missing attachments, NAV lookup misses, rejects — anything needing a closer look |
Appended after the email sends or an approval resolves. This is also what a nightly dashboard can be built from — what's in progress, waiting for approval, or already gone out.
Sample records — two real clients
What the lists look like with data, using two real clients from your config workbook: USVC (share price in the subject) and Champion (no share price). Recipients, subjects, and the sender are verbatim from Trailmark Support_Nightly Reports.xlsx and the sample emails.
Notification Assignments 2 rows
| Report Key | To | CC | Subject Template | Attachments | Send Mode | Active |
| USVC |
erik@angellist.com; jun.yoon@angellist.com; daniel.jeon@angellist.com; usvc@pineadvisorsolutions.com |
USVCSHARED@trailmark.us; fundaccounting@trailmark.us |
USVCX Nightly Reports - {{date:M/d/yyyy}} - {{nav:$0.00}} |
USVCX Nightly Reports (PDF); USVCX Nightly Reports (Excel); USVC Trade FMV Daily |
Manual |
Yes |
| CHMP |
robin@sweaterventures.com; jkr@sweaterventures.com |
SweaterShared@trailmark.us; fundaccounting@trailmark.us; marques@championpartners.co; nick@championpartners.co |
Champion Nightly Reports - {{date:M/d/yyyy}} |
CHMP Champion Nightly Reports (PDF); CHMP Champion Nightly Reports (Excel) |
Manual |
Yes |
Approver, Body Template, and Delayed-Send Time are omitted here for width — Body is a Rich Text field per row; Approver is set per client; USVC has no delayed-send set. Champion has no {{nav}} placeholder, so no price appears in its subject. Both are Manual, so both wait for approval.
Email Sender Settings 1 shared row
| Sender Mailbox | Footer / Signature HTML |
| reporting@trailmark.us | Thank you, Trailmark Fund Solutions <shared footer block> |
Confirmed from the sample emails: reports already send from reporting@trailmark.us as one shared mailbox.
NAV Values a new dated row each night
| Fund Ticker | As-of Date | Share Price |
| USVC | 2026-07-14 | $20.46 |
| USVC | 2026-07-13 | $20.41 |
| COIDX (IDX) | 2026-07-14 | $9.69 |
| CV01 (Connetic) | 2026-07-14 | $10.29 |
Only the funds that show a price in the email. The 7/14 values are real, taken from the sample-email subject lines (USVCX $20.46, COIDX $9.69, Connetic $10.29). The earlier USVC row is illustrative, to show the dated history building up. Champion isn't here because it doesn't show a price.
Where each old cell reference goes
A direct mapping from today's worksheet layout to the new list-driven flow, so nothing behavioral gets lost in translation.
| Today (worksheet) | New (flow / list) | Note |
| Rows 5/6/7 — To/CC/BCC cells | Notification Assignments: To/CC/BCC fields | Direct 1:1 |
| Row 9 — Subject formula (CONCATENATE/XLOOKUP) | Subject Template + flow-side NAV lookup + expression substitution | NAV lookup moves from a cell formula to a Power Automate "Get items" read of the NAV Values list (not the workbook) |
| Row 11 — Delay Delivery Time | Delayed-Send Time field → DeferredDeliveryTime on the draft | Direct 1:1 |
| Row 13 — Attachment path columns | Attachment Report Types field + Working library metadata match | Path concatenation replaced by metadata-driven file lookup; files added via Graph /attachments (upload session over 3MB) |
| Row 15 — Body formula | Body Template (Rich Text) + expression substitution | Column stores HTML internally — read straight into the draft's HTML body, no docx-to-HTML step |
| Outlook.Display() live signature merge | Email Sender Settings: Footer / Signature HTML (single shared value) | Graph has no live desktop client, so one shared footer is appended instead of a per-person signature |
| Implicit: whoever ran the macro sends it | Graph creates the draft in reporting@trailmark.us and sends it (on approval or automatically) | No personal mailbox involved; the shared mailbox is named in Email Sender Settings |
| Macro "displays, never sends" — a person clicks Send | Manual: build draft + "Start and wait for an approval" → on Approve, Graph sends by id; on Reject, delete the draft | The human checkpoint, now an explicit approval instead of a manual click in Outlook |
| Right-click macro assignment per tab | One flow instance per Report Key row | Adding a report = adding a list row, not a new worksheet tab |
Open items — pending confirmation
A few choices are shown with a recommended default so the design isn't blocked, but none are locked in yet.
Approval timeout & draft cleanup
What should happen if a Manual approval task isn't actioned within some window — escalate to another approver, remind, or expire? And we should confirm the cleanup rule so nothing stale lingers in the shared mailbox: the draft is deleted on
Reject, and we need a decision on whether it's also deleted (or retried) on
timeout. Not yet decided.
Permissions on the shared mailbox
Confirm how
Mail.ReadWrite (to create the draft) and
Mail.Send /
Send-As (to send it) are granted on reporting@trailmark.us with whoever administers the tenant — application permission scoped to the one mailbox is the intended approach.
How Step 3 populates NAV Values
The NAV Values list is required, not optional. Still open is the exact mechanism for the Step 3 workbook refresh to write that night's per-ticker values into it — an Office Script writes to the list directly at the end of its run, or a small follow-on Power Automate step reads the refreshed range and upserts the list. Needs a decision.
Templating location
Recommended: keep {{placeholder}} substitution as plain Power Automate expressions against the list's template strings — no code deploy for a subject-line tweak. Alternative: an Azure Function does the substitution — more testable/reusable, but every template change then needs a deploy instead of a list edit.
Flow trigger granularity
Recommended: one flow per report, triggered when that report's own file(s) land — isolates failures per client. Alternative: a single nightly scheduled flow loops the whole Notification Assignments list — simpler to build, but a stall on one report can delay the batch and failures are harder to isolate.
Costs aren't covered here — if the question comes up, it's addressed in a separate document.
What doesn't change
- A person approves by default. Manual is the default Send Mode — the human checkpoint is preserved; individual clients opt into Automatic per the setting.
- SharePoint is the whole interface. Every config value and every file lives in SharePoint — nothing hardcoded in a Function or Flow, for Operations or IT to edit.
- Report, don't block. A missing attachment is noted in the draft and logged to the Run Log, not treated as a hard failure that stops everything.
- Delayed delivery is honored. DeferredDeliveryTime is still set from config, exactly as today's worksheet cell.
- Archiving still happens. The finished report is served to the email from the Working library (a folder per client) and also archived to the Archive library, in year and date folders (Archive / {year} / {date}).