Symptom → diagnose → fix runbook for the two-way Obsidian ↔ Morgen sync. Each entry is one observable failure with the shortest path back to a working pipeline.
If your problem isn't here, open an issue with a redacted .sync-state.json
snippet and the last n8n execution log for whichever workflow stalled.
Note
Architecture recap. As of 2026-05-04 this kit is two-way:
- W1 (Obsidian → Morgen) — polls GitHub every 20 min, re-publishes Morgen tasks.
- W2 (Morgen → Obsidian) — polls Morgen every 20 min, commits markdown changes.
- W0 (orchestrator) — every 20 min, runs
W2 → W1in series. - Watchdog — hourly, checks for stale
[bot:W1]commits, opens a GH issue + optional Telegram on alert.
If you're upgrading from a 3-way (Notion) install, jump to the Legacy: Notion era section first, then come back here.
Find your symptom in the index, top to bottom. Severity tiers:
- STOP-EVERYTHING — sync is silently dead, nothing is moving.
- WORKING-BUT-WRONG — sync runs but the data on the other side is wrong.
- DAILY-DRIVER — small annoyances that don't break the loop.
- LEGACY — leftover symptoms from the 3-way Notion era.
- Sync stopped silently — no
[bot:W1]commits for hours - W1 401-failing on the GitHub API
- W2 401-failing on the Morgen API
- n8n cloud instance unreachable
- Duplicate tasks in Morgen
- Task edited in Obsidian, not in Morgen
- Task edited in Morgen, not in Obsidian
- Task deleted in Obsidian, ghost in Morgen
m-XXXXXXXXIDs being regenerated on every sync
- Task lands in
TASKS-GENERAL.mdinstead of its project area file - Watchdog crying wolf
- Telegram alerts not arriving
No
[bot:W1]commits for hours, no[bot:W2]either, edits aren't propagating in either direction.
Diagnose
- Open the GitHub mirror repo. Look at the last commit by
[bot:W1]or[bot:W2]. If it's >40 min old, the sync is stalled. - In n8n, open the W0 orchestrator workflow (the every-20-min cron). Check the Executions tab — should be a green run within the last 20 min.
- Verify both W1 and W2 workflows show
Active: ONin the n8n workflow list.
Fix
- If W0 is inactive, toggle it back to active and click Execute Workflow once to kick a run.
- If W0 is active but no recent executions, the schedule trigger died. Open the workflow, edit anything trivial (a comment in a Code node), save, and re-publish. n8n cloud sometimes drops cron triggers on a deploy and a republish re-arms them.
- If W0 is firing but W1 / W2 are archived, re-import their JSON from
workflows/and re-bind credentials. - If W0 ran and you see a red execution, click into it and follow whichever step below matches the failing node (401 on GitHub → entry 2; 401 on Morgen → entry 3).
Why it happens
n8n cloud occasionally drops schedule triggers after a workflow edit or a platform-side deploy. The watchdog will catch this within an hour by opening a GitHub issue, but the fix is the same — republish the workflow.
W1 execution shows red on the GitHub Tree fetch or GitHub Contents commit node with
401 UnauthorizedorBad credentials.
Diagnose
- Open the failed W1 execution in n8n.
- Click the red node. Look at the Authorization header value or the bound credential.
- If you see
Bearer ghp_...or the credential name is greyed out, the PAT is invalid, expired, or unbound.
Fix
- On GitHub: Settings → Developer settings → Personal access tokens → Fine-grained tokens → generate a new token. Scope: Contents: Read and write on your tasks mirror repo only.
- In n8n: Credentials → GitHub (task-maxxing) → paste the new token → Save.
- Re-publish W1 (open the workflow, click Save, then toggle Active off and on).
- Trigger one manual run via Execute Workflow. It should land green within 30s.
Why it happens
Fine-grained PATs default to 90-day expiry. Mark your calendar for renewal at ~80 days, or use a no-expiry token (less safe — your call).
W2 execution shows red on a
https://api.morgen.so/v3/...node with401 Unauthorized.
Diagnose
- Open the failed W2 execution.
- Click the red node, check the
Authorization: ApiKey ...header. - Verify the API key isn't
undefinedor empty (n8n shows[CREDENTIAL]placeholder when bound — empty means unbound).
Fix
- In Morgen: Settings → API & Integrations → Generate API key (or rotate the existing one — note that rotating revokes the old key).
- In n8n: Credentials → Morgen (task-maxxing) → paste the new key → Save.
- Re-publish W2 (toggle Active off / on).
- Execute Workflow once to confirm green.
Why it happens
Morgen API keys don't auto-expire, but they get revoked if you rotate them in the Morgen UI for any reason. There's no warning — the n8n credential just starts returning 401.
The n8n UI won't load, or workflows are stuck in a
runningstate forever.
Diagnose
- Visit status.n8n.io — check for active incidents.
- Try loading your n8n workspace URL in an incognito window (rules out a stale auth cookie).
Fix
- If n8n is down platform-wide: wait. The watchdog will alert when commits go stale, and the sync resumes automatically once n8n is back. No manual intervention needed.
- If only your instance is wedged: open a support ticket with n8n. In the meantime, you can self-host n8n locally and re-import the workflows from
workflows/— see n8n's self-host docs (out of scope here). - If your auth cookie went stale: clear cookies for the n8n domain and log back in.
Same task text appears twice (or more) in the Morgen sidebar.
Diagnose
# In your vault, grep for the duplicated task text without the 🆔 token:
cd "$VAULT_PATH/05-Tasks"
grep -rn "the task text here" .
# Then check whether the matching line in markdown carries 🆔 m-XXXXXXXX:
grep -rn "🆔 m-" . | grep "the task text here"If the markdown line has no 🆔 m-XXXXXXXX, that's the bug — W1 minted a fresh ID on the next run because it couldn't find the existing one.
Fix
- Open the Morgen sidebar. Find the dupe pair. Pick the one with the most recent
updatedAtand copy its task ID from the URL or details panel (Morgen task IDs look likem-a1b2c3d4). - Delete the other Morgen dupe.
- In your vault, edit the task line and append the surviving ID:
- [ ] task text ⏫ 📅 2026-05-10 🆔 m-a1b2c3d4 - Save. Daemon commits, W1 runs on the next 20-min tick. From now on, that task is stable.
Alternative: open 05-Tasks/.sync-state.json, find the entry by text, copy its morgenTaskId, paste back into the markdown line as the 🆔 value.
Why it happens
W1's only join key from markdown → Morgen is the 🆔 m-XXXXXXXX token. If a manual edit, a copy-paste, or a /save write strips it, W1 sees an "ID-less new task" and creates a fresh Morgen row alongside the old one. Treat the 🆔 token as load-bearing.
You changed a task's due date / priority / text in Obsidian, daemon committed it, but Morgen still shows the old value 30+ min later.
Diagnose
cd "$VAULT_PATH/05-Tasks"
git log -1 --pretty=format:"%s%n%b" -- TASKS-*.md FIDGETCODING/**/TASKS-*.mdLook at the commit subject. If it starts with any [bot:*] prefix, that's the problem — W1's echo guard skipped the run.
Also check the n8n W1 executions tab — if there's no execution within the last 20 min on that file's commit, the trigger never fired.
Fix
If the commit was bot-prefixed by accident (e.g., a tool committed for you with [bot:save]):
# Re-commit with a non-bot subject so W1 picks it up:
cd "$VAULT_PATH/05-Tasks"
git commit --allow-empty -m "manual edit on $(date -u +%FT%TZ)"
git push origin mainWithin 20 min, W1 will run and propagate to Morgen.
If you can't redo the commit (already squashed / pushed elsewhere): just edit the task manually in Morgen to match. The next time you change anything in either side, the sync will reconcile.
Why it happens
The echo-loop guard exists to prevent W2's commits from triggering W1 (which would create an infinite ping-pong). The cost is that any [bot:*] commit is invisible to W1. Use plain prefixes for human edits.
You changed a task in the Morgen UI (date, priority, completion), but the markdown still shows the old value 30+ min later.
Diagnose
- Open n8n → W2 workflow → Executions tab. Look for a run within the last 20 min.
- If executions are running but no commit is being pushed, click into the latest run and check the diff node — the change might be invisible to W2's filter.
- If no executions at all, W2's schedule trigger is dead (see entry 1).
- Check for Morgen 429s in the execution log — see "Why it happens" below.
Fix
- Open the W2 workflow in n8n.
- Click Execute Workflow to fire a manual run.
- Watch the run. If it goes green and commits to GitHub, the schedule trigger was just lagging — it'll resume on the next 20-min tick. Done.
- If the manual run fails on a Morgen API node, follow entry 3 (Morgen 401) or wait 15 min for a 429 rate-limit reset.
- If the run completes but no commit fires, the task's change wasn't in W2's six tracked dimensions (text, due, scheduled, priority, completion, deletion). Sub-second time changes from Morgen's auto-scheduler intentionally don't round-trip.
Why it happens
Morgen's API is rate-limited at 300 points / 15 min. W2's /v3/tasks/list call costs 10 points per run, so a 30-task batch update plus list overhead can push us past the budget — Morgen replies 429 and W2 silently defers to the next run.
You removed a task line from a
TASKS-*.mdfile. Daemon committed. Morgen still shows the task.
Diagnose
This is the documented asymmetric-delete behavior, not a bug. W1 does not soft-delete Morgen tasks when a markdown line disappears — only W2 propagates deletes (Morgen → Obsidian).
# Confirm the markdown line is actually gone:
cd "$VAULT_PATH/05-Tasks"
git log -p -- TASKS-*.md | grep "🆔 m-XXXXXXXX"Fix
To remove the task from both sides cleanly: delete it in Morgen. On the next W2 tick (≤20 min), W2 will see the Morgen task is gone and remove the corresponding markdown line (or strike it through, depending on your settings).
If the markdown is already gone and you just want to clear the Morgen ghost: open Morgen, delete it manually. No further sync action needed — there's no markdown line for the deletion to propagate back to.
Why it happens
Asymmetric delete is intentional. The original 3-way design treated Notion and Morgen as mirrors — deleting markdown was a destructive act that should require explicit confirmation in the source app. The 2-way version inherits that rule. If you want symmetric delete, that's a workflow change in W1 (open an issue).
Every W1 run mints a new
🆔 m-XXXXXXXXfor the same task, creating a Morgen dupe each cycle.
Diagnose
cd "$VAULT_PATH/05-Tasks"
git log -p -- TASKS-*.md | grep "🆔 m-" | sort | uniq -c | sort -rn | headIf the same task line shows multiple distinct m- IDs over recent commits, something is stripping the ID between syncs.
Common culprits:
- A pre-commit hook reformatting markdown (e.g., a Prettier hook flattening emoji tokens).
- A plugin or skill that rewrites task lines without preserving the
🆔token. - Manual edits that drop the trailing emoji block.
Fix
- Identify the writer. Run
git log -pon the task file and find the commit that introduced the ID-less version. The author / commit message tells you which tool. - Disable or fix the offending pre-commit / plugin / skill so it preserves trailing
🆔 m-XXXXXXXXtokens byte-for-byte. - Manually re-merge the duplicate Morgen tasks — keep the most recent
m-ID, delete the others, paste the surviving ID back into the markdown line. - Re-commit. From now on, the task should be stable.
Why it happens
W1 only mints new IDs when a task line lacks the 🆔 token. The mint is the right behavior for genuinely new tasks; it's the wrong behavior when a writer accidentally strips an existing ID. The fix is always upstream — protect the 🆔 token.
You created a task that should have gone into a project area file (
TASKS-LORECRAFT.md,TASKS-WAGMI.md, etc.) but it landed inTASKS-GENERAL.md.
Diagnose
Open the task line. Check the trailing 🏷️ tag (or whatever your task creator uses to route by area). If the tag doesn't match a known area, the creator falls back to GENERAL.
Fix
- Cut the line out of
TASKS-GENERAL.md. - Paste it into the correct
TASKS-{AREA}.md. - Preserve the
🆔 m-XXXXXXXXtoken byte-for-byte so the Morgen-side mapping survives the move. - Save. Daemon commits, W1 runs on the next tick — Morgen's task tag updates to match the new area.
If you use the maketasks skill to create tasks, fix the source: pass the area tag explicitly when creating, or update the alias map so the entity routes to the right area.
Why it happens
TASKS-GENERAL.md is the catch-all by design. Wrong-area writes are a tagging bug, not a sync bug.
You got a GitHub issue (or Telegram message) saying "no
[bot:W1]commit in 60+ min" but you can see W1 is healthy.
Diagnose
- Open the GH issue the watchdog created. Note the exact threshold it complained about.
- Cross-check against the actual last
[bot:W1]commit timestamp in the mirror repo. - Common false-positive causes:
- You took a real break (no markdown edits, no Morgen edits) for >60 min, so there was nothing for W1 to commit.
- Your real cadence is slower than 60 min (e.g., W0 firing every 30 min instead of 20 min for some reason).
Fix
- Comment on the watchdog-opened GH issue:
recovered manually — no actual edits in window. - Close the issue.
- If false positives are frequent, open the Watchdog workflow in n8n, find the Code node holding the
STALE_MINUTESconstant, and bump it from60to90or120. Save and re-publish.
Why it happens
The watchdog measures "time since last [bot:W1] commit" — but if you didn't edit anything in either Morgen or Obsidian for >60 min, W1 has nothing to commit. That's a quiet system, not a broken one. STALE_MINUTES is a heuristic; tune it to your actual rhythm.
The watchdog is opening GH issues fine but the optional Telegram pings never land.
Diagnose
- Open the Watchdog workflow in n8n. Find the Telegram node.
- Check the
TELEGRAM_CHAT_IDconstant in the upstream Code node — is it set, or is it the placeholder0? - Confirm the bot token is bound (n8n credential
Telegram (task-maxxing)).
Fix
- DM your bot once from the Telegram account that should receive alerts. (The bot can't initiate conversations — Telegram requires a first inbound message.)
- Fetch your chat ID:
curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates" | jq '.result[].message.chat.id'
- Paste the chat ID into the Watchdog workflow's
TELEGRAM_CHAT_IDconstant. Save and re-publish. - Trigger the watchdog manually with a stale state to confirm a real ping arrives.
Why it happens
Telegram's bot API requires the user to message the bot first before the bot can send DMs. Skipping that step leaves you with a green-looking workflow that silently no-ops on the Telegram step.
You cloned
task-maxxingbefore 2026-05-04, originally set it up with Notion in the loop, and now you're seeing weird errors mentioning Notion / W3 /NOTION_TOKEN.
Diagnose
Check for any of these symptoms:
- A workflow named
W3(or anythingNotion-flavored) still shows in your n8n workspace. - Your n8n credentials list still has a
Notion (task-maxxing)entry. - Your
.env(or n8n env vars) still definesNOTION_TOKEN,NOTION_DATABASE_ID, or similar. - Stale execution errors from before the cutover are still in the n8n executions list.
Fix
- In n8n:
- Archive the
W3workflow (or delete it). It's a no-op stub post-cutover and serves no purpose. - Delete the
Notion (task-maxxing)credential. - Open the W0 orchestrator workflow. Confirm it only chains
W2 → W1— if there's still a Notion / W3 step, replace it with a re-imported W0 fromworkflows/in this repo.
- Archive the
- In your env:
- Remove
NOTION_TOKEN,NOTION_DATABASE_ID, and any related vars from.envand from n8n's environment settings.
- Remove
- Re-import the current workflow JSON from
workflows/W0-orchestrator.json,workflows/W1-obsidian-to-morgen.json, andworkflows/W2-morgen-to-obsidian.json(filenames may vary — check the directory). Re-bind credentials. Re-publish. - Optional: if you have lingering Notion task rows you want to archive, do it from the Notion UI directly. The kit no longer touches Notion at all, so leftover rows are inert.
A MIGRATION.md may be present in the repo root with a step-by-step walkthrough of the 3-way → 2-way cutover. If it's there, follow it for the cleanup; if not, the steps above cover it.
Why it happens
Notion was dropped from the live stack on 2026-05-04. The kit on main is two-way only, but old installs carry their original n8n state until you clean it up. The leftover Notion plumbing won't break the 2-way sync — it just clutters the n8n UI and confuses future debugging.
Open an issue at github.com/fidgetcoding/task-maxxing/issues with:
- Which entry above you tried (or "not in the runbook").
- The exact error message, copy-pasted verbatim.
- Last 20 lines of the relevant n8n execution log, redacted of tokens.
- A redacted snippet from
05-Tasks/.sync-state.jsonfor the affected task.
Most issues are fixable in one round-trip with all four.