Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Excel desktop VBA with classic Outlook automation. The macro below lets you select a grouping column, creates one .xlsx workbook for each distinct nonblank value, finds the matching email address, attaches the completed file, and saves an Outlook draft for review. It never calls .Send.

This is a desktop workflow for a macro-enabled controller workbook. It is not a solution for Excel for the web or browser-only Outlook.

What the macro does

It splits rows, not arbitrary worksheets. For example, selecting Vendor ID in a purchase-order table creates one workbook for L0056T, one for T0074T, and so on. Each output contains the header and only that vendor’s rows.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Every group is checked for email consistency. A draft is created only when the group has one distinct, nonblank address. Conflicting or missing addresses are logged instead of being silently guessed.

Prepare the controller workbook

  1. Put the source data on a sheet named Data, with headers in row 1 and records below.
  2. Include a grouping column and an email column (for example, Vendor ID and Email address).
  3. Save the controller as .xlsm; .xlsx cannot retain VBA.
  4. On Windows Excel, enable Developer through File > Options > Customize Ribbon. Open Developer > Visual Basic, choose Insert > Module, and paste the code.
  5. Test with a copy containing only a few groups. Enable macros only when you trust and understand the code.

Complete VBA macro

Select any single cell in the grouping column before running SplitAndDraft. The email column is located by its header text; change EMAIL_HEADER if necessary.

Option Explicit

Private Const DATA_SHEET As String = "Data"
Private Const EMAIL_HEADER As String = "Email address"
Private Const OUTPUT_SUBFOLDER As String = "Split output"
Private Const SUBJECT_TEMPLATE As String = "Records for {GROUP} - {DATE}"
Private Const BODY_TEMPLATE As String = "Hello," & vbCrLf & vbCrLf & _
    "Please find attached the records for {GROUP}." & vbCrLf & _
    "This message was generated from the master workbook." & vbCrLf & vbCrLf & "Regards"

Public Sub SplitAndDraft()
    Dim src As Worksheet, pick As Range, emailCol As Long, splitCol As Long
    Dim lastRow As Long, lastCol As Long, r As Long, key As String
    Dim groups As Object, emails As Object, rows As Collection
    Dim outDir As String, groupKey As Variant, path As String, recipient As String
    Dim wbOut As Workbook, wsOut As Worksheet, i As Long, outRow As Long
    Dim olApp As Object, mail As Object, made As Long, skipped As Long
    Dim stamp As String, fileName As String

    On Error GoTo FatalError
    Set src = ThisWorkbook.Worksheets(DATA_SHEET)
    If ActiveSheet Is Not src Then src.Activate
    Set pick = Application.InputBox("Select one cell in the column used for grouping:", _
                                    "Split column", Type:=8)
    If pick Is Nothing Then Exit Sub
    If pick.Cells.CountLarge <> 1 Or pick.Row < 2 Or pick.Column > src.UsedRange.Columns(src.UsedRange.Columns.Count).Column Then
        MsgBox "Select one data cell on the Data sheet.", vbExclamation: Exit Sub
    End If
    splitCol = pick.Column
    emailCol = HeaderColumn(src, EMAIL_HEADER)
    If emailCol = 0 Then MsgBox "Header not found: " & EMAIL_HEADER, vbCritical: Exit Sub

    lastRow = src.Cells(src.Rows.Count, splitCol).End(xlUp).Row
    lastCol = src.Cells(1, src.Columns.Count).End(xlToLeft).Column
    If lastRow < 2 Then MsgBox "No data rows were found.", vbExclamation: Exit Sub

    Set groups = CreateObject("Scripting.Dictionary")
    groups.CompareMode = vbTextCompare
    Set emails = CreateObject("Scripting.Dictionary")
    emails.CompareMode = vbTextCompare

    For r = 2 To lastRow
        key = Trim$(CStr(src.Cells(r, splitCol).Value2))
        If Len(key) = 0 Then GoTo NextRow
        If Not groups.Exists(key) Then Set groups(key) = New Collection
        groups(key).Add r
        AddEmail emails, key, Trim$(CStr(src.Cells(r, emailCol).Value2))
