Reads each PDF that arrives by email and files it into the right Box folder by what it says, or leaves it for you when unsure.
UsesBox
Gmail
OpenAI
File emailed PDFs into the right Box folder
Mail gets filed by who sent it, so the contract from your accountant ends up in the accounts folder. This reads each PDF that arrives by email for what kind of document it is, what it concerns, its reference, its date and any total, and an AI picks one of the Box folders you have described from those answers alone. The sender and subject are never shown to it, so an invoice is filed as an invoice whoever sent it. Each copy keeps its name plus a reference to its email, so a second Invoice.pdf never clashes with the first.
When the answer is not exactly one of your folders, or nothing could be read, nothing is copied and the email is labelled 'Needs sorting', a short queue you work through by hand. So is an older email Gmail cannot open because the same sender's newer one is still in the inbox. The run's last message lists each one. If reading, choosing or copying fails, the email is labelled the same way and the run ends as failed, saying what went wrong.
You need Gmail, Box and OpenAI connected, and up to three folders described in advance: a name, its Box folder and a line saying what belongs there. The Gmail search starts as any email from the past week with a PDF attached.
documentSearch to has:attachment filename:pdf newer_than:7d.Set documentSearch to has:attachment filename:pdf newer_than:7d.categoryOneName to blank.Set categoryOneName to blank.categoryOneFolder to blank.Set categoryOneFolder to blank.categoryOneAbout to blank.Set categoryOneAbout to blank.categoryTwoName to blank.Set categoryTwoName to blank.categoryTwoFolder to blank.Set categoryTwoFolder to blank.categoryTwoAbout to blank.Set categoryTwoAbout to blank.categoryThreeName to blank.Set categoryThreeName to blank.categoryThreeFolder to blank.Set categoryThreeFolder to blank.categoryThreeAbout to blank.Set categoryThreeAbout to blank.processedLabel to Document filing.Set processedLabel to Document filing.needsSortingLabel to Needs sorting.Set needsSortingLabel to Needs sorting.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 1destinations to an empty list.Set destinations to an empty list.categoryOneName isn't empty and categoryOneFolder isn't emptyIf categoryOneName isn't empty and categoryOneFolder isn't emptydestinations and save it as destinations.Add a record of name, folder, and about to the end of destinations and save it as destinations.categoryTwoName isn't empty and categoryTwoFolder isn't emptyIf categoryTwoName isn't empty and categoryTwoFolder isn't emptydestinations and save it as destinations.Add a record of name, folder, and about to the end of destinations and save it as destinations.categoryThreeName isn't empty and categoryThreeFolder isn't emptyIf categoryThreeName isn't empty and categoryThreeFolder isn't emptydestinations and save it as destinations.Add a record of name, folder, and about to the end of destinations and save it as destinations.filingDestinations to an empty list.Set filingDestinations to an empty list.destination in destinations:For each destination in destinations:destination.folder and save it as folderValue.Trim destination.folder and save it as folderValue.folderValue contains /folder/ (ignoring case)If folderValue contains /folder/ (ignoring case)folderValue split by /folder/ split by / and save it as folderValue.Trim the first item of the last item of folderValue split by /folder/ split by / and save it as folderValue.folderValue is empty or folderValue multiplied by 1 is not folderValueIf folderValue is empty or folderValue multiplied by 1 is not folderValuefilingDestinations and save it as filingDestinations.Add a record of name, folder, and about to the end of filingDestinations and save it as filingDestinations.filingDestinations is emptyIf filingDestinations is emptychoicesText to blank.Set choicesText to blank.destination in filingDestinations:For each destination in filingDestinations:choiceLine to - destination.name.Set choiceLine to - destination.name.destination.about and save it as choiceAbout.Trim destination.about and save it as choiceAbout.choiceAbout isn't emptyIf choiceAbout isn't emptychoiceLine to choiceLine: choiceAbout.Change choiceLine to choiceLine: choiceAbout.choicesText to choicesText@choiceLineChange choicesText to choicesText@choiceLine
.@processedLabel -label:@needsSortingLabel'. Save it as found.List emails matching '@documentSearch has:attachment -label:@processedLabel -label:@needsSortingLabel'. Save it as found.handledCount to 0.Set handledCount to 0.filedCount to 0.Set filedCount to 0.messagesLeft to 0.Set messagesLeft to 0.leftForYou to an empty list.Set leftForYou to an empty list.problems to an empty list.Set problems to an empty list.message in found.emails:For each message in found.emails:handledCount is greater than or equal to documentCapIf handledCount 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.senderAddress doesn't contain @ (ignoring case)If senderAddress doesn't contain @ (ignoring case)processedLabel to email messageId.Add label processedLabel to email messageId.needsSortingLabel to email messageId.Add label needsSortingLabel to email messageId.messageDescription: its sender could not be read to the end of leftForYou and save it as leftForYou.Add messageDescription: its sender could not be read to the end of leftForYou and save it as leftForYou.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 messageIdprocessedLabel to email messageId.Add label processedLabel to email messageId.needsSortingLabel to email messageId.Add label needsSortingLabel to email messageId.messageDescription: only a sender's newest email can be opened, and a newer one from this sender is in the inbox to the end of leftForYou and save it as leftForYou.Add messageDescription: only a sender's newest email can be opened, and a newer one from this sender is in the inbox to the end of leftForYou and save it as leftForYou.senderAddress. Save it as downloaded.Download the attachments on the latest email from senderAddress. Save it as downloaded.processedLabel to email messageId.Add label processedLabel to email messageId.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.documentDetails to an empty list. Set documentDetails to an empty list.failure to blank. Set failure to blank.setAsideReason to blank. Set setAsideReason to blank.readError If a step below fails, save the error as readErrorreading.Run an AI agent: Read the attached document and pull out each of these fields, in this order: - What kind… and save it as reading.answer in reading.output.fields:For each answer in reading.output.fields:answerName to blank. Set answerName to blank.answerNameError If a step below fails, save the error as answerNameErroranswer.name and save it as answerName.Trim answer.name and save it as answerName.answerValue to blank. Set answerValue to blank.answerValueError If a step below fails, save the error as answerValueErroranswer.value and save it as answerValue.Trim answer.value and save it as answerValue.answerListErrorIf a step below fails, save the error as answerListErroranswer.value with , and save it as answerValue.Join answer.value with , and save it as answerValue.answerValue isn't empty If answerValue isn't emptyanswerName: answerValue truncated to 300 characters using … as the ellipsis to the end of documentDetails and save it as documentDetails.Add answerName: answerValue truncated to 300 characters using … as the ellipsis to the end of documentDetails and save it as documentDetails.it could not be read (@readError.message) and save it as failure.Build the text it could not be read (@readError.message) and save it as failure.chosenName to blank. Set chosenName to blank.chosenFolder to blank. Set chosenFolder to blank.documentDetails isn't empty If documentDetails isn't emptychoiceBrief to:Set choiceBrief to:Decide which folder a document belongs in, using only what the document itself says.
The folders:choiceRules to:Set choiceRules to:Reply with exactly one folder name from the list above and nothing else - no explanation, no punctuation. If the document does not clearly belong in exactly one of them, or you are not sure, reply with the single word UNCLEAR. Do not guess.choicePrompt to choiceBrief@choicesTextSet choicePrompt to choiceBrief@choicesText
What the document says:
documentDetails joined by a new line@choiceRules.verdict to blank.Set verdict to blank.choiceErrorIf a step below fails, save the error as choiceErrorchoicePrompt. Save it as chosen.Ask OpenAI: choicePrompt. Save it as chosen.chosen.response_text lowercase and save it as verdict.Make chosen.response_text lowercase and save it as verdict.the folder could not be chosen (@choiceError.message) and save it as failure.Build the text the folder could not be chosen (@choiceError.message) and save it as failure.verdict with replaced by " and save it as verdict.Trim verdict with replaced by " and save it as verdict.verdict ends with . (ignoring case)If verdict ends with . (ignoring case)verdict truncated to the length of verdict minus 1 characters and save it as verdict.Trim verdict truncated to the length of verdict minus 1 characters and save it as verdict.destination in filingDestinations:For each destination in filingDestinations:destination.name in lowercase is verdict If destination.name in lowercase is verdictchosenName to destination.name.Change chosenName to destination.name.chosenFolder to destination.folder.Change chosenFolder to destination.folder.setAsideReason to nothing could be read out of it.Change setAsideReason to nothing could be read out of it.chosenFolder is empty and failure is empty If chosenFolder is empty and failure is emptysetAsideReason is emptyIf setAsideReason is emptysetAsideReason to its contents did not clearly match one of your folders.Change setAsideReason to its contents did not clearly match one of your folders.chosenFolder isn't empty If chosenFolder isn't emptyattachmentNumber.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 boxNameIf 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.uploadErrorIf a step below fails, save the error as uploadErrorchosenFolder to Box folder documentRef. Save it as stored.Upload chosenFolder 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 lookupErrorchosenFolder. Save it as folderItems.List items in Box folder chosenFolder. 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 isn't emptyIf storedLink isn't emptyfiledCount.Add 1 to filedCount.handledCount.Add 1 to handledCount.attachmentLabel under chosenName as boxName.Show in the output: Filed attachmentLabel under chosenName as boxName.it could not be copied into the '@chosenName' folder in Box (@uploadProblem) and save it as failure.Build the text it could not be copied into the '@chosenName' folder in Box (@uploadProblem) and save it as failure.handledCount. Add 1 to handledCount.sortingLabelError If a step below fails, save the error as sortingLabelErrorneedsSortingLabel to email messageId.Add label needsSortingLabel to email messageId.attachmentLabel on messageDescription: the '@needsSortingLabel' label could not be added (sortingLabelError.message) to the end of problems and save it as problems.Add attachmentLabel on messageDescription: the '@needsSortingLabel' label could not be added (sortingLabelError.message) to the end of problems and save it as problems.failure isn't empty If failure isn't emptyattachmentLabel on messageDescription: failure to the end of problems and save it as problems.Add attachmentLabel on messageDescription: failure to the end of problems and save it as problems.filedCount is 0If filedCount is 0attachmentLabel on messageDescription was not filed because failure. It is labelled '@needsSortingLabel' for you. Check the connection or setting this points to; the rest of your mail is left for the next run.Stop and report a problem: Stopped before filing anything: attachmentLabel on messageDescription was not filed because failure. It is labelled '@needsSortingLabel' for you. Check the connection or setting this points to; the rest of your mail is left for the next run.attachmentLabel on messageDescription: setAsideReason to the end of leftForYou and save it as leftForYou.Add attachmentLabel on messageDescription: setAsideReason to the end of leftForYou and save it as leftForYou.attachmentLabel. Show in the output: Left for you: attachmentLabel.messageDescription: messageError.message to the end of problems and save it as problems.Add messageDescription: messageError.message to the end of problems and save it as problems.setAsideErrorIf a step below fails, save the error as setAsideErrorprocessedLabel to email message.Add label processedLabel to email message.needsSortingLabel to email message.Add label needsSortingLabel to email message.filedCount is 0If filedCount is 0messageDescription could not be handled (messageError.message). The rest of your mail is left for the next run.Stop and report a problem: Stopped before filing anything: messageDescription could not be handled (messageError.message). The rest of your mail is left for the next run.handledCount is 0 and leftForYou is empty and problems is emptyIf handledCount is 0 and leftForYou is empty and problems is emptyreportLines to an empty list.Set reportLines to an empty list.filedCount document(s) into Box. to the end of reportLines and save it as reportLines.Add Filed filedCount document(s) into Box. to the end of reportLines and save it as reportLines.messagesLeft is greater than 0If messagesLeft is greater than 0messagesLeft more email(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 email(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.leftForYou isn't emptyIf leftForYou isn't emptyleftForYou with "Join leftForYou with
- and save it as leftForYouList.leftForYouList to the end of reportLines and save it as reportLines.problems isn't emptyIf problems isn't emptyproblems with "Join problems with
- and save it as problemList.problemList 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.problems isn't emptyIf problems isn't emptyrunReport.Stop and report a problem: runReport.runReport.Finish the run: runReport.