Make custom macros that replace themselves with their value on first run

Thanks @mentaloid for sticking with this. If Logseq would provide a PortableApp I wouldn’t have to do stuff like this (it’s quie annoying to do this every update - i have updated 0.9.17 to 0.9.17 on this ocasion ).

The good:

  • I now see the “kits ok” message at Logseq startup;

The bad:

  • I have only two macros in my config.edn:
 :macros {
 :page-date "<% current page %>"
 :time "<div class='kit' data-kit='expandmacro'> <%time%></div>"

, but when I trigger the Macro with:

- time is {{time}}

It is not replaced with the time but rather functions the old way (same as :time "<%time%>" macro would), by updating this time every time i refresh (click on it to see the “{{time}}” text) :face_with_diagonal_mouth:

Edit: it looks like the

<div class='kit' data-kit='expandmacro'> <%time%></div>

definition isn’t applied somehow, maybe the expandmacro function in is not reached/applied… (is there a js command I can add to this function to show something on screen or whatever just so I can see if it’s actually run? - I tried with adding console.log("Expanding macro: " + macroName); to the function but I get nothing at the Logseq Dev Tools Console…)

  • Now that kits are ok, you can use {{togglemsg}} to get a button for toggling some potentially useful messages.
    • Enabling those messages in your case, should probably say that expandmacro is missing, because its codeblock was not found.
    • So it falls back to the old/repeating behavior.
  • Ensure that Logseq page ExpandMacro is present and contains the needed codeblock.
    • If the page seems empty, re-indexing may be needed.
  • You can further debug in the console, checking that logseq.kits contains logseq.kits.expandmacro

yeah, that is what I get:

The weird thing is that I have the markdown file under /pages (tried even copy/pasting it into the /logseq folder and I get a warning that I already have a page with that name so Logseq clearly finds/sees it). It contains your updated function:

logseq.kits.setStatic(function expandmacro(div){
    const blockId = div.closest(".ls-block").getAttribute("blockid");
    const content = logseq.api.get_block(blockId).content;
    const divMacro = div.closest(".macro");
    const macroName = (divMacro) ? divMacro.dataset.macroName : ( || "");
    const macroStart = content.indexOf("{{" + macroName);
    const macroEnd = content.indexOf("}}", macroStart) + 2;
    logseq.api.update_block(blockId, content.slice(0, macroStart) + div.innerHTML + content.slice(macroEnd));

I see nothing in the Dev Console when I click-in/out of the macro line.

Where should I see “that logseq.kits contains logseq.kits.expandmacro” ?

Hmm… copied that function inside custom.js and all works ok :face_with_diagonal_mouth:

It somehow finds the /pages/ because, if I remove that file, I get a different first error message: instead of ran 0 codeblock(s) I get page not found:

ok, I created the outside Logseq and had the dash for the block missing. I opened the page from Logseq and it added the dash (created the block containing the function) but I am sill having the same error.

This is what I get after I open the expandmacro page in logseq and I switch to the scratchpad page where I run my tests:


Ok, I solved the mystery :man_facepalming:

I omitted using the code-block for the javascript function… :roll_eyes:
Once I added it (without the “\” obviously):

logseq.kits.setStatic(function expandmacro(div){
    const blockId = div.closest(".ls-block").getAttribute("blockid");
    const content = logseq.api.get_block(blockId).content;
    const divMacro = div.closest(".macro");
    const macroName = (divMacro) ? divMacro.dataset.macroName : ( || "");
    const macroStart = content.indexOf("{{" + macroName);
    const macroEnd = content.indexOf("}}", macroStart) + 2;
    logseq.api.update_block(blockId, content.slice(0, macroStart) + div.innerHTML + content.slice(macroEnd));

It all works as expected.

Thanks for sticking with me. :bowing_man:

This is exactly what I was thinking, to have macros that are not dynamic but static -replacing themselves at runtime is something Logseq Maros shoudl have as a parameter or something. I wonder why nobody has posted any use cases … :thinking:

Man, it’s no easy task to get uploads less than 4MB in here. I wanted for a quite some long time to showcase my need for macros that replace themselves, so here is my use case:

  • Logseq Dynamic Variables are available in Templates, Queries and Macros. They are not available to Custom Commands nor they are available to Journal Blocks/Pages. I wanted to make them available to daily usage in Blocks as well as in Block Templates created with Custom Commands;
  • Macros are the obvious solution to be able to insert Dynamic Variables in Blocks. But there is one fat problem. Macros are Dynamic themselves and they update every time the page is loaded. So I was stuck until this solution -luckily- became available; :bowing_man: @Mentaloid
  • Now I can have Macros that “replace themselves with their result”. Awesome. So I created a few Macros that access Dynamic Variables:
 :macros {
 ;; make Dynamic Variables accessible by running [custom macros that replace themselves with their value on first run](  
 :current-page "<div class='kit' data-kit='expandmacro'><% current page %></div>"
 :time "<div class='kit' data-kit='expandmacro'><%time%></div>"
 :today "<div class='kit' data-kit='expandmacro'><%today%></div>"
  • Finally, I added the folowing Macro to be able to access the natural language syntax for dates, ex: <%next Tuesday%>:
 :var "<div class='kit' data-kit='expandmacro'><%$1%></div>"
  • I can write, then:
{{var next tuesday}}

and it gets replaced with the appropriate date just as I would do by using the /date Command and then selecting, with mouse, the day I am interested in.

Here is a screen recording showcasing it (you might want to right click > “open image in new Tab” for larger viewpoint)…


Now, to be able to pre-fill some properties in Block Templates (yes, these are Templates for a single Block that I can easily access by typing “<template_name” instead of going the route → “/templ”{ENTER} + Template_Name{Enter}, which would ruin my Quick Capture experience):



What’s the advantage of this compared to dynamic variables in templates?

I use a lot of Single-Block-“Templates” for every input i do in Logseq. I rarely do multi-block templates (the daily journal template is one of those, for example). I totally dislike to have to use /template {Enter}, then template name {Enter} and would rather do <template-name{Enter}. I also design my “single-block-teplates” (actually Custom Commands) to leave the cursor exactly where I feel it’s the most logical for quick capture of data (something Templates can’t do). And, because Custom Commands can’t use Dynamic Variables, I had to somehow make them work via Macros. But Normal Macros won’t do, as they are dynamic and change their result at every refresh. So @mentaloid 's solution is gold to my specific use case.


I’ve been looking for this since literally today and found today. This seems like it should become an official feature of Logseq. Thanks for the discussion here, very clear.

My usage is just writing complex templated, long winded latex equations for category theory. You can use the Katex macros but it requires custom.js. Going this far I found this post, which only required a little more and made the argument clear how it could be done. I just did this.

:var "<div class='kit' data-kit='expandmacro'>\u0024\u0024 $1 \u0024\u0024</div>"

Thank again guys.

1 Like

@mentaloid Is there a way to circumvent macros not being parsed inside Named Links?

If I do Ctrl+L I get “[]()” and the cursor is placed inside the square brackets, where I write my name for the link, then I go in between the round brackets and I call my macro “{{var next monday}}”. When I exit the Edit Mode for the Block I get nothing, the “{{…}}” is not parsed.

PS: I would extensively use this when writing days of week in my language: [Luni]([[Monday, 01.04.2024]]) but it’s impossible atm. I was wondering if you can see a solution for that by using Macros (it can be done by doing [Luni]( /date) and picking a date from the popup )…

  • No, nested calls are parsed as plain text, thus not called.
  • To achieve what you want, should have the link produced by the macro, by either:
    • performing all string manipulations inside Javascript’s codeblock
    • using Synthesis’ macro evalonce

Does evalonce replace expandmacro?

…ok I have read Tutorial: Initial steps and ended up here:Synthesis lab - Natural-language programming … reading

Super intrigued and holding my breath when reading:

Synthesis is (among other things):

a game-changer, that explodes the possibilities.
especially for simple users
“forget” datalog, clojurescript, javascript, VBA etc.

…and on:Comparison of Synthesis to other programming languages

The programming language that comes closer to Synthesis is pseudocode.

  • not well-defined, but Synthesis can resemble most of its variations


Is that relevant with AI around?

Short answer: AI makes Synthesis even more relevant.
Long answer: [Comparison of Synthesis to modern AI] (topic under construction)

It’s quite late here … can’t wait to learn more about this… especially if there are any practical examples around …

  • expandmacro is a specialized kit
    • has its own Logseq page
    • does a single job
    • its macro accepts a single parameter
    • can be customized only through javascript
  • evalonce is one of Synthesis’ macros
    • supports unlimited complexity
      • accepts nested natural language expressions
      • calls external definitions recursively
    • Synthesis is a general purpose kit
      • it is made up of multiple Logseq pages

I understand that and like the concept very mch. I was simply asking if configuring Synthesis would render expandmacro superseeded and I should use further evalonce instead of it, as it does the same thing and more.

Regarding Synthesis, can we maybe have a new thread under Synthesis - Logseq to gather practical usecases so it’s easier to grasp for people like me (non-programmers)?

  • Synthesis is meant to both simplify and improve plenty of other things, thus superseding them.
    • As long as someone is willing to afford the potential overhead in installing, learning, and using it.
      • In other words, configuring is not enough in itself.
  • Smaller focused threads are preferable to long gathering ones.
    • The dedicated subforum is a good-enough gatherer anyway.
      • Tags could be used to further group related threads.
    • Big meals should be eaten in small bites.
    • Baptism in basic lessons is necessary before diving into practical usecases.

I understand that one can use/call any program Logseq supports to do something and then define a “natural” language so that that specific “something” can be achieved using whatever form of natural language is covered in the corresponding definition patterns …

With existing info I can grasp the usefulness for evalonce (as per my previous question, to render a macro inside another macro - ex: [Luni]( {{macro-name var1 var2 etc }} ) so I guess one would write it something like this: {{evalonce [Luni]({{var this monday}})}}?

I can also see the potential to have a macro to create an empty Xcolumns x Yrows Markdown Table where Cells can be edited without going to block edit mode, in a “live” manner. I have no idea yet how that could possibly be triggered ({{evalonce table, 4x3, zeroed}} ??)… Another idea: importing a CSV into a table ??

I am also curious how can this be used to:

  • make a natural language querying environment, where complex queries are created behind the scenes from the natural language provided to the macro… ;
  • creating multi-block “templates” just like we can now call the (single)block templates;
  • call templates from an evalonce so the daily template would just contain an evalonce call that calls Weekday template if it’s a week day, Weekend template if it’s weekend, etc … (?)

I can’t wrap my head around how the above would work and even if my assumptions are correct so I am eagerly waiting for any discussions/examples/videos that anybody would post so we better understand how this works and what is possible.

  • Everything you mentioned is possible.
    • Some of them already tested and working.
    • Some of them needing some integration (e.g. file system stuff).
  • Actually the current problem is the inverse (i.e. how to prioritize among the endless possibilities).
  • But this is not just “a better macro”:
    • Forget things like {{evalonce [Luni]({{var this monday}})}}
    • Should instead look like {{evalonce link for text "Luni" and url (date for this Monday}}
      • Could still compress it to less and shorter words, but that’s not the point.
        • Could even have an old-fashioned macro make a call to Synthesis.
      • But the true power is in synthesizing smaller functionalities to:
        • automate most of the boring part of your work
        • abstract the generation of whole interfaces

Ideally when I write Luni during writing a text the system would have already made it into a markdown link just as we can do with text expanders :Luni goes straight to link but even without having to think about it.

Other automation I can think of is automatically populating some properties of my blocks. I use a dozen block templates to capture anything from Logs to Info to Video to Book to whatnot and I have a certain number of properties that go with each input data and I would like to define somewhere in Logseq:

  • if the block is of type “Info” and any word inside the Header of the block contains something that resembles a namespace, put that as the namespace property, otherwise look for some page-name inside my graph and, if it resembles that page-name, put that as “related-to::” property. If nothing is found in the header, move to the block body… and so on. Automatically filling some of the properties would drastically cut my time setting up each block. Ideally an AI-type thing that learns from previous such cases and helps with this meta-data “guessing” would be the thing to have;

Each user has his/her own set of automating needs and they will still have to learn to implement them if they are niche…

This is a most excellent project. Thanks for making it available to us.

For my use-case I’ll cross-post from another thread

I took a slightly different approach to this problem by reusing code from the custom macros that replace themselves thread.

My use case

  • I wanted a scheduled task to automatically appear on my journal page each day reminding me to take some vitamins.
  • I use Logseq on both mobile and desktop

So I wrote a macro {{schedule-date-today} }that replaces itsself with SCHEDULED: <todays date> when and only when it is evaluated on a journal page. Otherwise it just behaves as a normal macro.


  • I use :default-templates { :journals } to load a template to the daily journal.
  • I use kits to run custom javascript.


 :default-templates { :journals "daily-journal" }
 :macros {
          :schedule-date-today "<div class='kit' data-kit='expandmacro'>||scheduled today||</div>"

- daily journal template
  template:: daily-journal
  template-including-parent:: false
	- TODO {{icon ef63}} Take vitamins

The :schedule-date-today macro directs kits to run the javascript in the code block contained in the first child block of the page expandmacro.

  logseq.kits.setStatic(function expandmacro(div){
      const blockId = div.closest(".ls-block").getAttribute("blockid");
      const block = logseq.api.get_block(blockId);
      const pageId =;
      const page = logseq.api.get_page(pageId);
      const pageIsJournal = page['journal?'];
      if (!pageIsJournal) { console.log('is not journal'); return; }
      // This date is in the org-mode SCHEDULED format
      const date = new Date();
      const day = date.getDate().toString().padStart(2, '0');
      const month = (date.getMonth() + 1).toString().padStart(2, '0'); 
      const year = date.getFullYear();
      const dayOfWeek = date.toLocaleString('en-US', { weekday: 'short' }); 
      const formattedDate = `${year}-${month}-${day} ${dayOfWeek}`;
      const content = block.content;

      const macroStart = content.indexOf("{{" + div.closest(".macro").dataset.macroName);
      const macroEnd = content.indexOf("}}", macroStart) + 2;

      logseq.api.update_block(blockId, content.slice(0, macroStart) + `SCHEDULED: <${formattedDate}>` + content.slice(macroEnd));
  // {{schedule-date-today}}