NextRow:
    Next r

    outDir = ThisWorkbook.Path & Application.PathSeparator & OUTPUT_SUBFOLDER
    If Dir(outDir, vbDirectory) = vbNullString Then MkDir outDir
    stamp = Format$(Date, "yyyy-mm-dd")
    On Error Resume Next
    Set olApp = CreateObject("Outlook.Application")
    On Error GoTo FatalError

    For Each groupKey In groups.Keys
        Set rows = groups(groupKey)
        fileName = SafeName(BaseName(ThisWorkbook.Name) & "_" & _
                    CStr(src.Cells(1, splitCol).Value) & "_" & CStr(groupKey) & "_" & stamp) & ".xlsx"
        path = outDir & Application.PathSeparator & fileName
        If Len(Dir(path)) > 0 Then
            skipped = skipped + 1
            GoTo NextGroup
        End If

        Set wbOut = Workbooks.Add(xlWBATWorksheet)
        Set wsOut = wbOut.Worksheets(1)
        wsOut.Name = "Data"
        src.Range(src.Cells(1, 1), src.Cells(1, lastCol)).Copy wsOut.Cells(1, 1)
        outRow = 2
        For i = 1 To rows.Count
            src.Range(src.Cells(rows(i), 1), src.Cells(rows(i), lastCol)).Copy wsOut.Cells(outRow, 1)
            outRow = outRow + 1
        Next i
        wsOut.Columns.AutoFit
        Application.DisplayAlerts = False
        wbOut.SaveAs Filename:=path, FileFormat:=xlOpenXMLWorkbook
        wbOut.Close SaveChanges:=False
        Application.DisplayAlerts = True

        recipient = GroupEmail(emails, CStr(groupKey))
        If Len(recipient) = 0 Or InStr(1, recipient, "@", vbTextCompare) = 0 Then
            skipped = skipped + 1
            GoTo NextGroup
        End If
        If olApp Is Nothing Then GoTo NextGroup
        Set mail = olApp.CreateItem(0)
        mail.To = recipient
        mail.Subject = ReplaceTokens(SUBJECT_TEMPLATE, CStr(groupKey), fileName, stamp, rows.Count)
        mail.Body = ReplaceTokens(BODY_TEMPLATE, CStr(groupKey), fileName, stamp, rows.Count)
        mail.Save
        mail.Attachments.Add path, 1
        mail.Save
        made = made + 1
NextGroup:
        Set mail = Nothing
        Set wbOut = Nothing
    Next groupKey

    MsgBox groups.Count & " groups found." & vbCrLf & made & " Outlook drafts created." & vbCrLf & _
           skipped & " groups skipped or requiring attention." & vbCrLf & "Output: " & outDir, vbInformation
    Exit Sub
FatalError:
    Application.DisplayAlerts = True
    If Not wbOut Is Nothing Then wbOut.Close SaveChanges:=False
    MsgBox "The files already created remain in the output folder." & vbCrLf & Err.Description, vbCritical
End Sub

Private Function HeaderColumn(ws As Worksheet, headerText As String) As Long
    Dim c As Range
    Set c = ws.Rows(1).Find(headerText, LookAt:=xlWhole, LookIn:=xlValues, MatchCase:=False)
    If Not c Is Nothing Then HeaderColumn = c.Column
End Function

Private Sub AddEmail(dict As Object, k As String, address As String)
    If Len(address) = 0 Then Exit Sub
    If Not dict.Exists(k) Then dict.Add k, address ElseIf InStr(1, ";" & dict(k) & ";", ";" & address & ";", vbTextCompare) = 0 Then dict(k) = dict(k) & ";" & address
End Sub

