for Trailmark Fund Solutions

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.

1
Trigger — 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.
2
Resolve 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.
3
Resolve 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.
4
Build 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.
5
Create 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).
6
Attach 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.
7
Branch 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.

StepTool / actionEndpoint or notes
Get configSharePoint — Get itemsNotification Assignments, Email Sender Settings, NAV Values
Get reportSharePoint — Get file contentFinished report from Working / {client}
BranchConditionSend Mode: Manual / Automatic
Approval (Manual)Approvals — Start and wait for an approvalPreview: subject, recipients, body, attachment names
SendOffice 365 Outlook — Send an email from a shared mailbox (V2)To/Cc, subject, body, attachments (≤25 MB); leaves a Sent Items record
Run LogSharePoint — Create itemStatus + 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.

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.)

StepTool / actionEndpoint or notes
Get configSharePoint — Get itemsNotification Assignments, Email Sender Settings, NAV Values
Resolve NAVSharePoint — Get itemsNAV Values row for tonight (by fund ticker + as-of date)
Build draft + attach reportAzure Function (Graph SDK)Create message + LargeFileUploadTask; returns draft id + webLink
BranchConditionOn Send Mode (Manual / Automatic)
ApprovalApprovals — Start and wait for an approvalApprove / Reject; approver link to the draft
SendMicrosoft GraphPOST /users/reporting@trailmark.us/messages/{id}/send
Reject cleanupMicrosoft GraphDelete message (the draft)
Run LogSharePoint — Create itemStatus + 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
FieldTypeReplaces (today)
Report KeyText (unique)Sheet name / tab identity, e.g. USVC, CV01, IDX
To / CC / BCCText (semicolon-delimited)Rows 5 / 6 / 7 on each worksheet
Subject TemplateText, with {{placeholders}}Row 9 subject formula
Body TemplateMultiline text (Rich Text column)Row 15 CONCATENATE formula
Attachment Report TypesText / choice (multi)Row 13 attachment path columns C, D, E…
Delayed-Send TimeTimeRow 11 Delay Delivery Time
Send ModeChoice: Manual / AutomaticNEW. Manual (default) = build draft + approval; Automatic = send without approval. Per-client, one edit to flip.
ApproverPersonNEW. Who receives the approval task for this client when Send Mode = Manual.
ActiveYes/NoWhether 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
FieldTypeReplaces (today)
Sender MailboxText (single shared address) — reporting@trailmark.usThe mailbox everyone already sends from today — now named in one place instead of assumed.
Footer / Signature HTMLMulti-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
FieldTypePurpose
Fund TickerTextRow key — matches the ticker used in today's XLOOKUP against NAV Listing
Share Price / NAV ValueCurrency / NumberThe per-share value today's subject-line formula reads via XLOOKUP
As-of DateDateWhich 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
FieldTypePurpose
Client / RunTextWhich report this run is for, and the night
ArrivedDate/TimeWhen the report's files landed in Working
GeneratedDate/TimeWhen the draft was built
Approved / SentDate/TimeWhen it was approved (Manual) and sent
OutcomeChoice: Success / FailureWhether the run completed
Detailed LogMultiline textMissing 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 KeyToCCSubject TemplateAttachmentsSend ModeActive
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 MailboxFooter / Signature HTML
reporting@trailmark.usThank 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 TickerAs-of DateShare Price
USVC2026-07-14$20.46
USVC2026-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 cellsNotification Assignments: To/CC/BCC fieldsDirect 1:1
Row 9 — Subject formula (CONCATENATE/XLOOKUP)Subject Template + flow-side NAV lookup + expression substitutionNAV 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 TimeDelayed-Send Time field → DeferredDeliveryTime on the draftDirect 1:1
Row 13 — Attachment path columnsAttachment Report Types field + Working library metadata matchPath concatenation replaced by metadata-driven file lookup; files added via Graph /attachments (upload session over 3MB)
Row 15 — Body formulaBody Template (Rich Text) + expression substitutionColumn stores HTML internally — read straight into the draft's HTML body, no docx-to-HTML step
Outlook.Display() live signature mergeEmail 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 itGraph 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 SendManual: build draft + "Start and wait for an approval" → on Approve, Graph sends by id; on Reject, delete the draftThe human checkpoint, now an explicit approval instead of a manual click in Outlook
Right-click macro assignment per tabOne flow instance per Report Key rowAdding 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