p1tt151 downloadsTrack time on tasks with org-mode LOGBOOK-style clock entries, stored as Dataview-queryable inline fields.
Track time on tasks with org-mode LOGBOOK-style clock entries, stored as Dataview-queryable inline fields.
100% local — no network requests, no telemetry, no external services. Logbook only writes plain Markdown to your vault; nothing ever leaves your machine.
Settings → Community plugins → Browse → search "Logbook" → Install → Enable.
To test the latest pre-release build before it lands in the community browser:
obsidian42-brat) from the community browser.Cmd/Ctrl+P → BRAT: Add a beta plugin for testing.https://github.com/p1tt1/obsidian-logbook into the modal.main.js, manifest.json, and styles.css from the latest release.<vault>/.obsidian/plugins/logbook/.Cursor on a task line → run Logbook: Clock in to task from the command palette.
Run Logbook: Clock out → the open entry gains an end timestamp.
| Command | What it does |
|---|---|
Logbook: Clock in to task |
Inserts an open clock child below the current task. Hidden when cursor isn't on a task line. |
Logbook: Clock out |
Closes the active task's open entry. Hidden when no clock is active. |
Logbook: Clock report: current file |
Modal showing closed clock entries from the active file. |
Logbook: Find all open clocks |
Vault-wide scanner — surfaces any forgotten open entries. |
All queries below assume the Dataview plugin is installed with DataviewJS enabled. The Logbook plugin writes plain Markdown either way — Dataview is optional and only required if you want to run these queries.
How Dataview sees clock entries: Dataview indexes [clock:: START--END] as a plain string — the -- separator is not a recognized Dataview construct, so child.clock is text, not a pair of DateTime objects. All examples below split this string manually. If you prefer native DateTime arithmetic without splitting (e.g. item.end - item.start), see the two-field alternative at the end of this page.
Clock entries are accessible via FLATTEN file.lists — Dataview indexes every list item, including the clock children. Use split() and date() to extract timestamps from the string value.
All closed clock entries, vault-wide:
TABLE WITHOUT ID
file.link AS File,
date(split(item.clock, "--")[0]) AS Started,
date(split(item.clock, "--")[1]) AS Ended,
date(split(item.clock, "--")[1]) - date(split(item.clock, "--")[0]) AS Duration
FLATTEN file.lists AS item
WHERE item.clock AND !endswith(item.clock, "--")
SORT date(split(item.clock, "--")[0]) DESC
Limitation: DQL cannot easily surface the parent task name alongside each clock entry. For task-grouped reports, use the DataviewJS examples below.
Paste each DataviewJS block into a note inside a dataviewjs code fence.
Sums all closed clock entries for each task in the current file and outputs a table with task name and total time logged. Open (running) entries are skipped — only completed intervals are counted.
// Total time logged per task in the current file — closed entries only.
function parseClockValue(raw) {
const [startStr, endStr] = raw.split("--");
const start = dv.date(startStr.trim());
const end = endStr && endStr.trim() !== "" ? dv.date(endStr.trim()) : null;
return { start, end };
}
const rows = [];
for (const task of dv.current().file.tasks) {
let taskMinutes = 0;
for (const child of (task.children ?? [])) {
if (!child.clock) continue;
const { start, end } = parseClockValue(String(child.clock));
if (!end) continue; // skip open (running) entries
if (!start) continue;
const mins = Math.floor(end.diff(start, "minutes").minutes);
taskMinutes += mins;
}
if (taskMinutes > 0) {
const h = Math.floor(taskMinutes / 60);
const m = taskMinutes % 60;
rows.push([task.text, `${h}:${String(m).padStart(2, "0")}`]);
}
}
dv.table(["Task", "Total"], rows);
Sample output: a two-column table listing each task from the current file alongside its cumulative time, e.g. Fix login bug | 2:30.
Scans all files in the vault for clock entries from today and groups results by file and task. Useful as a daily standup summary — see everything you worked on today across all your notes.
// Today's clocked entries across the entire vault — grouped by file and task.
const today = dv.date("today");
function parseClockValue(raw) {
const [startStr, endStr] = raw.split("--");
const start = dv.date(startStr.trim());
const end = endStr && endStr.trim() !== "" ? dv.date(endStr.trim()) : null;
return { start, end };
}
const rows = [];
for (const page of dv.pages()) {
for (const task of page.file.tasks) {
for (const child of (task.children ?? [])) {
if (!child.clock) continue;
const { start, end } = parseClockValue(String(child.clock));
if (!start || !end) continue; // skip open entries
if (start < today || start >= today.plus({ days: 1 })) continue;
const mins = Math.floor(end.diff(start, "minutes").minutes);
const h = Math.floor(mins / 60);
const m = mins % 60;
rows.push([
task.text,
page.file.link,
start.toFormat("HH:mm"),
end.toFormat("HH:mm"),
`${h}:${String(m).padStart(2, "0")}`,
]);
}
}
}
dv.table(["Task", "File", "Start", "End", "Duration"], rows);
Sample output: a table of every completed session from today across all vault files, with start time, end time, and duration per row.
Groups all closed clock entries by ISO week number and totals the time per week. Useful for reviewing your productivity trend over several weeks.
// Total time per ISO week — all closed entries across the vault.
function parseClockValue(raw) {
const [startStr, endStr] = raw.split("--");
const start = dv.date(startStr.trim());
const end = endStr && endStr.trim() !== "" ? dv.date(endStr.trim()) : null;
return { start, end };
}
// Map: ISO week label (e.g. "2026-W25") → total minutes
const weekTotals = new Map();
for (const page of dv.pages()) {
for (const task of page.file.tasks) {
for (const child of (task.children ?? [])) {
if (!child.clock) continue;
const { start, end } = parseClockValue(String(child.clock));
if (!start || !end) continue; // skip open entries
const weekLabel = start.toFormat("kkkk-'W'WW");
const mins = Math.floor(end.diff(start, "minutes").minutes);
weekTotals.set(weekLabel, (weekTotals.get(weekLabel) ?? 0) + mins);
}
}
}
const rows = [...weekTotals.entries()]
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([week, mins]) => {
const h = Math.floor(mins / 60);
const m = mins % 60;
return [week, `${h}:${String(m).padStart(2, "0")}`];
});
dv.table(["Week", "Total"], rows);
Sample output: a two-column table with one row per ISO week (e.g. 2026-W25 | 14:20), sorted chronologically.
Scans the entire vault for any open [clock:: START--] entries — tasks where you clocked in but have not yet clocked out. Useful as a status widget or for recovering forgotten sessions.
// Find any task with an open [clock:: X--] entry across the entire vault.
// Useful for spotting forgotten clocks or verifying plugin state.
const rows = [];
for (const page of dv.pages()) {
for (const task of page.file.tasks) {
for (const child of (task.children ?? [])) {
if (!child.clock) continue;
const raw = String(child.clock);
// An open entry ends with "--" and has no end datetime
if (!raw.endsWith("--")) continue;
const startStr = raw.replace(/--$/, "").trim();
const start = dv.date(startStr);
if (!start) continue;
const { DateTime } = dv.luxon;
const now = DateTime.now();
const mins = Math.floor(now.diff(start, "minutes").minutes);
const elapsed = `${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, "0")}`;
rows.push([task.text, page.file.link, startStr, elapsed]);
}
}
}
if (rows.length === 0) {
dv.paragraph("No running clocks found.");
} else {
dv.table(["Task", "File", "Started", "Elapsed"], rows);
}
Sample output: a table listing any task with an open clock entry — or the message "No running clocks found." if all sessions are closed.
Adds up every closed clock entry in the current file and outputs a single grand total. A quick way to see how much time you have logged on everything in the active note.
// Grand total of all closed clock time in the current file.
function parseClockValue(raw) {
const [startStr, endStr] = raw.split("--");
const start = dv.date(startStr.trim());
const end = endStr && endStr.trim() !== "" ? dv.date(endStr.trim()) : null;
return { start, end };
}
let totalMinutes = 0;
for (const task of dv.current().file.tasks) {
for (const child of (task.children ?? [])) {
if (!child.clock) continue;
const { start, end } = parseClockValue(String(child.clock));
if (!start || !end) continue; // skip open entries
totalMinutes += Math.floor(end.diff(start, "minutes").minutes);
}
}
const h = Math.floor(totalMinutes / 60);
const m = totalMinutes % 60;
dv.paragraph(`Total: ${h}:${String(m).padStart(2, "0")}`);
Sample output: a single line such as Total: 7:45 — the sum of all closed clock sessions in the current file.
Clock entries are stored as indented child list items directly beneath each task:
- [ ] Fix login bug
- [clock:: 2026-06-15T09:00--2026-06-15T10:30]
- [clock:: 2026-06-16T14:00--]
[clock:: START--END] — closed entry (completed session)[clock:: START--] — open entry (clock is running)YYYY-MM-DDTHH:mm (local time, no timezone, no seconds)useTab: true in the vault)Because clock children are list items with inline fields, DataviewJS exposes them via task.children[].clock:
for (const page of dv.pages()) {
for (const task of page.file.tasks) {
for (const child of (task.children ?? [])) {
if (child.clock) {
// child.clock is a plain STRING — Dataview stores the full "START--END" value as text.
// Split on "--" and parse each half with dv.date() to get DateTime objects.
// "YYYY-MM-DDTHH:mm--YYYY-MM-DDTHH:mm" → closed entry
// "YYYY-MM-DDTHH:mm--" → open/running entry
}
}
}
}
Two-field alternative: if you want Dataview to treat start and end as native DateTime objects — enabling item.end - item.start directly in DQL without string splitting — use two separate inline fields per entry instead of one:
- [ ] Fix login bug
- [start:: 2026-06-15T09:00] [end:: 2026-06-15T10:30]
The tradeoff: two fields per session (more verbose) vs. the compact [clock:: START--END] format Logbook writes. The plugin uses the single-field format to match org-mode convention and keep entries readable.
vault.getFileByPath())isDesktopOnly: false)Logbook: clock out closes all open entries with the same end time and shows a Notice listing the extras.vault.on('rename') listener).useTab + tabSize settings.git clone https://github.com/p1tt1/obsidian-logbook
cd obsidian-logbook
npm install
npm run dev # esbuild watch mode — rebuilds main.js on save
Symlink into a dev vault:
ln -s /absolute/path/to/obsidian-logbook /path/to/vault/.obsidian/plugins/logbook
Install the Hot Reload plugin in the dev vault to auto-reload on main.js changes. The symlink target directory name must be logbook — it must match manifest.json.id.
Tests: npm test (Vitest, 79+ cases for the pure modules under src/core/).
MIT — see LICENSE.