Private Function GroupEmail(dict As Object, k As String) As String
    If dict.Exists(k) Then
        If InStr(dict(k), ";") = 0 Then GroupEmail = dict(k)
    End If
End Function

Private Function SafeName(s As String) As String
    Dim bad, x
    bad = Array("\", "/", ":", "*", "?", Chr$(34), "<", ">", "|")
    For Each x In bad: s = Replace(s, x, "_"): Next
    s = Replace(Replace(s, vbCr, "_"), vbLf, "_")
    If Len(s) > 150 Then s = Left$(s, 150)
    If UCase$(s) = "CON" Or UCase$(s) = "PRN" Or UCase$(s) = "AUX" Or UCase$(s) = "NUL" Then s = "_" & s
    SafeName = s
End Function

Private Function BaseName(s As String) As String
    BaseName = Left$(s, InStrRev(s, ".") - 1)
End Function

Private Function ReplaceTokens(t As String, g As String, f As String, d As String, n As Long) As String
    t = Replace(t, "{GROUP}", g): t = Replace(t, "{FILE}", f)
    t = Replace(t, "{DATE}", d): t = Replace(t, "{COUNT}", CStr(n))
    ReplaceTokens = t
End Function

Important behavior and customization

  • Blank grouping values: They are skipped. Change the loop if you prefer an Unassigned workbook.
  • Email conflicts: More than one distinct address prevents a draft. Correct the source data or implement an explicit business rule.
  • Existing files: The sample skips them. Add a sequence suffix or an overwrite prompt if required.
  • Formulas: The sample copies formulas. If they reference the master workbook, copy values instead (for example, assign .Value2) to avoid broken external links.
  • Formatting and filters: Headers, cell contents, and basic widths are copied. Hidden rows are still processed; use an explicit visible-row rule if that is required.
  • Subject and body: Edit the constants or move them to a Settings sheet. Supported tokens are {GROUP}, {FILE}, {DATE}, and {COUNT}.
  • Output format: Outputs are ordinary .xlsx files. Keep VBA in the controller .xlsm.

Why drafts are saved instead of sent

MailItem.Save stores the new message in Outlook’s default folder for that item type, normally Drafts. The macro saves once, adds the attachment using its full local path, and saves again. The output workbook is closed before Outlook attaches it, reducing file-lock problems.

Do not replace the final mail.Save with mail.Send unless you deliberately want immediate delivery. .Send can use Outlook’s default account; multiple-account workflows should explicitly set SendUsingAccount and should be tested separately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting

Symptom What to check
Outlook drafts are not created Classic Outlook must be installed, configured, and available to COM automation. New Outlook, web Outlook, policy restrictions, or security prompts may not support this VBA path.
“Header not found” Make the email header exactly match EMAIL_HEADER, including spelling and spaces.
Files exist but no drafts Inspect missing or conflicting addresses. The macro intentionally creates the workbook but skips an unsafe recipient.
Permission denied Choose a writable local output folder and close any workbook with the same name.
Broken formulas Convert copied formulas to values, or rewrite references for the new workbook.
Macro will not run Save as .xlsm, place the file in a trusted location, and review Excel’s macro security settings.

When another tool is better

VBA is the practical choice for a local, one-button process that must leave messages in classic Outlook Drafts. Power Query is useful for preparing grouped data but does not by itself create thousands of workbooks and drafts. Office Scripts and Power Automate are better for OneDrive/SharePoint, scheduled or unattended workflows; they generally require a cloud design using files, connectors, and attachment content rather than classic Outlook’s local draft model.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Run-safety checklist

  • Back up the master workbook and test on a small copy.
  • Confirm the selected grouping column and email header.
  • Verify the output folder is writable and has enough space.
  • Check that the macro contains no .Send call.
  • Review every generated file and Outlook draft before sending.
  • Keep an exception list for blank, malformed, or conflicting email addresses.

Microsoft references: Attachments.Add, MailItem.Save, and MailItem.Send.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.