Logbook

approved

by p1tt1

Track time on tasks with org-mode LOGBOOK-style clock entries, stored as Dataview-queryable inline fields. - This plugin has not been manually reviewed by Obsidian staff.

1 stars44 downloadsUpdated 26d agoMIT

Logbook

Track time on tasks with org-mode LOGBOOK-style clock entries, stored as Dataview-queryable inline fields.

Release Obsidian License Privacy

100% local — no network requests, no telemetry, no external services. Logbook only writes plain Markdown to your vault; nothing ever leaves your machine.

Installation

From the community browser

Settings → Community plugins → Browse → search "Logbook" → Install → Enable.

Beta install via BRAT

To test the latest pre-release build before it lands in the community browser:

  1. Install the BRAT plugin (obsidian42-brat) from the community browser.
  2. In Obsidian: Cmd/Ctrl+PBRAT: Add a beta plugin for testing.
  3. Paste https://github.com/p1tt1/obsidian-logbook into the modal.
  4. Enable Logbook under Settings → Community plugins.

Manual install

  1. Download main.js, manifest.json, and styles.css from the latest release.
  2. Copy them to <vault>/.obsidian/plugins/logbook/.
  3. Reload Obsidian → Settings → Community plugins → enable Logbook.

Usage

Clocking in

Cursor on a task line → run Logbook: Clock in to task from the command palette.

Clocking out

Run Logbook: Clock out → the open entry gains an end timestamp.

Commands

CommandWhat it does
Logbook: Clock in to taskInserts an open clock child below the current task. Hidden when cursor isn't on a task line.
Logbook: Clock outCloses the active task's open entry. Hidden when no clock is active.
Logbook: Clock report: current fileModal showing closed clock entries from the active file.
Logbook: Find all open clocksVault-wide scanner — surfaces any forgotten open entries.

DataviewJS examples

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.

DQL (without JavaScript)

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.

1. Total time per task (current file)

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.

2. Daily time rollup (vault-wide)

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.

3. Weekly summary (by ISO week)

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.

4. Running clock indicator

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.

5. Current file summary (grand total)

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.

Data format reference

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)
  • Datetime format: YYYY-MM-DDTHH:mm (local time, no timezone, no seconds)
  • Indentation: parent task's indent + 2 spaces (or one tab if 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.

Requirements

  • Obsidian 1.5.7 or newer (required for vault.getFileByPath())
  • Works on desktop and mobile (isDesktopOnly: false)
  • Optional: Dataview plugin with DataviewJS enabled — only required for the query examples; the plugin writes plain Markdown either way.

Edge cases & known behaviors

  • File deleted while clocked in — state is cleared silently on next load; the clock interval is lost.
  • Multiple open entries (manual edit / corruption)Logbook: clock out closes all open entries with the same end time and shows a Notice listing the extras.
  • File rename — the active clock follows the renamed file automatically (vault.on('rename') listener).
  • Tab-indented vaults — clock children are indented using your vault's useTab + tabSize settings.
  • Running clock > 24h — a Notice surfaces on next load to remind you to close it.

Contributing

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/).

License

MIT — see LICENSE.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.