Noob Report 2: How it's going, 10 months later

Summary

I started with Logseq in September 2024. I posted my early impressions here. Since then, I’ve been using LS nearly every workday. This is an update of my experience, perhaps of some value to others who are considering a move to Logseq.

Priors

  • Windows 11

  • I’m a happy v0.10 camper. I’m not paying much attention to what’s happening with the db version. My expectation is that it will be fine and I won’t notice much difference if/when the day comes that I must make the switch. I notice a lot of agita about the state of Logseq development and comparisons to other platforms, and honestly, it just doesn’t concern me . Logseq already has significantly advanced my knowledge capture and workflow management. If the devs call it quits tomorrow and never write another line of code, I’ll be indefinitely happy with the way Logseq works today.

  • Strictly desktop. I don’t use, or want, access to Logseq by mobile device. I am a mobile minimalist anyway: no work-related apps or content. It can wait.

  • For this reason, I don’t worry about sync issues. The \assets, \journals, \logseq and \pages directories are stored in my Windows Documents directory, which is automatically backed up to OneDrive by virtue of my Win11 settings. It’s not sync, it’s backup, which is all I really need.

  • I spent most of this time in the original theme. More recently I’ve adopted the Bonofix theme, which I like very much.

What problem was I trying to solve?

I was coming from many years of using OneNote to manage a GTD-esque workflow.

OneNote is a natural GTD platform. You can quickly set up lists for next actions, waiting, etc. You can build a warehouse for reference material. It integrates tightly with Outlook and other Office tools. OneNote is a near-perfect digital version of the analog office with inboxes, to-do-baskets and file cabinets full of paper.

And that also is OneNote’s chief drawback. It may be a digital platform, but like the paper-shuffling office, it is a manual environment, for the most part. Managing GTD in OneNote required lots of clerical touch points: changing the status of tasks, for example. It was cumbersome to create and maintain associations among scattered bits of information — stuff that had more than one relationship. Above all, there was the friction of where — i.e., determining the proper location for each new input or status change. Do the meeting notes go in “meetings”? Or do they go with the project? Both? What if my meeting notes deal with multiple projects? What if a new input is not related to a project and is just a good idea I need to follow up with someone? Where does that go? Etc. I found myself nodding along to @alanyoung as he described this fundamental flaw with a file-and-folder system.

Logseq’s essential insight — blocks and backlinks — seemed to solve the where issue right out of the box.

Still, my chief anxiety at the start was finding stuff. In OneNote, when the question was “where is it?” I had to know the answer. In Logseq I have to know how to ask the question. There’s a difference, and at first, it was a bit of a leap into the unknown.

Learning how to ask LogSeq the right question is a bit of a skill, and it took some practice. I built some tools, methods and documentation that help. Over time, my where anxiety abated. These days I dump new info into my journal with no worry I’ll be able to retrieve it when the need arises.

Biggest wins so far

Templates

I’ve built a template for the daily journal. It’s wired into logseq/config.edn so that the template is inserted into each new day’s journal. The template is built mostly along GTD lines: Inbox, do, delegate, defer; plus a section for meetings. I also have a template for meetings and for project dashboards.

Agendas

The #agenda tag and query are at the top of the list of benefits of how I’ve been using LS so far.

I have lots of meetings. I need to feel confident that when it’s meeting time, I’ve got a current, complete list of topics we need to discuss.

Whenever a topic, fact, question, etc. comes up during note-taking, or just pops into my head and I jot it into the journal, the block is tagged with the person’s [[Name]] and with the #agenda hashtag. This is real easy for capturing I need to talk to [[Name]] about this

Then I created a separate Agenda page. It has a list of blocks. Each block is a different person, or team, with whom/which I have meetings, either regularly scheduled or as-needed. Each one of those persons or teams is their own [[Page]]

Each of those blocks contains a query that returns the person/group’s [[Name]] page and the #agenda hashtag.

Before the meeting

  • I go to the Agenda page, and open the block corresponding to the person or group with whom I am meeting
  • Copy the block embed of the query. That’s important: it’s the block embed of the query, not of the blocks returned by the query
  • Back on the Journal page, I paste the query block embed into the agenda section of the meeting template. This displays all the topics I need to discuss with the person or team.

