{
  "name": "Journal entry → Obsidian daily note",
  "nodes": [
    {
      "parameters": {
        "content": "## Journal entry → Obsidian daily note\n\nReceives an entry as JSON (`id`, `title`, `text`, `createdAt`) and appends it to the daily note of the day it was written.\n\n**Setup**\n1. Mount your vault into n8n, e.g. `/files/vault`.\n2. Set `DAILY_NOTES_DIR` and `HEADING` in **Prepare entry**.\n3. Create a Header Auth credential (`Authorization` / `Bearer <your token>`) and select it in **Entry webhook**.\n4. Activate, then add the production URL and the token as a text endpoint in the app.\n\nSending the same entry twice does not add it twice.",
        "height": 380,
        "width": 420
      },
      "id": "note",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-1020, -140]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "journal-entry",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Entry webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-520, 0]
    },
    {
      "parameters": {
        "jsCode": "// Folder of your daily notes as n8n sees it, and the heading entries go under.\nconst DAILY_NOTES_DIR = '/files/vault/Daily';\nconst HEADING = '## Journal';\n\nconst body = $json.body ?? {};\nconst clean = (value, max = 200000) => String(value ?? '').trim().slice(0, max);\n\nconst text = clean(body.text);\nif (!text) return [{ json: { valid: false, message: 'The field \"text\" is missing or empty.' } }];\n\nconst title = clean(body.title, 200).replace(/\\s+/g, ' ');\nconst id = clean(body.id, 36).toLowerCase().replace(/[^0-9a-f-]/g, '');\n\n// The day and time the entry was written, in the workflow's timezone.\nconst parsed = DateTime.fromISO(clean(body.createdAt, 100));\nconst written = (parsed.isValid ? parsed : $now).setZone($now.zoneName);\n\n// The marker is an HTML comment: invisible in Obsidian's reading view, and it\n// lets the workflow recognise an entry it has already written.\nconst marker = id ? `<!-- entry:${id} -->` : '';\nconst heading = `### ${written.toFormat('HH:mm')}${title ? ` · ${title}` : ''}`;\nconst block = [heading, ...(marker ? [marker] : []), '', text].join('\\n');\n\nconst fileName = `${written.toFormat('yyyy-MM-dd')}.md`;\nreturn [{ json: { valid: true, heading: HEADING, marker, block, fileName, filePath: `${DAILY_NOTES_DIR}/${fileName}` } }];"
      },
      "id": "prepare",
      "name": "Prepare entry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-280, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            {
              "id": "valid",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "has-text",
      "name": "Has text?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [-40, 0]
    },
    {
      "parameters": {
        "fileSelector": "={{ $json.filePath }}",
        "options": { "dataPropertyName": "data" }
      },
      "id": "read",
      "name": "Read daily note",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [200, -100],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const entry = $('Prepare entry').first().json;\n\n// A missing daily note is fine: the entry then starts a new one.\nlet existing = '';\nif ($input.first()?.binary?.data) {\n  existing = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8');\n}\n\nif (entry.marker && existing.includes(entry.marker)) {\n  return [{ json: { changed: false, fileName: entry.fileName } }];\n}\n\nlet note = existing.replace(/\\s+$/, '');\nconst hasHeading = note.split('\\n').some(line => line.trim() === entry.heading);\nif (entry.heading && !hasHeading) note += `${note ? '\\n\\n' : ''}${entry.heading}`;\nnote += `${note ? '\\n\\n' : ''}${entry.block}\\n`;\n\nreturn [{ json: { changed: true, markdown: note, fileName: entry.fileName, filePath: entry.filePath } }];"
      },
      "id": "append",
      "name": "Append entry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [440, -100]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            {
              "id": "changed",
              "leftValue": "={{ $json.changed }}",
              "rightValue": "",
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "is-new",
      "name": "New entry?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [680, -100]
    },
    {
      "parameters": {
        "operation": "toText",
        "sourceProperty": "markdown",
        "binaryPropertyName": "data",
        "options": { "encoding": "utf8", "fileName": "={{ $json.fileName }}" }
      },
      "id": "file",
      "name": "Build Markdown file",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [920, -200]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "={{ $('Append entry').item.json.filePath }}",
        "dataPropertyName": "data",
        "options": {}
      },
      "id": "write",
      "name": "Write daily note",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [1160, -200]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":true,\"file\":{{ JSON.stringify($('Append entry').item.json.fileName) }}}",
        "options": { "responseCode": 201 }
      },
      "id": "saved",
      "name": "201 saved",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [1400, -200]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":true,\"duplicate\":true,\"file\":{{ JSON.stringify($json.fileName) }}}",
        "options": { "responseCode": 200 }
      },
      "id": "duplicate",
      "name": "200 already there",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [920, 0]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":false,\"message\":{{ JSON.stringify($json.message) }}}",
        "options": { "responseCode": 400 }
      },
      "id": "reject",
      "name": "400 no text",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [200, 120]
    }
  ],
  "connections": {
    "Entry webhook": { "main": [[{ "node": "Prepare entry", "type": "main", "index": 0 }]] },
    "Prepare entry": { "main": [[{ "node": "Has text?", "type": "main", "index": 0 }]] },
    "Has text?": {
      "main": [
        [{ "node": "Read daily note", "type": "main", "index": 0 }],
        [{ "node": "400 no text", "type": "main", "index": 0 }]
      ]
    },
    "Read daily note": { "main": [[{ "node": "Append entry", "type": "main", "index": 0 }]] },
    "Append entry": { "main": [[{ "node": "New entry?", "type": "main", "index": 0 }]] },
    "New entry?": {
      "main": [
        [{ "node": "Build Markdown file", "type": "main", "index": 0 }],
        [{ "node": "200 already there", "type": "main", "index": 0 }]
      ]
    },
    "Build Markdown file": { "main": [[{ "node": "Write daily note", "type": "main", "index": 0 }]] },
    "Write daily note": { "main": [[{ "node": "201 saved", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
