Dynamic blocks
Table of Contents
- 1. How to use this file
- 2. The sample data
- 3. How dynamic blocks work
- 4. Clock tables
- 5. Column view blocks
- 6. Your own dynamic blocks
- 7. Further reading
1. How to use this file
A dynamic block is a region of the file that org.nvim writes for you. It
starts with a #+BEGIN: NAME PARAMETERS line and ends with #+END:.
Everything between the two lines is generated: updating the block throws
the old content away and writes a fresh one, computed from the rest of the
file. Two kinds are built in, clocktable (time you clocked) and
columnview (a table of properties), and you can write your own in Lua.
- The file starts folded.
<Tab>on a heading opens it,<S-Tab>cycles the whole buffer. - Nothing breaks if you make a mess:
uundoes, andgit checkout examples/18-dynamic-blocks.orgrestores the file. <prefix>means<leader>o(the defaultmappings.prefix).g?lists every key of the buffer.- Lines starting with Try: are exercises, Expect: says what you should see afterwards. Lines starting with =# = are Org comments that annotate the examples.
- The generated tables below were written by org.nvim itself. Updating a block again gives the same table, except for the "Clock summary at" time stamp, which is the time of the update.
Start Neovim from the repo root with the bundled init file, so your own config is not involved:
nvim -u examples/minimal_init.lua examples/18-dynamic-blocks.org
1.1. Keys in this file
| Key | Emacs | What it does |
|---|---|---|
<C-c><C-c> on #+BEGIN: |
C-c C-c |
update this block |
<prefix>xu |
C-c C-x C-u |
update the block around the cursor |
<prefix>xU |
update every block of the file | |
<prefix>xr |
C-c C-x x |
insert / update a clock table |
:Org insert_columnview |
C-c C-x i |
insert a column view block |
:Org insert_dblock |
C-c C-x x |
insert a block, choosing its type |
<S-Left> / <S-Right> |
S-left S-right |
on a clocktable #+BEGIN: line: |
shift its :block period |
||
<prefix>C |
C-c C-x C-c |
the interactive column view |
2. The sample data
The clock tables and column views below read this data. It is a small
project with clocked time in the week of Mon 2026-09-21, plus some admin
work. Each task has an Effort estimate and an OWNER.
2.1. Website relaunch  project
2.1.1. DONE Design mockups  design
2.1.3. TODO Launch checklist  ops
2.2. Admin  admin
2.2.1. Weekly review
2.2.2. Email
3. How dynamic blocks work
The general form is:
#+BEGIN: NAME :param1 value1 :param2 "a string" :param3 (a list) ...generated content... #+END:
NAMEpicks the writer:clocktable,columnview, or one you register yourself (see "Your own dynamic blocks").- Parameters are
:key valuepairs. A value is a word or number, a"quoted string"(for values with spaces), a(list in parentheses),t(true) ornil(false). A timestamp like<2026-09-21 Mon>may contain spaces. - Updating replaces everything between the two lines. Don't edit the
content by hand: your edits are lost on the next update. (A
#+TBLFM:line under a table is the exception, see "Formulas that survive".)
Ways to update:
<C-c><C-c>on the#+BEGIN:line updates that block.<prefix>xuanywhere inside a block (EmacsC-c C-x C-u).<prefix>xUupdates every dynamic block of the file, from the last one to the first, and reports "Updated N dynamic block(s)".
This block is intentionally out of date: its content was typed by hand.
| This text is stale | and will be replaced |
Try: put the cursor on the #+BEGIN: line above and press <C-c><C-c>.
Expect: the stale line is replaced by a table: a #+CAPTION: line
("Clock summary at" and the current time), a header row
| Headline | Time |, the row | *Total time* | *9:25* |, and one row
per level-1 heading with clocked time; here only
| The sample data | 9:25 | (all the clocks of this file are below that
heading). Press u to see the old text again, and <C-r> to redo.
Try: press <prefix>xU.
Expect: the message "Updated 21 dynamic block(s)": every
clocktable and columnview block of the file is rewritten, and only the
"Clock summary at" times change. The blocks of "Your own dynamic blocks"
fail until you run the Lua blocks there: three warnings "No writer for
dynamic block: …" and one error about :formatter effort_list.
4. Clock tables
A clocktable block sums the CLOCK: lines of the file (or of other
files) per heading. Clocking itself (clocking in and out, the logbook,
effort) is covered in 08-clocking.org; this section is about the
table.
<prefix>xr (Emacs: C-c C-x x then clocktable) inserts a new clock table below the
cursor line (:scope subtree :maxlevel 2, or :scope file above the
first heading), or updates the one the cursor is in. With a count
(1<prefix>xr) it first jumps to the first clock table of the file.
Try: put the cursor on the "Admin" heading in "The sample data" and
press <prefix>xr.
Expect: right under the heading line, a new block
#+BEGIN: clocktable :scope subtree :maxlevel 2 with *Total time*
*1:45* and one row \_ Admin 1:45. The children (level 3) are not
listed: :maxlevel counts the stars of the headings, not the depth
below the block. Press u to remove it again.
4.1. The basic table
:scope file reads the whole file, :maxlevel 3 shows headings down to
level 3 (deeper levels are added to their parent).
| Headline | Time | ||
|---|---|---|---|
| Total time | 9:25 | ||
| The sample data | 9:25 | ||
| Website relaunch | 7:40 | ||
| Design mockups | 3:45 | ||
| Build the pages | 3:25 | ||
| Launch checklist | 0:30 | ||
| Admin | 1:45 | ||
| Weekly review | 1:15 | ||
| 0:30 |
Try: change :maxlevel 3 to :maxlevel 2 and press <C-c><C-c> on the
#+BEGIN: line.
Expect: the level-3 rows (Design mockups, Build the pages,
Launch checklist, Weekly review, Email) disappear; Website
relaunch still shows 7:40 and Admin 1:45: the time of the hidden
children is still counted in their parents.
4.2. Limiting the period: :block, :tstart, :tend
:block keeps only clocked time inside a period: today, yesterday,
thisweek, lastweek, thismonth, lastmonth, thisyear, a fixed day
2026-09-23, a week 2026-W39, a month 2026-09, a quarter 2026-Q3 or
a year 2026. The this... forms and today take an offset:
thisweek-1 is last week.
| Headline | Time | ||
|---|---|---|---|
| Total time | 8:40 | ||
| The sample data | 8:40 | ||
| Website relaunch | 7:40 | ||
| Design mockups | 3:45 | ||
| Build the pages | 3:25 | ||
| Launch checklist | 0:30 | ||
| Admin | 1:00 | ||
| Weekly review | 0:30 | ||
| 0:30 |
| Headline | Time | ||
|---|---|---|---|
| Total time | 2:45 | ||
| The sample data | 2:45 | ||
| Website relaunch | 2:45 | ||
| Build the pages | 2:45 |
:tstart and :tend give any range (:tend is exclusive). They also
accept <today>, <now>, <-2d> (two days ago), <+1w>, <-3h>.
| Headline | Time | ||
|---|---|---|---|
| Total time | 4:15 | ||
| The sample data | 4:15 | ||
| Website relaunch | 3:45 | ||
| Design mockups | 3:45 | ||
| Admin | 0:30 | ||
| 0:30 |
Try: put the cursor on the #+BEGIN: line of the 2026-09-23 table and
press <S-Right>.
Expect: the line now reads :block 2026-09-24 and the table is
updated: *Total time* *0:40*, all in Build the pages (the
0:40 of "Contact form", a level-4 entry). <S-Left> twice goes back to
2026-09-22 (Design mockups 1:15). On :block thisweek the keys give
thisweek+1 / thisweek-1.
4.3. One table per day: :step
:step day (or week, semimonth, month, quarter, year) splits
the :block or :tstart / :tend range into periods and writes one table
per period. :stepskip0 t leaves out the periods without time.
Daily report:
| Headline | Time | |
|---|---|---|
| Total time | 3:00 | |
| The sample data | 3:00 | |
| Website relaunch | 2:30 | |
| Admin | 0:30 |
Daily report:
| Headline | Time | |
|---|---|---|
| Total time | 1:15 | |
| The sample data | 1:15 | |
| Website relaunch | 1:15 |
Daily report:
| Headline | Time | |
|---|---|---|
| Total time | 2:45 | |
| The sample data | 2:45 | |
| Website relaunch | 2:45 |
Daily report:
| Headline | Time | |
|---|---|---|
| Total time | 0:40 | |
| The sample data | 0:40 | |
| Website relaunch | 0:40 |
Daily report:
| Headline | Time | |
|---|---|---|
| Total time | 1:00 | |
| The sample data | 1:00 | |
| Website relaunch | 0:30 | |
| Admin | 0:30 |
Try: remove :stepskip0 t and update the block.
Expect: seven daily tables, Monday to Sunday; the tables of Saturday
2026-09-26 and Sunday 2026-09-27 only have the row
| *Total time* | *0:00* |.
4.4. Choosing the entries: :match and :scope
:match takes a tags/property match (the same syntax as the agenda's
match views, :h org-match-syntax).
| Headline | Time | ||
|---|---|---|---|
| Total time | 4:15 | ||
| The sample data | 4:15 | ||
| Website relaunch | 4:15 | ||
| Design mockups | 3:45 | ||
| Launch checklist | 0:30 |
| OWNER | Headline | Time | |||
|---|---|---|---|---|---|
| Total time | 3:25 | ||||
| The sample data | 3:25 | ||||
| Website relaunch | 3:25 | ||||
| Build the pages | 3:25 | ||||
| Ben | Home page | 2:45 | |||
| Ben | Contact form | 0:40 |
:scope says which entries are read:
:scope |
reads |
|---|---|
file |
this whole file |
subtree |
the subtree the block is in |
tree / treeN |
the level-1 (level-N) tree the block is in |
agenda |
every agenda file (a File column is added) |
agenda-with-archives |
the same, plus their archive files |
file-with-archives |
this file and its archive |
("a.org" "b.org") |
the listed files |
Try: on the #+BEGIN: line of "The basic table" change :scope file to
:scope agenda and update it.
Expect: with examples/minimal_init.lua the table covers every
examples/*.org file. A File column comes first; the total row reads
ALL *Total time*, and every file gets a *File time* row (*0:00* for
files without clocks) followed by its headings. The
18-dynamic-blocks.org part shows the same 9:25 as before. Change it
back to :scope file.
4.5. Extra columns
| Parameter | Adds |
|---|---|
:tags t |
a Tags column |
:level t |
a level column (the number of stars) |
:timestamp t |
the first timestamp of the entry |
:properties ("P" …) |
one column per property (:inherit-props t) |
:formula % |
the percentage of the total time |
:link t |
headings as links to the entries |
:emphasize t |
level 1 in bold, level 2 in italic |
| Tags | Effort | Headline | Time | % | ||
|---|---|---|---|---|---|---|
| Total time | 9:25 | 100.0 | ||||
| The sample data | 9:25 | 100.0 | ||||
| project | Website relaunch | 7:40 | 81.4 | |||
| project, design | 4:00 | Design mockups | 3:45 | 39.8 | ||
| project, dev | Build the pages | 3:25 | 36.3 | |||
| project, ops | 1:00 | Launch checklist | 0:30 | 5.3 | ||
| admin | Admin | 1:45 | 18.6 | |||
| admin | Weekly review | 1:15 | 13.3 | |||
| admin | 0:30 | 5.3 |
Try: add :link t to the block above and update it.
Expect: every heading in the table becomes a link to the entry: a
file: link with the full path of this file and ::*Design mockups
(like Emacs, which also writes the absolute file name). The link shows as
its description, Design mockups; <C-c><C-c> on it jumps to the
entry.
4.6. Layout
| Parameter | Effect |
|---|---|
:compact t |
one time column, headings indented (\_) |
:indent nil |
no \_ indentation of sub-headings |
:tcolumns N |
at most N time columns (one per level by default) |
:narrow 20! |
cut headings to 20 characters |
:hidefiles t |
no File column with :scope agenda |
:fileskip0 t |
skip files without clocked time |
:sort (2 . ?T) |
sort the rows by column 2, time, descending (?t ascending) |
:lang de |
headers in German (also es, fr, nl, nn, pl, pt-BR, sk) |
:header "text" |
replace the #+CAPTION: line with your own text |
| Headline | Time |
|---|---|
| Total time | 9:25 |
| The sample data | 9:25 |
| Website relaunch | 7:40 |
| Design mockups | 3:45 |
| Build the pages | 3:25 |
| Launch checklist | 0:30 |
| Admin | 1:45 |
| Weekly review | 1:15 |
| 0:30 |
| Kopfzeile | Dauer | ||
|---|---|---|---|
| Gesamtdauer | 9:25 | ||
| The… | 9:25 | ||
| Website… | 7:40 | ||
| Design… | 3:45 | ||
| Build the… | 3:25 | ||
| Launch… | 0:30 | ||
| Admin | 1:45 | ||
| Weekly… | 1:15 | ||
| 0:30 |
4.7. Formulas that survive
:formula "..." adds a #+TBLFM: line to the table. A #+TBLFM: line you
write yourself right under the table is kept on every update and
re-applied. Here :tcolumns 1 puts all times in one column and a formula
converts them into hours as a decimal number (;t reads H:MM as
hours):
| Headline | Time | |
|---|---|---|
| Total time | 9:25 | 9.42 |
| The sample data | 9:25 | 9.42 |
| Website relaunch | 7:40 | 7.67 |
| Admin | 1:45 | 1.75 |
Try: update the block above twice.
Expect: the #+TBLFM: $3=$2;t line is still there after each update,
and the third column shows the hours as decimals: 9.42 for 9:25,
7.67 for 7:40, 1.75 for 1:45.
5. Column view blocks
A columnview block writes the column view of a subtree (or of the whole
file) as a table: one row per heading, one column per property of the
COLUMNS format. The format of "Website relaunch" is:
%ITEM %TODO %Effort{:} %OWNER %CLOCKSUM
%Effort{:} sums the efforts of the children as times, %CLOCKSUM is the
clocked time of the entry and its children. The interactive column view
(<prefix>C, see 06-properties-columns.org) shows the same data; a
columnview block freezes it into the file, where it can be exported.
:Org insert_columnview (Emacs C-c C-x i) inserts a new block and asks
for the scope: local, global or the ID of an entry.
5.1. Choosing the subtree: :id
:id |
Captures |
|---|---|
local |
the subtree the block is in (the default) |
global |
the whole file |
file:path.org |
the whole of another file |
ID |
the entry with that ID or CUSTOM_ID (any agenda file) |
| ITEM | TODO | Effort | OWNER | CLOCKSUM |
|---|---|---|---|---|
| Website relaunch | 14:00 | 7:40 | ||
| Design mockups | DONE | 4:00 | Ana | 3:45 |
| Build the pages | NEXT | 9:00 | 3:25 | |
| Home page | DONE | 3:00 | Ben | 2:45 |
| Contact form | TODO | 2:00 | Ben | 0:40 |
| Blog | TODO | 4:00 | Cai | |
| Launch checklist | TODO | 1:00 | Ana | 0:30 |
Try: change the Effort of "Blog" (in "The sample data") from 4:00 to
2:00 with <prefix>xe or by editing the drawer, then update the block
above.
Expect: the Blog row shows 2:00, Build the pages goes from 9:00
to 7:00 and Website relaunch from 14:00 to 12:00.
5.2. The current subtree: :id local
With :id local (or no :id at all) the block captures the subtree it is
written in: this heading and its children. The format comes from this
heading's own COLUMNS property. %Pages{+} sums the pages,
%Rating{mean} averages the ratings.
| ITEM | Pages | Rating | Read |
|---|---|---|---|
| The current subtree: :id local | 1165 | 4.5 | |
| Dune | 412 | 5 | yes |
| Neuromancer | 271 | 4 | yes |
| Hyperion | 482 | no |
| ITEM | Pages | Rating | Read |
|---|---|---|---|
| The current subtree: :id local | 1165 | 4.5 | |
| Dune | 412 | 5 | yes |
| Neuromancer | 271 | 4 | yes |
| Hyperion | 482 | no |
5.2.1. Dune
5.2.2. Neuromancer
5.2.3. Hyperion
Try: give "Hyperion" a rating: add the line :Rating: 3 to its
drawer (or press <prefix>p on the heading, type the property Rating and
the value 3),
then update both blocks.
Expect: the Hyperion row shows 3, and the top row's Rating mean
becomes 4 ((5 + 4 + 3) / 3) instead of 4.5.
5.3. Depth and hlines: :maxlevel, :hlines
:maxlevel N stops at level N. :hlines t puts a separator before every
entry; :hlines N only before entries of level N or less.
| ITEM | TODO | Effort | OWNER | CLOCKSUM |
|---|---|---|---|---|
| Website relaunch | 14:00 | 7:40 | ||
| Design mockups | DONE | 4:00 | Ana | 3:45 |
| Build the pages | NEXT | 9:00 | 3:25 | |
| Launch checklist | TODO | 1:00 | Ana | 0:30 |
5.4. Leaving rows out: :skip-empty-rows, :match, :exclude-tags
:skip-empty-rows tdrops rows where every column but ITEM is empty.:match "..."keeps the entries that match a tags/property match.:exclude-tags (tag ...)drops entries with one of these tags (inherited tags count).
| ITEM | OWNER | Effort |
|---|---|---|
| Design mockups | Ana | 4:00 |
| Home page | Ben | 3:00 |
| Contact form | Ben | 2:00 |
| Blog | Cai | 4:00 |
| Launch checklist | Ana | 1:00 |
| ITEM | TODO | Effort | OWNER | CLOCKSUM |
|---|---|---|---|---|
| Home page | DONE | 3:00 | Ben | 2:45 |
| Contact form | TODO | 2:00 | Ben | 0:40 |
| ITEM | TODO | Effort | OWNER | CLOCKSUM |
|---|---|---|---|---|
| Website relaunch | 14:00 | 7:40 | ||
| Design mockups | DONE | 4:00 | Ana | 3:45 |
| Build the pages | NEXT | 9:00 | 3:25 | |
| Home page | DONE | 3:00 | Ben | 2:45 |
| Blog | TODO | 4:00 | Cai | |
| Launch checklist | TODO | 1:00 | Ana | 0:30 |
5.5. Indentation and links: :indent, :link
:indent t indents the ITEM column by level (\_ like the clock table).
:link t turns each item into a link to its heading.
| ITEM | TODO | Effort |
|---|---|---|
| Website relaunch | 14:00 | |
| Design mockups | DONE | 4:00 |
| Build the pages | NEXT | 9:00 |
| Home page | DONE | 3:00 |
| Contact form | TODO | 2:00 |
| Blog | TODO | 4:00 |
| Launch checklist | TODO | 1:00 |
Try: add :link t to the block above and update it. Then press
<C-c><C-c> on the Contact form link.
Expect: the items become file: links (the full path of this file,
then ::*Contact form), shown as their titles; following one jumps to
that heading.
5.6. Your own format and formulas: :format, #+TBLFM
:format overrides the COLUMNS format. Keywords above the table (like
#+NAME:) and a #+TBLFM: line under it are kept when the block is
updated, and the formula is applied again, so a column view can feed a
computed column. Here the format has a fourth column, Left, for a
property no entry has (so it starts empty), and the formula fills it with
the effort minus the clocked time (;U writes the result as HH:MM):
| ITEM | Effort | CLOCKSUM | Left |
|---|---|---|---|
| Website relaunch | 14:00 | 7:40 | 06:20 |
| Design mockups | 4:00 | 3:45 | 00:15 |
| Build the pages | 9:00 | 3:25 | 05:35 |
| Home page | 3:00 | 2:45 | 00:15 |
| Contact form | 2:00 | 0:40 | 01:20 |
| Blog | 4:00 | 04:00 | |
| Launch checklist | 1:00 | 0:30 | 00:30 |
Try: change the Effort of "Contact form" (in "The sample data") from
2:00 to 3:00 and update the block above.
Expect: the #+NAME: and #+TBLFM: lines are still there, and the
Contact form row shows 3:00, 0:40 and 02:20 left; Build the
pages goes to 10:00 and 06:35, Website relaunch to 15:00 and
07:20.
6. Your own dynamic blocks
A writer is a Lua function registered under a name. It receives the
parameters of the #+BEGIN: line (a table: :name "Ada" gives
params.name = "Ada", =t gives true, numbers are numbers) and a
context ctx (ctx.bufnr, ctx.start_line, ctx.end_line,
ctx.content with the current lines). It returns the new content as a
list of lines.
require("org.dblock").register("myblock", function(params, ctx)
return { "| generated | table |" }
end)
Put the registration in your config (after require("org").setup()) to
have the block everywhere. Here you can register writers for this session
by running Lua source blocks (see 17-babel.org): the code runs inside
Neovim.
6.1. A greeting
require("org.dblock").register("greeting", function(params)
local who = params.name or "world"
local times = params.times or 1
local lines = {}
for i = 1, times do
lines[#lines + 1] = string.format("%d. Hello, %s!", i, who)
end
return lines
end)
Try: first update the greeting block: <C-c><C-c> on its #+BEGIN:
line.
Expect: a warning "No writer for dynamic block: greeting": nothing is registered yet.
Try: now put the cursor in the Lua block above, press <C-c><C-c> (and
answer y), then update the greeting block again.
Expect: three lines: 1. Hello, Ada!, 2. Hello, Ada! and
3. Hello, Ada!. Change :times 3 to :times 1 and :name "Ada" to
:name "Linus", update again: one line, 1. Hello, Linus!.
6.2. A table of contents
Writers can read the buffer. This one lists the headings of the file up
to :maxlevel as links, like a table of contents.
require("org.dblock").register("toc", function(params, ctx)
local max = params.maxlevel or 1
local out = {}
for _, hl in ipairs(require("org.files").get_buffer(ctx.bufnr).headlines) do
if hl.level <= max then
local title = hl:plain_title()
out[#out + 1] = string.rep(" ", hl.level - 1)
.. "- [[*" .. title .. "][" .. title .. "]]"
end
end
return out
end)
Try: run the Lua block above, then update the toc block. Then change
:maxlevel 1 to :maxlevel 2 and update it again.
Expect: first a list with one link per top-level heading of this file
(How to use this file, The sample data, …). With :maxlevel 2 the
second-level headings appear as indented sub-items.
6.3. A TODO summary
The writer can return any Org text, for example a table:
require("org.dblock").register("todo-summary", function(params, ctx)
local counts = {}
for _, hl in ipairs(require("org.files").get_buffer(ctx.bufnr).headlines) do
if hl.todo then
counts[hl.todo] = (counts[hl.todo] or 0) + 1
end
end
local lines = { "| State | Count |", "|-------+-------|" }
for _, state in ipairs({ "TODO", "NEXT", "DONE" }) do
lines[#lines + 1] = string.format("| %s | %d |", state, counts[state] or 0)
end
return lines
end)
Try: run the Lua block, update the todo-summary block, then mark
"Contact form" (in "The sample data") DONE: on its heading press
<prefix>S then d. Come back and update the block again.
Expect: first TODO 3, NEXT 1, DONE 2 (the table is not aligned: the
writer did not pad it; press <C-c><C-c> inside it to align). After the
change: TODO 2, DONE 3.
6.4. A column view in your own layout: :formatter
A columnview block can hand its rows to your own Lua function instead of
writing a table: :formatter NAME names a global Lua function
NAME(rows, params) that returns the lines. rows[1] holds the column
titles, rows[2] is the string "hline", and each following row is a
table with the cells (row[1], row[2], …) and row.level (the
heading level). The option columns_dblock_formatter sets a formatter for
every columnview block.
function _G.effort_list(rows, params)
local out = {}
for i = 3, #rows do
local r = rows[i]
local line = string.rep(" ", r.level - 2) .. "- " .. r[1] .. ": "
.. (r[2] ~= "" and r[2] or "no estimate")
if r[3] ~= "" then
line = line .. " (" .. r[3] .. ")"
end
out[#out + 1] = line
end
return out
end
Try: run the Lua block above, then update the columnview block below
it.
Expect: a nested list instead of a table:
- Website relaunch: 14:00, then indented items such as
- Design mockups: 4:00 (Ana) and, one level deeper,
- Home page: 3:00 (Ben). Before the Lua block has run, updating gives
an error that ends in "unknown :formatter effort_list".
6.5. Inserting a block of any type
:Org insert_dblock (Emacs C-c C-x x) asks for a block type among the
registered writers (clocktable, columnview and yours), inserts
#+BEGIN: NAME / #+END: below the cursor line and fills it.
Try: on the empty line below, run :Org insert_dblock and choose
greeting (after registering it above).
Expect: a #+BEGIN: greeting block with the content 1. Hello, world!
(no parameters: the defaults of the writer).
7. Further reading
:h org-dblocks(both built-in writers and all their parameters):h org-clockand 08-clocking.org (clocking time):h org-columnsand 06-properties-columns.org (column view):h org-match-syntax(the:matchparameter)- 17-babel.org (the Lua blocks used to register writers)