Zero-to-working install for task-maxxing, the two-way Obsidian ↔ Morgen task sync. Budget ~60–90 minutes the first time, mostly clicking through third-party UIs (n8n, GitHub, Morgen). Every command in this doc is copy-pasteable; placeholders look like {{YOUR_THING}} and need to be replaced before you run.
Coming from a pre-2026-05-04 install (3-way with Notion)? Skip to Migrating from the 3-way version at the end.
- A 2-way live sync between your Obsidian vault's
05-Tasks/folder and your Morgen account. - A local launchd daemon on your Mac that auto-commits every task edit and pushes it to a private GitHub repo.
- Three workflows running in n8n cloud — W0 (orchestrator), W1 (Obsidian → Morgen), W2 (Morgen → Obsidian) — firing on a 20-minute cycle.
Tick a checkbox in either Obsidian or Morgen, the other side catches up within ~60 seconds of the next W0 tick.
You need accounts at:
- Obsidian — desktop app at obsidian.md, with a vault that has a
05-Tasks/folder. If you don't have a vault yet, clone2ndBrain-moggingfirst — it's the recommended starting layout andtask-maxxingdrops straight into its05-Tasks/. - Obsidian Tasks plugin by Clare Macrae — install via Obsidian → Settings → Community plugins → Browse → "Tasks". Enable it.
- GitHub account with one private repo slot free.
- n8n cloud — sign up at n8n.io. The free Starter tier works; the Pro tier is recommended for reliable cron triggers.
- Morgen account with API access — Pro tier required (Morgen's free tier doesn't expose the API). morgen.so.
Local tools (macOS):
node --version # need v20 or newer
git --version # any 2.x
gh --version # GitHub CLI — used once for repo creation
jq --version # required by the workflow installerIf anything is missing:
brew install node git gh jqMake sure the Tasks plugin is enabled, then create the folder and one sample file the sync needs.
Don't put your vault under
~/Desktop,~/Documents, or~/Downloads— those are macOS permission-protected (TCC/Full-Disk-Access) and break terminal/git access for the daemon. A home-level path like~/BRAIN2works.
mkdir -p /path/to/your-vault/05-TasksInside the vault, create 05-Tasks/TASKS-URGENT.md:
# Urgent
- [ ] task-maxxing smoke test 📅 2099-12-31 ⏫ 🆔 m-deadbeefThe canonical task line shape is mandatory — the W1 parser only matches this token order:
- [ ] <task text> <priority?> 📅 YYYY-MM-DD 🆔 m-XXXXXXXX
🆔 m-XXXXXXXXis required on every task. The 8 chars are lowercase hex.- For new tasks you create by hand, you can leave the 🆔 off — W1 will mint and inject one on its first run. Once minted, never rewrite it; the sync uses it as the join key.
- Priorities: 🔺 highest · ⏫ high · 🔼 medium · 🔽 low · ⏬ lowest.
- See
examples/sample-TASKS-URGENT.mdfor a fuller reference file.
cd ~/code # or wherever you keep dev repos — NOT ~/Desktop, ~/Documents, or ~/Downloads (macOS permission-protected; breaks terminal/git access)
git clone https://github.com/fidgetcoding/task-maxxing.git
cd task-maxxing
npm install # repo has zero runtime deps; this just locks package.json
cp examples/sample-.env.example .env
$EDITOR .envYou'll fill in .env as you walk through the next steps. The shape you'll end up with:
VAULT_PATH=/absolute/path/to/your-vault/05-Tasks
TASK_MAXXING_REPO=/absolute/path/to/your-vault/05-Tasks # same value
GITHUB_REPO_OWNER={{YOUR_GH_USERNAME}}
GITHUB_REPO_NAME={{YOUR_VAULT_NAME}}-tasks
GITHUB_TOKEN=github_pat_{{YOUR_PAT}}
MORGEN_API_KEY={{YOUR_MORGEN_KEY}}
MORGEN_KEY=${MORGEN_API_KEY}
N8N_BASE_URL=https://{{YOUR_SUBDOMAIN}}.app.n8n.cloud
N8N_API_KEY={{YOUR_N8N_KEY}}Leave the file open. Notion env vars (NOTION_TOKEN, NOTION_DATABASE_ID) are no-ops as of 2026-05-04 — leave them unset.
n8n cloud can't reach your laptop's filesystem, so the sync uses a tiny private GitHub repo as the shared state surface. The local daemon pushes your 05-Tasks/ markdown into it; n8n reads and writes it via the GitHub API.
gh repo create {{YOUR_GH_USERNAME}}/{{YOUR_VAULT_NAME}}-tasks \
--private \
--description "task-maxxing sync state for my vault" \
--clone=falsePaste the values into .env:
GITHUB_REPO_OWNER={{YOUR_GH_USERNAME}}
GITHUB_REPO_NAME={{YOUR_VAULT_NAME}}-tasksThe simplest setup is to make 05-Tasks/ itself a git working tree pointing at the new mirror repo. (If your vault is already a git repo, treat 05-Tasks/ as a submodule instead — same end state.)
cd /path/to/your-vault/05-Tasks
git init
git remote add origin https://github.com/{{YOUR_GH_USERNAME}}/{{YOUR_VAULT_NAME}}-tasks.git
git branch -M main
git add .
git commit -m "[bot:save] initial 05-Tasks snapshot"
git push -u origin mainYou should see one commit on the GitHub repo. If you don't, fix that before continuing — the rest of the pipeline depends on the daemon being able to push here.
GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token
Configure it like this:
- Token name:
task-maxxing - Expiration: 1 year (set a calendar reminder to rotate)
- Resource owner: your GitHub username
- Repository access: Only select repositories → pick
{{YOUR_VAULT_NAME}}-tasks(the one you just created — nothing else) - Repository permissions:
- Contents: Read and write
- Metadata: Read-only (auto-enabled)
- Everything else: No access
Click Generate token, copy it once (it's only shown one time), and paste into .env:
GITHUB_TOKEN=github_pat_{{YOUR_PAT}}-
Open platform.morgen.so/integrations/developers-api and sign in.
-
Click Create API key, name it
task-maxxing. -
Copy the key. Paste into
.env:MORGEN_API_KEY={{YOUR_MORGEN_KEY}} # MORGEN_KEY is an alias the workflow installer reads — it should already # be set to ${MORGEN_API_KEY} from the sample file.
Sanity check the key:
set -a; source .env; set +a
curl -sS \
-H "Authorization: ApiKey ${MORGEN_API_KEY}" \
"https://api.morgen.so/v3/tasks/list?limit=1" | head- 401 → wrong key.
- 403 → your Morgen account doesn't have API access. Upgrade to Pro and try again.
- JSON with a
tasksarray → you're good.
Go to n8n.io, pick a subdomain — that becomes {{YOUR_SUBDOMAIN}}.app.n8n.cloud. Verify your email and log in.
Paste into .env:
N8N_BASE_URL=https://{{YOUR_SUBDOMAIN}}.app.n8n.cloudIn n8n: avatar (bottom-left) → Settings → API → Create an API key. Name it task-maxxing-installer, copy it, paste into .env:
N8N_API_KEY={{YOUR_N8N_KEY}}cd ~/code/task-maxxing
set -a; source .env; set +a # reload all the new values
# Preview the rendered JSON without sending it (writes to /tmp)
DRY_RUN=1 ./scripts/install-workflows.sh
# Import for real
./scripts/install-workflows.shWhat the script does:
- Validates every required env var is set, exits non-zero with a clear message if not.
- Substitutes
{{GITHUB_TOKEN}},{{MORGEN_KEY}},{{GITHUB_OWNER}},{{GITHUB_REPO_NAME}}, etc. into the workflow JSON files. - Refuses to continue if any unreplaced
{{PLACEHOLDER}}remains. - POSTs W1 and W2 first, captures their assigned IDs, then templates those IDs into W0 and POSTs it last.
- Prints the three created workflow IDs and the next step.
The script is not idempotent. n8n's API has no upsert-by-name. Re-running creates duplicate workflows. If you need to reinstall, delete the previous W0/W1/W2 in the n8n UI first.
Open the n8n UI → Workflows. You should see three new entries, all currently inactive.
The daemon watches 05-Tasks/ via launchd's WatchPaths, debounces, and runs git add && git commit && git push once per fire (30s throttle, 5min heartbeat). This is the only part of the system that touches your local disk.
cd ~/code/task-maxxing
BUNDLE_ID=io.example.task-maxxing-daemon \
WATCH_PATH="/path/to/your-vault/05-Tasks" \
SCRIPT_PATH="$(pwd)/src/auto-commit.js" \
bash daemon/install-daemon.shThe installer:
- Wraps your
nodebinary in a.appbundle at~/Library/Application Support/task-maxxing/TaskMaxxingDaemon.app. (macOS Full Disk Access only applies to.app-wrapped executables — a bare Node script can't be granted FDA.) - Renders
daemon/io.example.task-maxxing-daemon.plist.templateto~/Library/LaunchAgents/${BUNDLE_ID}.plist. - Loads it with
launchctl bootstrap. - Prints the path you'll need for the FDA grant in the next step.
Without FDA, the daemon will log FATAL: cannot read .../.git/HEAD on every tick — macOS sandbox policy blocks reading anything in ~/Desktop, ~/Documents, ~/Downloads, or iCloud-synced folders.
- System Settings → Privacy & Security → Full Disk Access.
- Click +, unlock with Touch ID.
- Press Cmd+Shift+G, paste the
.apppath the installer printed (something like~/Library/Application Support/task-maxxing/TaskMaxxingDaemon.app). - Make sure the toggle next to the new entry is ON (green).
Reload the agent so launchctl picks up the permission:
launchctl bootout "gui/$(id -u)/${BUNDLE_ID}"
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/${BUNDLE_ID}.plist"Verify it's running:
launchctl list | grep task-maxxing
# expect one line with a numeric PID, not "-"The first time you run task-maxxing, Morgen has no idea your tasks exist. The backfill script walks VAULT_PATH, creates a Morgen task for each open markdown task, and writes a seed .sync-state.json so the workflows can dedupe.
# Always preview first
node scripts/morgen-backfill.js --dry-run
# If the projected payload + point cost looks right, run live
node scripts/morgen-backfill.jsThe script reads VAULT_PATH and MORGEN_API_KEY from your sourced .env. It is resume-safe — re-running picks up where it left off using the partial .sync-state.json it wrote on the previous abort.
When it's done:
cd "$VAULT_PATH"
git diff .sync-state.json
git add .sync-state.json
git commit -m "[bot:backfill] seed Morgen task IDs"
git pushBudget abort? If the projected cost exceeds
--max-points(default 85, ceiling 300/15min), the script exits 2 and tells you the suggested batch size. Wait 15 minutes between batches.
Don't activate W0 yet — manually fire it once and watch the executions panel.
- In n8n, open W0-Sync-Orchestrator.
- Click Execute Workflow (top-right).
- Watch the Executions tab. W0 should call W2 first (Morgen → Obsidian), then W1 (Obsidian → Morgen). Both should finish green.
- In Morgen, look for your "task-maxxing smoke test" task. It should be in your inbox list with priority High and due 2099-12-31.
- In Obsidian, open
05-Tasks/TASKS-URGENT.md. The line you wrote should now have a🆔 m-XXXXXXXXtoken (W1 minted it during the run).
If both checks pass, the sync is working end-to-end. If not, jump to TROUBLESHOOTING.md.
In the n8n UI:
- Open W0-Sync-Orchestrator.
- Toggle Active on (top-right).
- Leave W1 and W2 inactive. W0 calls them directly via
executeWorkflowwithwait=true, which serializes them so they never race on.sync-state.json. If you activate W1 or W2's own triggers alongside W0, you'll get double-fires.
That's it. The sync is now live. W0 fires every 20 minutes; an edit on either side propagates by the next tick.
- In Morgen, tick the "task-maxxing smoke test" task complete.
- Wait one W0 cycle (≤20 min).
- Open
05-Tasks/TASKS-URGENT.md— the line should have flipped from- [ ]to- [x], andgit log --onelineon the mirror repo should show a[bot:w2]commit applying the change.
Then test the other direction:
- In Obsidian, add a new line to
TASKS-URGENT.md:- [ ] hello from obsidian 📅 2099-12-31 ⏫ - Save. The daemon will commit and push within ~30s; check the GitHub repo to confirm.
- Wait one W0 cycle.
- Open Morgen — a new task appears. Open the markdown again — the line now has a
🆔 m-XXXXXXXXtoken.
If both round-trips pass, you have a working install.
There's a fourth workflow you can wire in to ping you when the sync silently breaks (e.g., n8n cron pauses, GitHub PAT expires). It's not in the default install-workflows.sh flow because most people don't need it for the first install — add it later if you start losing signal.
The minimum useful watchdog:
- A schedule trigger every 30 minutes.
- An HTTP node that hits
${N8N_BASE_URL}/api/v1/executions?workflowId=<W0_ID>&status=success&limit=1. - An IF node that compares the latest execution timestamp to "now" — if older than 60 minutes, fire a Telegram (or Slack, or email) alert.
If you want to wire Telegram specifically: create a bot via @BotFather, grab the token, send a message to your bot, then GET https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat ID. Plug both into n8n credentials and point the alert node at them.
Don't reinvent these — see TROUBLESHOOTING.md for the full table.
- Daemon shows up as
-inlaunchctl list→ FDA not granted, see Step 5. - Daemon log says
FATAL: cannot read .../.git/HEAD→ same root cause, FDA. - W1 fails with
403 Resource not accessible by personal access token→ PAT scope wrong, redo Step 2c. - W2 fails with
401 Unauthorizedagainst Morgen → key copied wrong, redo Step 3. install-workflows.sherrors onunreplaced placeholders→ an env var is empty in your sourced.env. Re-runset -a; source .env; set +a.- Tasks appearing in Morgen but flipping
- [x]doesn't propagate to Obsidian → the daemon isn't pushing.tail -50 ~/Library/Logs/task-maxxing.log. - Two orchestrators firing at once → you re-ran the installer without deleting the old workflows. Delete duplicates in the n8n UI.
- Commit on the mirror repo not prefixed
[bot:daemon]/[bot:w2]/[bot:backfill]→ manual edit slipped past the prefix guard. Amend the message before W1 re-ingests.
If you set this up before 2026-05-04, your install still has Notion in the loop. The 2-way cutover is short:
-
Pull the latest code:
cd ~/code/task-maxxing git pull origin main
-
Delete the old workflows in the n8n UI. Specifically the
W3-Notion-Done-To-Obsidian-Syncworkflow, plus any olderW2-3-1-Sync-Orchestratorfrom before W0 existed. Keep only the new W0/W1/W2 you're about to import. -
Re-run the installer:
set -a; source .env; set +a ./scripts/install-workflows.sh
Activate the new W0 only.
Your 05-Tasks/ markdown, your Morgen tasks, and your .sync-state.json keep working — none of those changed shape. The Notion database is no longer touched; you can archive it (or keep it around as a read-only backup; the kit will not write to it again). Leave NOTION_TOKEN / NOTION_DATABASE_ID unset in .env.
You're done. Day-to-day usage is "edit your markdown tasks like you always have" — the rest happens behind the scenes on a 20-minute cadence.
If something breaks: TROUBLESHOOTING.md. If you want to understand the wiring: ARCHITECTURE.md.