# mu4e-dashboard turns an org file into a mail query dashboard

> mu4e-dashboard is a small Emacs Lisp package that adds a mu: link type to org-mode, so a hand written org file can hold clickable mail queries that call mu4e-headers-search, show live counts, expand mu4e bookmarks and carry its own key bindings. It is a presentation layer over mu4e rather than a mail client, and the file you write is the whole configuration.

**rougier/mu4e-dashboard** — A dashboard for mu4e (mu for emacs) 

- Repository: https://github.com/rougier/mu4e-dashboard
- Stars: 521 · Forks: 42
- Language: Emacs Lisp
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/rougier-mu4e-dashboard

## A dashboard is an org file you write yourself

mu4e-dashboard is described as a dashboard for mu4e, and the description is precise about what it does: it provides a new mu4e org link type that allows you to execute various mu4e queries when clicked. The dashboard itself is not a widget or a generated view. You write an org file, you put links in it, and that file is the dashboard.

The README calls the link syntax quite intuitive, and the whole design follows from that. The repository ships two example files, dashboard.org and side-dashboard.org, and two screenshots showing a full page dashboard and a side dashboard, which suggests two placements: a file you open when you start the day, and a narrower one you keep in a side window.

The audience is narrow, and it is the kind of narrow that comes with a good reason. It is for someone who already uses mu4e as their mail client inside Emacs, and who wants the summary view, the counters and the shortcuts to live in org-mode alongside their notes, in a text file they can edit with the same muscle memory as everything else. People who want a graphical mail client are not the target, and people who do not use mu4e at all have no reason to install this.

What the package does not do is worth stating too. It does not read mail, it does not manage your mailboxes, and it does not replace any part of mu4e. It is a link handler and a minor mode, sitting on top of a client that does the actual work.

## The link syntax carries a query, a format and a count

The link type is described with a schematic form.

```org
[[mu:query|format|count][description]]
```

The query has to be a valid mu4e query, and the count is optional and sets the maximum number of results to be returned. When the link is clicked, mu4e-headers-search is called with the query. The description is the text you see on the page, and the format is what turns that description into a live number.

The simplest case is a plain query with a static description.

```org
[[mu:flag:unread][All unread]]
[[mu:flag:unread||10][Last 10 unread]]
```

The second one shows the count field in use, an empty value with the limit of ten, which produces a link that shows the ten most recent unread messages rather than the whole unread set.

The format field is the part that makes the page a dashboard rather than a list of shortcuts. It specifies that the query results are to be counted, and the count is then inserted into the description using the given format.

```org
[[mu:flag:unread|%3d][---]]
```

With that example, when the link is clicked, the dashes are replaced by the number of unread mails. The %3d is a printf style width and format, so the counter is padded to a fixed width, which is what keeps a column of counters from jittering as the numbers change. The click is what triggers the count, which means the page shows the state at the moment you last looked at it rather than a live-updating view.

That last point is a design consequence worth noticing. A dashboard that refreshes on its own is a different program, and this one is explicitly a static org file with links you activate.

## Bookmarks expand inside a query, hygienically

Queries can include references to mu4e bookmarks, which is the feature that makes the dashboard maintainable. A bookmark is denoted with the bm: prefix followed by the bookmark name.

The name cannot contain spaces or a closing bracket, a query can contain several bookmarks, and their expansion is not recursive. Those three constraints are stated in the README, and the third one is the one to remember: a bookmark that refers to another bookmark is not expanded again.

The example in the README starts from a bookmark defined in Emacs Lisp.

```elisp
(add-to-list 'mu4e-bookmarks
     '(:name "Unread"
           :query "flag:unread and not flag:trashed"
           :key ?f)
     t)
```

A dashboard query that uses it looks like this.

```elisp
mu:bm:Unread and date:7d..now
```

And it is expanded before the query reaches mu4e.

```elisp
mu:(flag:unread and not flag:trashed) and date:7d..now
```

