Advanced Logseq Queries for Real‑World Catalogs: From Zero to Powerful

Turn your property‑rich blocks into clean, clickable tables without plugins. A friendly, practical guide to advanced Logseq queries you can follow in a coffee break.

Who this is for: Anyone who stores structured info in Logseq properties (think: reading notes, vendor lists, research registries, design patterns, bookmarks) and wants to build useful tables with internal and external links using Advanced Logseq Queries.


Contents

  • The Challenge
  • Why advanced Logseq queries (and what Logseq Advanced Queries do)
  • Your data model: how Logseq stores page names and block properties
  • A tiny dataset you can copy
  • Start simple: find property‑only blocks
  • Make it visual: from tuples to a custom table view
  • Clickable links (internal page links vs external URLs)
  • Sorting, column order, and scoping to a page tree
  • Debugging patterns that actually help
  • Use cases & ideas
  • Copy‑paste recipes

The Challenge

LogSeq became a central place where I stored notes and knowlege, and LogSeq Advanced Queries open a lot of doors. However, knowledge was increasingly coming from external sources. This is easy with LogSeq as you can just add any markdown folder in a subfolder of the its pages folder, and it immediately becomes available via its file name.

The source of truth was often external, meaning that I can copy and paste an updated version over the LogSeq copy at any time. To handle this, I created a LogSeq page that has a set of tags for each document with each set of tags in its own block. This became a databases of key fields such as the LogSeq link to the document

Here is an example of what a block could look like:

doc_type:: mirror
doc_page:: [[server123 - BIOS]]
doc_source_title:: server123 - Install Rocky 9
doc_source_url:: https://example.com/theUrl
doc_area:: Server Infrastructure
doc_status:: active
doc_last_sync:: [[Sep 23rd, 2025]]

Now I needed to query it to produce a table. I also wanted it to create an external link combing the doc_source_title for display with a link that opens the doc_source_url in the browser, introducing the concept of a computed field in our output table.

There are a lot of examples online for querying page properties or TODO properties, but not a lot on querying custom Logseq block properties that could be anywhere. The examples we have here don’t care that these are all in a single page, or that the block only has properties, though those were my conventions. This covers how to create a table for block tags that can be anywhere, though you can certainly scope it to just one page, children of the current page, or any filter you want. It’s just not a limitation.

With advanced Logseq queries, you can build reusable catalogs from block properties and render custom views.

LogSeq version used: 0.10.12

Why advanced Logseq queries?

Because the built‑in queries are great for TODOs and page lists, but as soon as you start cataloging things with custom properties (e.g., item-url::, item-title::, source-page::), you’ll want:

  • A table with exactly the columns you care about
  • A column that opens a Logseq page, and perhaps another that opens an external site
  • The ability to sort, filter, and reuse the pattern across different datasets

Advanced Logseq queries give you that control—especially when paired with a minimal custom view.


How Logseq stores your data (the 3 gotchas)

  1. Page names are normalized and stored as :block/name in lowercase with slashes preserved (e.g., projects/catalog/registry).
  2. Block properties live in a map at :block/properties. A property like: item-page:: [[My Page]] is often stored as a set in the DB: #{"My Page"}. You’ll usually want (first …).
  3. Hyphens vs underscores: The keyword you use in queries must match the stored key. Both forms are valid:
    • :item-page (hyphen)
    • :item_page (underscore)
      The DB will expose whichever you see when you view the property, which can be different than what you used and see when you are editing the block. Underscores are converted to hyphens. We’ll show the hyphen style in examples.
advanced Logseq queries

A tiny dataset you can paste

Create a page named Projects/Catalog/Registry and add a few properties‑only blocks (no body text is fine):

item-page:: [[Example Page A]]
item-title:: A Great Article
item-url:: https://example.com/a

item-page:: [[Example Page B]]
item-title:: Demo Docs
item-url:: https://example.com/docs

item-page:: [[Example Page C]]
item-title:: Product Overview
item-url:: https://example.com/product

Start simple: return blocks with the properties you need

We’ll bind from the properties map (reliable for properties‑only blocks):

#+BEGIN_QUERY
{
  :title [:h3 "Catalog – raw tuples"]
  :query [
    :find ?b ?props
    :where
      [?b :block/properties ?props]
      [(get ?props :item-page)  ?_]
      [(get ?props :item-title) ?_]
      [(get ?props :item-url)   ?_]
  ]
}
#+END_QUERY

You’ll see a list of pairs: the block id and its properties map. Good—data is flowing.


Make it visual: render a minimal custom table

The built‑in table tries to guess columns from pulled fields and isn’t great for computed columns in 0.10.x. Use a custom :view to render exactly what you want.

#+BEGIN_QUERY
{
  :title [:h3 "Catalog – page | title | url table"]
  :query [
    :find ?b ?props
    :where
      [?b :block/properties ?props]
      [(get ?props :item-page)  ?_]
      [(get ?props :item-title) ?_]
      [(get ?props :item-url)   ?_]
  ]
  :view (fn [rows]
    (let [pairs (partition 2 (into [] rows))]
      [:table
       [:thead [:tr [:th "Item page"] [:th "Title"] [:th "URL"]]]
       [:tbody
        (for [[_ p] pairs
              :let [pg-val (:item-page p)
                    pg     (if (coll? pg-val) (first pg-val) pg-val)
                    t      (:item-title p)
                    u      (:item-url  p)]]
          [:tr
           [:td (str pg)]
           [:td (str t)]
           [:td (str u)]])]]))
}
#+END_QUERY

To open a Logseq page from a custom view, link to the internal route #/page/<name>. For external URLs, use a normal href.

