Why the search query is the whole trick
Gmail's search API is stateless. Every time your script calls GmailApp.search(), it re-scans your inbox from scratch against whatever query string you pass in. There is no cursor, no offset, no marker that says "start where I left off." If you search for 'in:inbox' on a 15-minute trigger, you will log every thread in your inbox on every run — duplicating rows until the sheet is worthless.
The fix is a label. Gmail labels are persistent metadata on threads; they survive across sessions, API calls, and time. The search query '-label:Logged in:inbox' tells Gmail to return only threads that do not have the Logged label. After the script appends a row for a thread, it immediately calls addLabel() on that thread, so the next trigger cycle skips it. The label acts as a processed-set, and the query exclusion makes the whole operation idempotent — safe to run as often as you want.
One edge case worth knowing: GmailApp.search() caps at 500 results per call. If a label-blast or newsletter flood drops more than 500 unlabeled threads into your inbox between runs, you will fall behind. For high-volume inboxes, drop the trigger interval or add a batch loop that calls search() in pages using the start parameter.