The parentheses around the expansion are the detail that matters. The README says they are added to make sure the expansion is hygienic, and that is the point: without them, a bookmark whose query contains an or, or any operator with a precedence different from the surrounding query, would silently change the meaning of the whole expression. Wrapping it in parentheses keeps the bookmark's own grouping intact.

For a user this means the dashboard file can stay short. You define the reusable filters once, in your Emacs config, where they are already defined for mu4e itself, and the org file only refers to them by name.

## Key bindings and a way to try a query before linking it

The dashboard can carry its own key bindings, configured in the org file with a KEYMAP keyword.

```org
#+KEYMAP: key | elisp function
```

A concrete example binds the semicolon key to mu4e-context-switch.

```org
#+KEYMAP: ; | mu4e-context-switch
```

Because the binding is expressed as an Emacs Lisp function name, the dashboard can do anything you could type in the minibuffer, not just the mu4e commands.

There is also a query test facility, and it is the most useful thing in the README for anyone building a dashboard. The provided dashboard.org contains a definition for a mu query that can be called directly inside the org file using org-babel, which the README says is convenient if you want to test a query before creating a link.

```org
#+CALL: query(flag:unread, 10)
#+RESULTS:
```

The usage is simple: put the cursor on the call line and press C-c C-c, and the query results appear under the RESULTS line. The difference from a link is that a link is clicked to open a mu4e search buffer, while a call evaluates the query in place and writes the output into the document, so you can see the shape of the result set while you are still writing the link.

The two features fit together. The keymap is for the finished dashboard, where you want a shortcut for the action you take most, and the call is for the authoring phase, where you are checking that the query returns what you expect before you commit it to a link.

## Enabling the mode, and why editing needs it off

The runtime behaviour is short enough to state precisely. To activate the dashboard you type mu4e-dashboard-mode=, which turns on the mu4e-dashboard-mode minor mode and installs any custom keybindings in the current buffer. To edit the org file, you have to turn the mode off first.

That last requirement is the one design decision in the package with a visible consequence. In a normal org buffer, a key press is a text insertion or an org command. In dashboard mode, the same key press is bound to a function, which is what you want when you are reading the dashboard and clicking through to mail. It is also what stops you from editing the file while the mode is on, so the workflow is: open the file, read it, press the mode to enter the dashboard, and turn it off again when you want to change a link.

Installation is the least documented part, and the README is honest about it. You evaluate the buffer and you are good to go, and the README says it is your responsibility to write an org file with links pointing to various queries on your mailboxes. In other words, there is no command that generates a dashboard for you, and no sample of your own mailboxes. The two example files in the repository, dashboard.org and side-dashboard.org, are the starting point, and they are the same files the screenshots show.

That is a coherent choice for a package whose premise is that the dashboard is a document. The cost is that there is no quick start beyond copying an example and editing it.

## The Makefile reveals how it is checked

The repository is small, with mu4e-dashboard.el, a test directory, a Makefile, README.org and the example org files, and the Makefile is where the engineering practice shows.

There is a checkdoc target that runs the Emacs documentation checker over the elisp file in batch mode, and the default target builds the byte compiled file after the lints pass.

```make
check: all
	srcdir=$(PWD) $(EMACS) -batch -Q $(LOADPATH) -L $(MU4E) -l test/regression-tests.el -f ert-run-tests-batch
```

The check target loads the regression tests and runs them with ert in batch mode, which means the suite runs without a display and without an interactive Emacs. For an Emacs Lisp package that is the right level of rigour, and the presence of test/regression-tests.el in the tree says the package has a test suite rather than being an untested snippet.

The load path handling is the other interesting part. The Makefile looks for the ELPA directory and the Straight directory under the XDG config home, falls back to the older locations under the home directory, and adds the async packages to the load path when it finds them, since mu4e depends on async to talk to the mail server. All of that is done so that a contributor on any of the common package setups can run make check without configuring anything.

One portability wart is worth flagging, since it will bite the first person who runs make on a machine that is not the author's. The mu4e load path is hard coded to a Homebrew installation.

