M A N D A L I V I A
Obsidian Lab All Notes

Nightly Toggl Summary in Your Obsidian Daily Note

The Problem

I built a voice-driven system for logging time to Toggl through Claude — speak naturally, entries get created. That solved the input side. But the data lived in Toggl, disconnected from where I actually reflect on my day: the daily note.

My nightly review asks “what happened today?” and my weekly review asks “where did time go this week?” Neither could answer those questions without opening Toggl separately. The daily note needed to become the permanent record — not just for tasks completed, but for how time was spent.

The Solution

A nightly script that fetches yesterday’s Toggl entries via the API and writes a detailed ### Time Log section into yesterday’s daily note. It runs alongside an existing Things task sync — same script, same schedule, independent of each other.

Here’s what it produces:

### Time Log
> **Total: 5h 45m** · Foundation 1h 30m · Creating 3h 15m · Community 1h
 
- 07:00–07:30 · Spanish Class · Learning (Creating) · 30m
- 07:30–08:00 · Meditation · Meditation (Foundation) · 30m
- 09:30–10:30 · Gym · Exercise (Foundation) · 1h
- 11:00–12:30 · Writing · Writing (Creating) · 1h 30m
- 14:00–15:15 · Client session · Consulting (Creating) · 1h 15m
- 16:00–16:30 · Meeting with [John Doe](/obsidian/john-doe/) · Friends (Community) · 30m
- 17:00–17:30 · Email and planning · Admin (Sustaining) · 30m

How To

Prerequisites

  1. Toggl account with API token (Profile → API Token)
  2. Workspace ID (visible in URL when in your workspace)
  3. Add to ~/.env:
TOGGL_TOKEN=your_token_here
TOGGL_WORKSPACE_ID=your_workspace_id

Project-to-Area Mapping

Toggl projects map to life areas for the summary line. Define the mapping in the script:

const PROJECT_TO_AREA = {
  exercise: 'Foundation',
  meditation: 'Foundation',
  breathwork: 'Foundation',
  family: 'Community',
  friends: 'Community',
  consulting: 'Creating',
  writing: 'Creating',
  coding: 'Creating',
  learning: 'Creating',
  admin: 'Sustaining',
  rest: 'Rest',
};

This mirrors how Toggl clients group projects — but happens at write time, so you don’t need a paid Toggl plan for client features. Unmatched projects fall through to “Other.”

The area names are yours to define. I use five that correspond to how I think about my time: Foundation (body and spirit), Community (relationships), Creating (work), Sustaining (life maintenance), and Rest.

The script scans a Personal CRM folder for filenames and matches them against Toggl entry descriptions. If “John Doe” appears in a description and Personal CRM/John Doe.md exists, it becomes [John Doe](/obsidian/john-doe/) in the output.

function loadCrmNames(vaultPath) {
  const crmDir = path.join(vaultPath, 'Personal CRM');
  return fs.readdirSync(crmDir)
    .filter((f) => f.endsWith('.md'))
    .map((f) => f.replace(/\.md$/, ''))
    .filter((name) => name.length >= 3)
    .sort((a, b) => b.length - a.length); // longest first
}
 
function wikilinkCrmNames(text, crmNames) {
  let result = text;
  for (const name of crmNames) {
    if (SELF_NAMES.includes(name.toLowerCase())) continue;
    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const regex = new RegExp(
      `(?<!\\[\\[)\\b(${escaped})\\b(?!\\]\\])`, 'i'
    );
    if (regex.test(result)) {
      result = result.replace(regex, `[${name}](/obsidian/name/)`);
    }
  }
  return result;
}

Conservative by design: names are sorted longest-first to avoid partial matches, your own name is excluded via a SELF_NAMES list, and only exact whole-word matches trigger a link. No fuzzy matching, no guessing. If it doesn’t find a match, it leaves the text alone.

Daily Note Anchor

The script looks for a ## 🌘 Nightly Review heading (configurable) and inserts the Time Log before the next ## section. Adjust the heading constant to match your template:

const NIGHTLY_REVIEW_HEADING = '## 🌘 Nightly Review';
const TIME_LOG_HEADING = '### Time Log';

If the heading doesn’t exist or a Time Log section is already present, the script skips — no partial writes, no duplicates.

Scheduling

The script runs daily via a macOS LaunchAgent, same as the Things task sync. It targets yesterday’s note and uses a state file for idempotency — running it twice on the same day does nothing after the first success.

A --dry-run flag shows exactly what would be written without touching any files. Use it liberally when setting up.

The full script — which handles both Toggl time entries and Things completed tasks in a single nightly pass — is available on GitHub.

What I Learned

Life area rollups matter more than raw entries. The summary line (Foundation 1h 30m · Creating 3h) is what I actually look at during weekly review. Individual entries are there for drilling in, but the rollup shows patterns — am I neglecting rest, leaning too hard into admin, getting enough creative work done.

CRM linking was cheap. One readdir call, pure string matching, no API calls — milliseconds against a couple hundred names with zero false positives. The payoff goes beyond the time log: entries create backlinks to people, so a person’s CRM note shows when you last met without any manual logging.

The script fetches Things tasks and Toggl entries separately, and either source can fail without blocking the other. Toggl’s API down? Things tasks still get written. No Things CLI? The time log still appears. This independence wasn’t the original design — it emerged from not wanting to lose a whole night’s sync because one service hiccuped.

Caveats

Project mapping is manual. When you add a new Toggl project, you need to add it to the PROJECT_TO_AREA mapping. Unmapped projects show as “Other” — not broken, just less informative.

CRM linking only catches exact names. If your Toggl entry says “call with Ben” but the CRM file is Benjamin Smith.md, it won’t match. Nicknames and abbreviations fall through silently. This is by design — false links are worse than missing links.

Toggl free plan rate limits. Roughly 10 API calls per hour. The nightly script makes 2 calls (entries + projects), so you’ll never hit it. But if you’re also running the hourly live-update report, keep an eye on it.

Voice-Driven Time Tracking with Toggl and Claude Syncing Completed Things 3 Tasks into Obsidian Daily Notes

Keep Exploring