Files every document that arrives by email into Box and records what it says as a row on your register.
UsesBox
Gmail
Google Sheets
OpenAI
File emailed documents and log what they say
Whenever new mail arrives in Gmail, the workflow sweeps your inbox for messages matching a search you describe, copies the PDF attachments on each into a Box folder and adds one row per document to a register in Google Sheets. The row records when it was filed, the sender, the subject, the name it was filed under, a link to the copy and whether it could be read, followed by details read from the document itself, such as a supplier, document number, date and total, written exactly as the document gives them.
A field the document does not carry is left empty rather than guessed. Each copy keeps its own name plus a reference to its email, so this month's Invoice.pdf never collides with last month's. An email is labelled as registered only once every document on it is copied and logged. One that cannot be finished, such as an older email from a sender whose newer one is still in the inbox, is labelled 'Needs filing' for you, and any problem, including a document that could not be read, ends the run marked as failed with a note of what to do.
You need Gmail, Box, Google Sheets and OpenAI connected, a Box folder to file into, and a spreadsheet tab whose column headings match the row. Fill in the Gmail search, the Box folder and the spreadsheet link; everything else has a working default.
documentSearch to blank.Set documentSearch to blank.boxFolder to blank.Set boxFolder to blank.registerSheetUrl to blank.Set registerSheetUrl to blank.sheetTabName to Documents.Set sheetTabName to Documents.documentFields to Supplier, Document number, Document date, Total amount, Currency.Set documentFields to Supplier, Document number, Document date, Total amount, Currency.processedLabel to Document register.Set processedLabel to Document register.needsFilingLabel to Needs filing.Set needsFilingLabel to Needs filing.maxDocumentsPerRun to 5.Set maxDocumentsPerRun to 5.documentCap to maxDocumentsPerRun multiplied by 1.Set documentCap to maxDocumentsPerRun multiplied by 1.documentCap is greater than 25If documentCap is greater than 25documentCap is less than 1If documentCap is less than 1boxFolder and save it as boxFolderId.Trim boxFolder and save it as boxFolderId.boxFolderId contains /folder/ (ignoring case)If boxFolderId contains /folder/ (ignoring case)boxFolderId split by /folder/ split by / and save it as boxFolderId.Trim the first item of the last item of boxFolderId split by /folder/ split by / and save it as boxFolderId.boxFolderId is empty or boxFolderId multiplied by 1 is not boxFolderIdIf boxFolderId is empty or boxFolderId multiplied by 1 is not boxFolderIdfieldNames to an empty list.Set fieldNames to an empty list.rawFieldName in documentFields split by ,:For each rawFieldName in documentFields split by ,:rawFieldName and save it as fieldName.Trim rawFieldName and save it as fieldName.fieldName isn't emptyIf fieldName isn't emptyfieldName to the end of fieldNames and save it as fieldNames.Add fieldName to the end of fieldNames and save it as fieldNames.fieldNames is emptyIf fieldNames is empty@processedLabel -label:@needsFilingLabel'. Save it as found.List emails matching '@documentSearch has:attachment -label:@processedLabel -label:@needsFilingLabel'. Save it as found.runStarted.Get the current date and time and save it as runStarted.runStarted readable as “2026-11-02 14:05” and save it as filedAt.Make runStarted readable as “2026-11-02 14:05” and save it as filedAt.filedCount to 0.Set filedCount to 0.messagesLeft to 0.Set messagesLeft to 0.setAside to an empty list.Set setAside to an empty list.unreadDocuments to an empty list.Set unreadDocuments to an empty list.readProblem to blank.Set readProblem to blank.message in found.emails:For each message in found.emails:filedCount is greater than or equal to documentCapIf filedCount is greater than or equal to documentCapmessagesLeft.Add 1 to messagesLeft.messageDescription to an email.Set messageDescription to an email.messageErrorIf a step below fails, save the error as messageErrormessageId to blank.Set messageId to blank.idErrorIf a step below fails, save the error as idErrormessage.id and save it as messageId.Trim message.id and save it as messageId.messageId is emptyIf messageId is emptySkipped a message Gmail returned without an id..Show in the output: Skipped a message Gmail returned without an id..rawFrom to blank.Set rawFrom to blank.fromErrorIf a step below fails, save the error as fromErrormessage.from and save it as rawFrom.Trim message.from and save it as rawFrom.senderAddress to rawFrom.Set senderAddress to rawFrom.rawFrom contains < (ignoring case)If rawFrom contains < (ignoring case)rawFrom split by < split by > and save it as senderAddress.Trim the first item of the last item of rawFrom split by < split by > and save it as senderAddress.subject to (no subject).Set subject to (no subject).subjectErrorIf a step below fails, save the error as subjectErrormessage.subject and save it as subjectText.Trim message.subject and save it as subjectText.subjectText isn't emptyIf subjectText isn't emptysubject to subjectText.Change subject to subjectText.'@subject' from @senderAddress and save it as messageDescription.Build the text '@subject' from @senderAddress and save it as messageDescription.messageProblems to an empty list.Set messageProblems to an empty list.senderAddress doesn't contain @ (ignoring case)If senderAddress doesn't contain @ (ignoring case)rawFrom) to the end of messageProblems and save it as messageProblems.Add its sender could not be read (rawFrom) to the end of messageProblems and save it as messageProblems.latestId to blank.Set latestId to blank.senderAddress'. Save it as latestFromSender.Find email matching 'from:senderAddress'. Save it as latestFromSender.latestErrorIf a step below fails, save the error as latestErrorlatestFromSender.email.id and save it as latestId.Trim latestFromSender.email.id and save it as latestId.latestId is not messageIdIf latestId is not messageIdonly a sender's newest email can be opened, and a newer one from this sender is in the inbox; archive that one first to the end of messageProblems and save it as messageProblems.Add only a sender's newest email can be opened, and a newer one from this sender is in the inbox; archive that one first to the end of messageProblems and save it as messageProblems.senderAddress. Save it as downloaded.Download the attachments on the latest email from senderAddress. Save it as downloaded.messageDocuments to 0.Set messageDocuments to 0.attachmentNumber to 0.Set attachmentNumber to 0.namesOnMessage to an empty list.Set namesOnMessage to an empty list.attachment in downloaded.documents:For each attachment in downloaded.documents:fileName to blank. Set fileName to blank.nameError If a step below fails, save the error as nameErrorattachment.filename and save it as fileName.Trim attachment.filename and save it as fileName.attachmentLabel to (unnamed attachment). Set attachmentLabel to (unnamed attachment).fileName isn't empty If fileName isn't emptyattachmentLabel to fileName.Change attachmentLabel to fileName.fileName in lowercase doesn't end with .pdf (ignoring case) If fileName in lowercase doesn't end with .pdf (ignoring case)attachmentLabel: only PDF attachments are treated as documents.Show in the output: Skipped attachmentLabel: only PDF attachments are treated as documents.documentRef to blank. Set documentRef to blank.refError If a step below fails, save the error as refErrorattachment.document_ref and save it as documentRef.Trim attachment.document_ref and save it as documentRef.documentRef is empty If documentRef is emptyattachmentLabel; it was left alone.Show in the output: Could not open attachmentLabel; it was left alone.attachmentNumber. Add 1 to attachmentNumber.\.pdf$ with in attachmentLabel (ignoring case) and save it as fileStem. Replace the pattern \.pdf$ with in attachmentLabel (ignoring case) and save it as fileStem.@fileStem (@messageId).pdf and save it as boxName. Build the text @fileStem (@messageId).pdf and save it as boxName.namesOnMessage contains boxName If namesOnMessage contains boxName@fileStem (@messageId @attachmentNumber).pdf and save it as boxName.Build the text @fileStem (@messageId @attachmentNumber).pdf and save it as boxName.boxName to the end of namesOnMessage and save it as namesOnMessage. Add boxName to the end of namesOnMessage and save it as namesOnMessage.storedLink to blank. Set storedLink to blank.uploadProblem to blank. Set uploadProblem to blank.uploadError If a step below fails, save the error as uploadErrorboxFolderId to Box folder documentRef. Save it as stored.Upload boxFolderId to Box folder documentRef. Save it as stored.stored.file_link and save it as storedLink.Trim stored.file_link and save it as storedLink.uploadError.message by a new line and save it as uploadErrorLines.Split uploadError.message by a new line and save it as uploadErrorLines.uploadErrorLines and save it as uploadProblem.Get the first item of uploadErrorLines and save it as uploadProblem.lookupErrorIf a step below fails, save the error as lookupErrorboxFolderId. Save it as folderItems.List items in Box folder boxFolderId. Save it as folderItems.folderItems.items where item.name is boxName and save it as sameName.Keep each item in folderItems.items where item.name is boxName and save it as sameName.sameName isn't emptyIf sameName isn't emptyhttps://app.box.com/file/@sameName[0].id and save it as storedLink.Build the text https://app.box.com/file/@sameName[0].id and save it as storedLink.storedLink is empty If storedLink is emptyfiledCount is 0If filedCount is 0attachmentLabel on messageDescription could not be copied into Box (uploadProblem). Check that Box is connected and that the 'boxFolder' step names a folder you can add files to. The email is left as it was, so the next run tries it again.Stop and report a problem: Nothing was filed this run: attachmentLabel on messageDescription could not be copied into Box (uploadProblem). Check that Box is connected and that the 'boxFolder' step names a folder you can add files to. The email is left as it was, so the next run tries it again.attachmentLabel could not be copied into Box (uploadProblem) to the end of messageProblems and save it as messageProblems.Add attachmentLabel could not be copied into Box (uploadProblem) to the end of messageProblems and save it as messageProblems.readStatus to Read. Set readStatus to Read.returnedNames to an empty list. Set returnedNames to an empty list.returnedValues to an empty list. Set returnedValues to an empty list.extractError If a step below fails, save the error as extractErrorfieldName… and save it as extraction.Run an AI agent: Read the attached document and pull out each of these fields, in this order: - fieldName… and save it as extraction.returnedField in extraction.output.fields:For each returnedField in extraction.output.fields:returnedName to blank. Set returnedName to blank.fieldNameError If a step below fails, save the error as fieldNameErrorreturnedField.name and save it as returnedName.Trim returnedField.name and save it as returnedName.returnedValue to blank. Set returnedValue to blank.fieldValueError If a step below fails, save the error as fieldValueErrorreturnedField.value and save it as returnedValue.Trim returnedField.value and save it as returnedValue.fieldListErrorIf a step below fails, save the error as fieldListErrorreturnedField.value with , and save it as returnedValue.Join returnedField.value with , and save it as returnedValue.returnedName to the end of returnedNames and save it as returnedNames. Add returnedName to the end of returnedNames and save it as returnedNames.returnedValue to the end of returnedValues and save it as returnedValues. Add returnedValue to the end of returnedValues and save it as returnedValues.readStatus to Filed but could not be read.Change readStatus to Filed but could not be read.boxName to the end of unreadDocuments and save it as unreadDocuments.Add boxName to the end of unreadDocuments and save it as unreadDocuments.readProblem to extractError.message.Change readProblem to extractError.message.rowValues to an empty list. Set rowValues to an empty list.filedAt to the end of rowValues and save it as rowValues. Add filedAt to the end of rowValues and save it as rowValues.senderAddress to the end of rowValues and save it as rowValues. Add senderAddress to the end of rowValues and save it as rowValues.subject to the end of rowValues and save it as rowValues. Add subject to the end of rowValues and save it as rowValues.boxName to the end of rowValues and save it as rowValues. Add boxName to the end of rowValues and save it as rowValues.storedLink to the end of rowValues and save it as rowValues. Add storedLink to the end of rowValues and save it as rowValues.readStatus to the end of rowValues and save it as rowValues. Add readStatus to the end of rowValues and save it as rowValues.wantedField in fieldNames: For each wantedField in fieldNames:cellValue to blank. Set cellValue to blank.wantedField in returnedNames and save it as foundAt. Find the position of wantedField in returnedNames and save it as foundAt.foundAt is greater than or equal to 0 If foundAt is greater than or equal to 0cellValue to returnedValues[@foundAt].Change cellValue to returnedValues[@foundAt].cellValue to the end of rowValues and save it as rowValues. Add cellValue to the end of rowValues and save it as rowValues.registerError If a step below fails, save the error as registerErrorsheetTabName in registerSheetUrl.Add a row to sheetTabName in registerSheetUrl.filedCount is 0If filedCount is 0boxName could not be written (registerError.message). Check that Google Sheets is connected and that 'registerSheetUrl' and 'sheetTabName' point at your register. The copy is already in Box; the email is left as it was, so the next run tries again and uses that copy.Stop and report a problem: Nothing was added to the register this run: the row for boxName could not be written (registerError.message). Check that Google Sheets is connected and that 'registerSheetUrl' and 'sheetTabName' point at your register. The copy is already in Box; the email is left as it was, so the next run tries again and uses that copy.boxName is in Box but could not be added to the register (registerError.message) to the end of messageProblems and save it as messageProblems.Add boxName is in Box but could not be added to the register (registerError.message) to the end of messageProblems and save it as messageProblems.filedCount. Add 1 to filedCount.messageDocuments. Add 1 to messageDocuments.boxName and added it to the register. Show in the output: Filed boxName and added it to the register.messageProblems is empty and messageDocuments is 0If messageProblems is empty and messageDocuments is 0messageDescription - it carried no PDF.Show in the output: Nothing to register on messageDescription - it carried no PDF.messageProblems is emptyIf messageProblems is emptyprocessedLabel to email messageId.Add label processedLabel to email messageId.messageProblems with ; and save it as problemText.Join messageProblems with ; and save it as problemText.messageDescription: problemText to the end of setAside and save it as setAside.Add messageDescription: problemText to the end of setAside and save it as setAside.needsFilingLabel to email messageId.Add label needsFilingLabel to email messageId.filedCount is 0If filedCount is 0messageDescription could not be handled (messageError.message). The email is left as it was, so the next run tries it again.Stop and report a problem: Nothing was filed this run: messageDescription could not be handled (messageError.message). The email is left as it was, so the next run tries it again.messageDescription: messageError.message to the end of setAside and save it as setAside.Add messageDescription: messageError.message to the end of setAside and save it as setAside.setAsideErrorIf a step below fails, save the error as setAsideErrorneedsFilingLabel to email message.Add label needsFilingLabel to email message.filedCount is 0 and setAside is empty and messagesLeft is 0If filedCount is 0 and setAside is empty and messagesLeft is 0reportLines to an empty list.Set reportLines to an empty list.filedCount document(s) into Box and onto the register. to the end of reportLines and save it as reportLines.Add Filed filedCount document(s) into Box and onto the register. to the end of reportLines and save it as reportLines.messagesLeft is greater than 0If messagesLeft is greater than 0messagesLeft more message(s) are waiting: this run reached its limit of maxDocumentsPerRun documents, and the next run carries on from there. to the end of reportLines and save it as reportLines.Add messagesLeft more message(s) are waiting: this run reached its limit of maxDocumentsPerRun documents, and the next run carries on from there. to the end of reportLines and save it as reportLines.unreadDocuments isn't emptyIf unreadDocuments isn't emptyunreadDocuments with , and save it as unreadList.Join unreadDocuments with , and save it as unreadList.unreadList. The reader said: readProblem to the end of reportLines and save it as reportLines.Add Filed, but the details could not be read, so their rows are empty for you to fill in: unreadList. The reader said: readProblem to the end of reportLines and save it as reportLines.setAside isn't emptyIf setAside isn't emptysetAside with "Join setAside with
- and save it as setAsideList.setAsideList to the end of reportLines and save it as reportLines.reportLines with a new line and save it as runReport.Join reportLines with a new line and save it as runReport.setAside isn't emptyIf setAside isn't emptyrunReport.Stop and report a problem: runReport.unreadDocuments isn't emptyIf unreadDocuments isn't emptyrunReport.Stop and report a problem: runReport.runReport.Finish the run: runReport.