```make
MU4E = /opt/homebrew/share/emacs/site-lisp/mu/mu4e
```

On any other system you have to override that variable on the command line, which is a small thing but the kind of detail that turns a first build into an afternoon.

## Where a dashboard layer stops being the answer

The limitation is the same as the feature, seen from the other side. mu4e-dashboard gives you a static page of links that you write by hand, and it expects you to already know the mu4e query language, because a query that is not valid is a query that does not work. The package contributes no query builder, no saved search list, no per mail folder defaults.

The second limitation is that the counters are only as fresh as your last click. A page that says there are three unread messages because you clicked that link five minutes ago is a useful glance, and it is not a status display. For a mail client, freshness is usually the point, and mu4e has its own ways of showing it, which this package sits beside rather than replaces.

The third is the coupling to org-mode and to mu4e. You need Emacs, you need org-mode, and you need mu4e with its Emacs Lisp interface. The package is a good fit if all three are already there, and a poor fit if the reason you want a dashboard is that your current mail setup is uncomfortable.

The README does point at a genuine alternative, which is instructive because it shows the design is not tied to mu4e. There is a fork of this software designed to be used with notmuch instead of with mu4e, and it replaces the standard notmuch-hello dashboard with an org-mode version. The difference in approach is instructive: the same idea, the same link mechanism, aimed at a different mail backend, where the starting point is a welcome screen you already had rather than a query set you have to invent. If you are on notmuch, that fork is the version of this idea that already has a dashboard to replace. If you are on mu4e, then the honest question is whether you want to write a dashboard at all, or whether mu4e's own context and summary screens are the thing you were actually looking for.

## Conclusion

mu4e-dashboard fits an existing mu4e user who wants a single static page of mail queries, countdowns and counts that is versioned in git like the rest of their org files, and it does not fit someone who wants a graphical mail client or who has not decided what their queries are yet. Before you adopt it, write two or three links by hand and check that the count format and the bookmark expansion behave the way you expect, because the query syntax is mu4e syntax and the dashboard inherits it wholesale. The last push was on 2026-07-20, the project publishes no GitHub releases, and the code is under GPL-3.0.

## FAQ

### How do I install mu4e-dashboard?

The README says to evaluate the buffer and you are good to go, and that writing an org file with links pointing to your mail queries is your responsibility. The repository ships example files, dashboard.org and side-dashboard.org, which are the quickest starting point.

### What is the mu link syntax in org mode?

The link takes the form [[mu:query|format|count][description]], where the query must be a valid mu4e query and the count is an optional limit on the number of results. Clicking the link calls mu4e-headers-search with that query, and the format field makes the description show the number of matching results.

### How do I show the number of unread messages in an org link?

Give the link a format, for example [[mu:flag:unread|%3d][---]]. When the link is clicked, the query results are counted and the dashes are replaced by the number of unread mails, with the printf style width keeping the counter the same width.

### Can a mu4e-dashboard link use mu4e bookmarks?

Yes. A bookmark is written as bm:<bookmarkName> and can be combined with other query terms. The expansion is wrapped in parentheses so the bookmark's own grouping stays intact, and the README notes that the expansion is not recursive.

### How do I run the tests for mu4e-dashboard?

The Makefile has a check target that runs the regression tests in test/regression-tests.el with ert in batch mode, and a lints target that runs the Emacs checkdoc checker over the elisp file. The mu4e load path in the Makefile points at a Homebrew installation, so on other systems you need to override that variable.

## Sources

- [Issues](https://github.com/rougier/mu4e-dashboard/issues)
- [License: GPL-3.0](https://github.com/rougier/mu4e-dashboard/blob/main/LICENSE)
- [README](https://github.com/rougier/mu4e-dashboard/blob/main/README.md)
- [rougier/mu4e-dashboard on GitHub](https://github.com/rougier/mu4e-dashboard)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rougier-mu4e-dashboard