After the meeting

  • I delete the #agenda hashtag from each block in the query block embed that is pasted in the Agenda section of the meeting.
  • This removes the item from the query and thus also the meeting template – not a loss because my discussion notes are already entered in the meeting template. It also moves the block outside the scope of the query running on the Agenda page in the block associated with that person or group’s name. It cleans up both places at once.
  • This routine, by itself, has made the investment in learning LogSeq worth it. It has been, so far, the most concrete manifestation of the “second brain” attribute of a PKMS. And the most direct realization of frictionless entry/complete recall.

Project dashboards

An essential element of GTD is managing multi-step projects. Because projects tend to span significant stretches of time, inputs tend to trickle in: an email today, a report next week, a meeting on Friday, etc. It’s easy to dump these inputs into Logseq via the journal. The critical issue is keeping these bits connected to the project, and being able to recall them.

In my system, each project gets a [[project name]]. Each parent block related to the project – notes of meeting, etc. – includes the [[project name]]

For each project I also build a dashboard. The dashboard is a Logseq template that I built, and it contains basic project info: name, purpose, principals, a section for links to resource materials, and a task list, which is continually updated. I explained how the task list works here.

I keep a page that is nothing but a query that displays a list of all projects, with links to their dashboards. This gives me a one-stop place to find all projects and quickly click in to their dashboards.

Biggest challenges so far

Email

Managing email is essential to any workflow. Email contains inputs that need to be dumped into the GTD inbox, assessed, and tracked. Because my GTD Inbox is in my Logseq daily journal, I need a way to make new emails visible there. But I also do not want to allow hundreds, and eventually thousands, of emails to pile up indefinitely in the assets folder. I need a way to move email into Logseq, and also a method to remove email from \assets when it’s no longer needed. (See Bloat, below).

Microsoft Exchange (Outlook) does not integrate with Logseq, so managing email is a manual, somewhat clunky, process. This is Logseq’s weakest link. Here’s the method I’ve devised:

Email for projects

Whenever I create a new project, I anticipate I will be receiving regular email about that project. Accordingly, in Outlook I create a new folder and give it the same name as [[Project Name]] in Logseq. I keep all of it there. Over in Logseq, in the project dashboard, I enter a block that reminds me that all email related to the project is stored in that Outlook folder.