[:a {:href (str "#/page/" pg)} (str pg)]      ; internal
[:a {:href (str u)} (str t)]                    ; external

Putting it together (2 columns only):

#+BEGIN_QUERY
{
  :title [:h3 "Catalog – item page + external link"]
  :query [
    :find ?b ?props
    :where
      [?b :block/properties ?props]
      [(get ?props :item-page)  ?_]
      [(get ?props :item-title) ?_]
      [(get ?props :item-url)   ?_]
  ]
  :view (fn [rows]
    (let [pairs (partition 2 (into [] rows))]
      [:table
       [:thead [:tr [:th "Item page"] [:th "Resource"]]]
       [:tbody
        (for [[_ p] pairs
              :let [pg-val (:item-page p)
                    pg     (if (coll? pg-val) (first pg-val) pg-val)
                    t      (:item-title p)
                    u      (:item-url  p)]]
          [:tr
           [:td [:a {:href (str "#/page/" pg)} (str pg)]]
           [:td [:a {:href (str u)} (str t)]]])]]))
}
#+END_QUERY

Why not .page-ref? In custom views, .page-ref spans can bubble clicks to the underlying block and trigger edit mode. Routing with #/page/<name> avoids that.


Sorting, column order, and scoping

  • Column order Whatever order you emit <th>/<td> in your custom :view is the order you’ll see—just rearrange those cells to reorder columns.
  • Scope to a page only after you confirm bindings: [?b :block/page ?p] [?p :block/name "projects/catalog/registry"]
  • Child pages: Instead of constraining in :where, filter in the view by checking the page name prefix if you also pull it.
  • Row sorting: Instead of constraining in :where, filter in the view by checking the page name prefix if you also pull it.

The most robust time sort: sort by block id (newest first)

Block ids are monotonic enough for “newest first” and don’t require extra attributes. Keep your proven :find ?b ?props shape and sort in the view:

:query [
  :find ?b ?props
  :where
    [?b :block/properties ?props]
    [(get ?props :item-page)  ?_]
    [(get ?props :item-title) ?_]
    [(get ?props :item-url)   ?_]
]
:view (fn [rows]
  (let [pairs  (partition 2 (into [] rows))        ;; [[id props] ...]
        sorted (reverse (sort-by (fn [[id _]] id) pairs))]  ;; newest first
    [:table
     [:thead [:tr [:th "Item page"] [:th "Resource"]]]
     [:tbody
      (for [[_ p] sorted
            :let [pgv (:item-page p)
                  pg  (if (coll? pgv) (first pgv) pgv)
                  t   (:item-title p)
                  u   (:item-url p)]]
        [:tr
         [:td [:a {:href (str "#/page/" pg)} (str pg)]]
         [:td [:a {:href (str u)} (str t)]]])]]))

Why this? Sorting by timestamps can be flaky across graphs/builds; id sort is reliably stable.

advanced Logseq queries scoping


Debugging patterns that actually help

  1. Dump rows: :view (fn [rows] [:pre (pr-str (take 3 (into [] rows)))])
  2. Normalize page refs: pg might be a set → use (first pg).
  3. Avoid mixing pull + scalars in one :find row when you plan to reshape columns—prefer a custom view with either only scalars or a single pull, not both.
  4. Renderer errors often mean shape mismatches; step back to a smaller working shape and grow it.

Use cases & ideas

  • Reading registry: item-page, item-title, item-url, author, summary. Show page and link to the article.
  • Vendor catalog: vendor-page, vendor-name, contact-url, category. Show vendor page and website link.
  • Research corpus: paper-page, paper-title, pdf-url, venue, year. Show page + PDF link; sort by year.
  • Bookmark library: bookmark-page, title, url, tags. Filter by tags with a tiny extra predicate.

Copy‑paste recipes

A) Minimal tuples filtered by properties

:query [
  :find ?b ?props
  :where
    [?b :block/properties ?props]
    [(get ?props :item-page)  ?_]
    [(get ?props :item-title) ?_]
    [(get ?props :item-url)   ?_]
]

B) Two‑column custom table (internal page | external link)

:view (fn [rows]
  (let [pairs (partition 2 (into [] rows))]
    [:table
     [:thead [:tr [:th "Item page"] [:th "Resource"]]]
     [:tbody
      (for [[_ p] pairs
            :let [pg-val (:item-page p)
                  pg     (if (coll? pg-val) (first pg-val) pg-val)
                  t      (:item-title p)
                  u      (:item-url  p)]]
        [:tr
         [:td [:a {:href (str "#/page/" pg)} (str pg)]]
         [:td [:a {:href (str u)} (str t)]]])]])

C) Add a third column or reorder columns
Just add/permute the <th> and <td> cells in the :view hiccup.

D) Scope to a single page (optional)

:where
  [?b :block/page ?p]
  [?p :block/name "projects/catalog/registry"]

Final thoughts

Advanced queries look intimidating because Datalog is new to many of us. But the winning pattern is simple:

  1. Bind from :block/properties with get.
  2. Keep result shape tiny and predictable.
  3. Render the table yourself with a custom :view.

Once you’ve got Logseq datalog down, you can mix in sorting, filtering, and richer columns at your own pace.

If you turn this into your own internal registry template, share it—this pattern helps anyone building real‑world catalogs in Logseq using advanced Logseq queries.

About Erik Calco

With a passion for Investing, Business, Technology, Economics, People and God, Erik seeks to impact people's lives before he leaves. Contact Erik
This entry was posted in Technology, Learning, Uncategorized and tagged , , . Bookmark the permalink.

Leave a Reply