As email arrives in Outlook, I determine if the email is related to a project I am managing. If yes, then:

  • I move the email into the Outlook folder created for the project
  • Over in Logseq, I create a new block in the GTD inbox with a quick Email: [[Project name]] note that alerts me to the new email. Later, as I work through the GTD inbox, the note will prompt me to review the email.
  • After I review the email, I will update the Logseq block with appropriate GTD instruction (and move it to the proper GTD category.

Standalone email (not related to a project)

  • I save the email to my local \attachments directory as a .msg file, then in Logseq I /upload the .msg file to the daily journal GTD inbox. After I’ve cleared the Outlook inbox, I’ll jump over to Logseg and work through the GTD Inbox, opening the .msg files as I come to them. I will assess the emails under the GTD method, and either deal with the email immediately or move it into the Next Actions list per the GTD method.

  • When emails are no longer needed, I search/delete the .msg file from \assets and delete the block from Logseq.

All of this is less than elegant and, so far, has proven to be Logseq’s No. 1 weakness in my use case. But with time this workaround has become more committed to muscle memory and is only a minor inefficiency.

Everyday stuff I use

  • Logseq Mastery course, by Dario da Sylva. I’m not even halfway through the course. I go through a new chunk every now and then. It’s been a worthwhile investment of money and time. I find comfort in having it at handy as a reference. If you don’t want to spend the money, Dario has made a slice of his course available for free on YouTube , and it will get you a good way up the learning curve all by itself.

  • aText text expander. This is a suggestion I picked up from Dario, and I’m glad I did. I use aText to:

    • Apply markdown in Logseq, especially for headings. Typing #3 applies the ### Markdown code to the block, for example
    • Assign properties to blocks
    • Type query strings, e.g. there’s an aText command that will type out the and query syntax. I just fill in the search values.
    • Fill out my keyboard shorthand, e.g. aText will turn cd into could.
  • Self-edited help page. As I discover new things about Logseq that I want to remember, I keep notes about them in a help page of my own making.

  • Queries library. Similarly, as I devise new queries that I know I want to use again, I keep a page dedicated to them, explaining their purpose and spelling out the query string syntax. Then I build aText shortcuts that will invoke the query.

  • Reference material. Long-term storage of reference material is kept in One Note, not Logseq. Links to the OneNote sections/pages are placed in Logseq blocks as necessary. Those blocks are given a gtd property value of reference. That way, an and query can call up links to reference material associated with a given search term or page or project.

Stuff I still haven’t figured out

The Deadline and Scheduled functions

How do these relate to the daily journal? It remains a mystery to me. In particular, I have not yet figured out how to have the journal display only those dated items that are due, or scheduled, for the current day. It’s just the same dated items in a list, every day.

Stuff I worry about

Bloat

Saving email messages seems wasteful. The message (A) already is stored in Outlook local directory. Then a copy (B) must be saved to another location. That copy is then uploaded (C) to Logseq and stored in the assets folder. That’s 3x the storage for the same email. To prevent bloat, I have to delete B and C when the email is no longer needed.

Similarly, I am careful about uploading documents to Logseq. Once there is no longer a need to keep a document close at hand, I will move it from assets to long-term storage in OneNote.

When TODO items are complete, I tend to delete the block, not merely mark the item as done — unless the task is part of a project task list. I will delete the entire list when the project is done.

I don’t know whether bloat is a justified worry. If not, I am trying to regularly prune the ever-accumulating blocks in the graph, in the name of good PKM platform hygiene.

10 Likes

Hi @jthomas.
Why export an Outlook item when you can link to it instead?!

This VBA script generates an interactive HTML dashboard that lists Outlook items (Appointments, Tasks, and Emails) created or modified within the last X days. For each item, it displays:

  • the date
  • the subject as a clickable link using the outlook:entryID format
  • the folder path
  • a “Copy link HTML” button to copy the <a href='outlook:...'>...</a> HTML code for pasting in OneNote, Word, or any web content

:magnifying_glass_tilted_left: How it works

  1. Folder traversal
    The script recursively scans all folders in the mailbox open when run (excluding “Deleted Items”, “Sync Issues”, “Bin”, “Sync Issues (This computer only)”, “Spam” and “Inbox” for emails).
  2. Date filter
    It selects only items created or modified in the last X days (default: 7).
  3. HTML generation
    Each item is rendered as a table row with:
  • a clickable link
  • folder name
  • a button to copy the HTML link
  1. Export and open
    The file dashboard.html is written to the Desktop and automatically opened in the browser.
  2. User interaction
    Sections for Appointments, Tasks, and Emails are collapsible, and the copy button uses JavaScript to copy the link HTML.
    Sub GeneraDashboardHTML()
        Const GIORNI_INDIETRO As Integer = 7
        Dim olApp As Outlook.Application
        Dim olNS As Outlook.NameSpace
        Dim currentFolder As Outlook.MAPIFolder
        Dim storeRoot As Outlook.MAPIFolder
        Dim olFolder As Outlook.MAPIFolder
        Dim htmlApp As String, htmlTask As String, htmlMail As String
        Dim html As String, percorsoFile As String
    
        Set olApp = Outlook.Application
        Set olNS = olApp.GetNamespace("MAPI")
        Set currentFolder = Application.ActiveExplorer.currentFolder
        Set storeRoot = currentFolder.Store.GetRootFolder
        Set olFolder = storeRoot
    
        Call ScansionaCartelleFiltrate(olFolder, GIORNI_INDIETRO, htmlApp, htmlTask, htmlMail)
    
        html = "<html><head><style>" & _
               "body { font-family: Segoe UI, Arial, sans-serif; line-height: 1.5; }" & _
               "table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" & _
               "th, td { border: 1px solid #ccc; padding: 6px; text-align: left; vertical-align: top; }" & _
               "th { background-color: #f0f0f0; }" & _
               "th.date, td.date { width: 120px; white-space: nowrap; }" & _
               "h2 { cursor: pointer; user-select: none; }" & _
               "</style></head><body>"
    
        html = html & "<h1>Dashboard Outlook – ultimi " & GIORNI_INDIETRO & " giorni</h1>"
    
        If htmlApp <> "" Then
            html = html & _
                "<h2 onclick='toggle(""app"")'>Appuntamenti</h2>" & _
                "<div id='app'><table><thead><tr>" & _
                "<th class='date'>Data</th><th>Oggetto</th><th>Cartella</th><th></th>" & _
                "</tr></thead><tbody>" & _
                htmlApp & "</tbody></table></div>"
            End If
        
            If htmlTask <> "" Then
                html = html & _
                "<h2 onclick='toggle(""task"")'>Task</h2>" & _
                   "<div id='task'><table><thead><tr>" & _
                "<th class='date'>Data</th><th>Oggetto</th><th>Cartella</th><th></th>" & _
                "</tr></thead><tbody>" & _
                htmlTask & "</tbody></table></div>"
            End If
        
            If htmlMail <> "" Then
                html = html & _
                "<h2 onclick='toggle(""mail"")'>Email</h2>" & _
                "<div id='mail'><table><thead><tr>" & _
                "<th class='date'>Data</th><th>Oggetto</th><th>Cartella</th><th></th>" & _
                "</tr></thead><tbody>" & _
                htmlMail & "</tbody></table></div>"
            End If
    
        html = html & "<script>" & vbCrLf & _
              "function toggle(id) {" & vbCrLf & _
              "  var el = document.getElementById(id);" & vbCrLf & _
              "  el.style.display = el.style.display === 'none' ? 'block' : 'none';" & vbCrLf & _
              "}" & vbCrLf & _
              "function copyHtml(btn) {" & vbCrLf & _
              "  var html = btn.getAttribute('data-html');" & vbCrLf & _
              "  navigator.clipboard.writeText(html).then(() => {" & vbCrLf & _
              "    btn.innerText = 'Copied!';" & vbCrLf & _
              "    setTimeout(() => { btn.innerText = 'Copy link HTML'; }, 2000);" & vbCrLf & _
              "  });" & vbCrLf & _
              "}" & vbCrLf & "</script>"
    
        html = html & "</body></html>"
    
        percorsoFile = Environ("USERPROFILE") & "\Desktop\dashboard.html"
        With CreateObject("Scripting.FileSystemObject").CreateTextFile(percorsoFile, True, True)
            .Write html
            .Close
        End With
    
        shell "cmd /c start """" """ & percorsoFile & """", vbHide
    End Sub

    Sub ScansionaCartelleFiltrate(f As Outlook.MAPIFolder, giorniIndietro As Integer, _
                                  ByRef htmlApp As String, ByRef htmlTask As String, ByRef htmlMail As String)
        Dim sottof As Outlook.MAPIFolder
        Dim dataRif As Date: dataRif = Now - giorniIndietro
        Dim items As Outlook.items
        Dim item As Object
        Dim tipo As String, dataItem As Date
        Dim htmlRiga As String
    
        ' Escludi cartelle indesiderate
        If f.Name = "Deleted Items" Or f.Name = "Sync Issues" Then Exit Sub
    
        Set items = f.items
        On Error Resume Next
    
        If f.DefaultItemType = olMailItem Then
            items.Sort "[LastModificationTime]", True
        Else
            items.Sort "[CreationTime]", True
        End If
    
        On Error GoTo 0
    
        For Each item In items
            tipo = TypeName(item)
            On Error Resume Next
    
            Select Case tipo
                Case "AppointmentItem"
                    dataItem = item.CreationTime
                    If dataItem >= dataRif Then
                        htmlRiga = CostruisciRigaTR(dataItem, item.subject, item.entryID, item.Parent.folderPath)
                        htmlApp = htmlApp & htmlRiga
                    End If
    
                Case "TaskItem"
                    dataItem = item.CreationTime
                    If dataItem >= dataRif Then
                        htmlRiga = CostruisciRigaTR(dataItem, item.subject, item.entryID, item.Parent.folderPath)
                        htmlTask = htmlTask & htmlRiga
                    End If
    
                Case "MailItem"
                    If f.Name <> "Inbox" Then
                        dataItem = item.LastModificationTime
                        If dataItem >= dataRif Then
                            htmlRiga = CostruisciRigaTR(dataItem, item.subject, item.entryID, item.Parent.folderPath)
                            htmlMail = htmlMail & htmlRiga
                        End If
                    End If
            End Select
            On Error GoTo 0
        Next
    
        For Each sottof In f.Folders
            Call ScansionaCartelleFiltrate(sottof, giorniIndietro, htmlApp, htmlTask, htmlMail)
        Next
    End Sub
    
Function CostruisciRigaTR(dataItem As Date, subject As String, entryID As String, folderPath As String) As String
    Dim linkHref As String
    Dim htmlEncodedSubject As String
    htmlEncodedSubject = HtmlEncode(subject)

    linkHref = "<a href='outlook:" & entryID & "'>" & htmlEncodedSubject & "</a>"

    Dim encodedHtmlLink As String
    encodedHtmlLink = HtmlEncode("<a href='outlook:" & entryID & "'>" & subject & "</a>")

    CostruisciRigaTR = "<tr>" & _
        "<td class='date'>" & Format(dataItem, "yyyy-mm-dd") & "</td>" & _
        "<td>" & linkHref & "</td>" & _
        "<td>" & HtmlEncode(folderPath) & "</td>" & _
        "<td><button onclick=""copyHtml(this)"" data-html=""" & encodedHtmlLink & """>Copy link HTML</button></td>" & _
        "</tr>" & vbCrLf
End Function

Function HtmlEncode(text As String) As String
    HtmlEncode = text
    HtmlEncode = Replace(HtmlEncode, "&", "&amp;")
    HtmlEncode = Replace(HtmlEncode, "<", "&lt;")
    HtmlEncode = Replace(HtmlEncode, ">", "&gt;")
    HtmlEncode = Replace(HtmlEncode, """", "&quot;")
    HtmlEncode = Replace(HtmlEncode, "'", "&#39;")
End Function

Note: You have to move the email to its final folder before running the script and copying the link. This because, for email only, “entryID”, different thing from “GUID”, changes every time the folder changes.

1 Like

Very nice write-up. Thank you for writing your report and sharing parts of your workflow.

I strongly agree that the tie-in between email and logseq is one of the aspects that feel clunkiest.

Similarly, to @Harlock’s suggestion, I use a unique identifier for the email instead; an outlook macro to obtain it, and an outlook macro to retrieve the email.

To obtain the ID

Sample output

From: [[John Smith]] | To: [[Jane Doe]] [[Janine Williams]] | CC: [[Big Important Team]] 
Subj: "RE: Big Important Project: This message contains important/actionable information" | Date: [[2025-07-15 Tuesday]] 08:55h 
Email ID: 000000007A49FE62CED12D4AAF0785663E81E3AE0700054D9BA3012D5646B7D0FCAA092E54AC00000000010C0000054D9BA3012D5646B7D0FCAA092E54AC0003D8D0B9470000 #@email-message

Code

Sub CreateOutlookTaskString()
'Adds a link to the currently selected message to the clipboard

Dim doClipboard As New DataObject
Dim oMail As Outlook.MailItem


Set oMail = ActiveExplorer.Selection.item(1)

Dim MailDate As String
Dim MailTime As String
MailDate = "[[" + Format(oMail.ReceivedTime, "YYYY-MM-DD dddd") + "]]"
MailTime = Format(oMail.ReceivedTime, "hh:mm") + "h "

FromLinks = ToLogseqString(oMail.SenderName)
ToLinks = ToLogseqString(oMail.To)
CCLinks = "None"
If Len(oMail.CC) > 1 Then
    CCLinks = ToLogseqString(oMail.CC)
End If

SetClipBoardText ("From: " + FromLinks + "| To: " + ToLinks + "| CC: " + CCLinks + vbNewLine + "Subj: """ + oMail.subject + """ | Date: " + MailDate + " " + MailTime + vbNewLine + "Email ID: " + oMail.entryID + " #@email-message")

Set oMail = Nothing
End Sub

Private Function ToLogseqString(oMailStr As String) As String
    On Error GoTo Error_Handler
 
    Dim RecList As String
    Dim NameRecipients As String
    Dim Counter

    RecList = " " & oMailStr & ";" '<-- structure of oMailStr is "Smith Smoke, John; Martinez, Ricardo", it adds " " at the beginning and ";" at the end to ensure same structure for all recipients and ease looping
    'MsgBox "RecList: " & RecList & vbCrLf & vbCrLf '<-- for Debugging'
    numberTos = CountOccurrences(RecList, ";") '<-- there are as many Recipients as semicolons

    Counter = 0
    pos = 1
    While Counter < numberTos ' Test value of Counter.
        endOfSemicolon = InStr(pos, RecList, ";") '<-- structure of RecList is "A; B; C;" and we look for the semicolon
        'MsgBox "Pos: " & Pos & vbCrLf & vbCrLf & _
        '        "endOfSemicolon: " & endOfSemicolon & vbCrLf & vbCrLf & _
        '        "From Pos the length of the name: " & Mid(RecList, Pos, endOfSemicolon - Pos) & vbCrLf & vbCrLf
        
        endOfRecLastName = InStr(pos, RecList, ",") '<-- structure is " Smith Smoke, John" and we look for the comma
        'MsgBox "endOfRecLastName: " & endOfRecLastName & vbCrLf & vbCrLf



        If endOfRecLastName = 0 Or endOfRecLastName > endOfSemicolon Then
        'MsgBox "Structure most likely is ' John Smith Smoke' or ' email@domain.com'" & vbCrLf & vbCrLf
            RecLastName = ""
            'MsgBox "RecLastName: " & RecLastName & vbCrLf & vbCrLf
            
            RecFirstName = Mid(RecList, pos + 1, endOfSemicolon - pos - 1)
            'MsgBox "RecFirstName " & RecFirstName & vbCrLf & vbCrLf

        Else
        'MsgBox "Structure must be ' Smith Smoke, John'" & vbCrLf & vbCrLf
            RecLastName = Mid(RecList, pos + 1, endOfRecLastName - pos - 1) '<-- from Pos (+1 to not grab the space) same length as endOfRecLastName (-1, since we skipped the space)
            'MsgBox "RecLastName: " & RecLastName & vbCrLf & vbCrLf
            
            RecFirstName = Mid(RecList, endOfRecLastName + 2, endOfSemicolon - endOfRecLastName - 2) & " " '<-- from endOfRecLastName +2 to skip comma and space, length -2 since we skipped the comma and space
            'MsgBox "RecFirstName " & RecFirstName & vbCrLf & vbCrLf

        End If

        NameRecipients = NameRecipients & "[[" & RecFirstName & RecLastName & "]] " '<-- transform the name into a logseq-style link and add it to the string of name links
        'MsgBox "NameRecipients: " & NameRecipients & vbCrLf & vbCrLf
        
        Counter = Counter + 1
        pos = endOfSemicolon + 1 ' <-- set starting position to right after the completed item
    Wend

    ToLogseqString = NameRecipients
 
Error_Handler_Exit:
    On Error Resume Next
    Exit Function
 
Error_Handler:
    MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Source: ToLogseqString" & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occurred!"
    Resume Error_Handler_Exit
End Function

To retrieve the email:

Sub OpenByEntryID()

    Dim App As Outlook.Application
    Dim ns As Outlook.NameSpace
    Dim Msg As Outlook.MailItem
    Dim MsgID As Variant

On Error Resume Next
    Set App = CreateObject("Outlook.Application")
    Set ns = App.GetNamespace("MAPI")
    ns.Logon
    MsgID = InputBox("Enter EntryID")
    Set Msg = ns.GetItemFromID(MsgID)
    Msg.Display

End Sub

P.S. to me, a synced service is not a back-up, but you are likely already aware of the trade-offs and it’s certainly convenient.

1 Like

Thanks for sharing the code here. I do not think I will use it with logseq, and the hundreds of emails that it returns can be daunting, but I will keep it in my toolshed for when I cannot find something or need to extract a summary of all Outlook activity from a certain period. I appreciate it!