{
  "name": "ToolTasted - Voice note transcript to reviewable tasks",
  "nodes": [
    {
      "parameters": {},
      "id": "8da98cb4-b151-4c5c-a89b-0b64756f1001",
      "name": "Manual start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8da98cb4-b151-4c5c-a89b-0b64756f1002",
              "name": "note_id",
              "value": "demo-note-v1",
              "type": "string"
            },
            {
              "id": "8da98cb4-b151-4c5c-a89b-0b64756f1003",
              "name": "transcript",
              "value": "1. Review the retry patch before merging.\n2. Run the regression tests for HTTP 401.\n3. Draft the release note after verification.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8da98cb4-b151-4c5c-a89b-0b64756f1004",
      "name": "Voice note transcript",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const inputs = $input.all();\nconst seen = new Set();\nconst tasks = [];\nfor (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) {\n  const note = inputs[inputIndex].json;\n  if (typeof note.note_id !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(note.note_id)) {\n    throw new Error(\"note_id must be 1–64 letters, digits, underscores or hyphens; start with a letter or digit.\");\n  }\n  if (seen.has(note.note_id)) throw new Error(\"Duplicate note_id in one run: \" + note.note_id);\n  seen.add(note.note_id);\n  if (typeof note.transcript !== \"string\" || !note.transcript.trim()) {\n    throw new Error(\"transcript must be a non-empty string.\");\n  }\n  if (note.transcript.length > 10000) throw new Error(\"transcript exceeds 10000 characters.\");\n  const lines = note.transcript.split(/\\r?\\n/).map((text, i) => ({text, line: i + 1})).filter(row => row.text.trim());\n  if (lines.length > 50) throw new Error(\"A note may contain at most 50 non-empty lines.\");\n  for (const row of lines) {\n    const title = row.text.trim().replace(/^(?:[-*•]\\s+|\\d+[.)]\\s+)/, \"\").trim();\n    if (!title || title.length > 500 || /^[-*•]$/.test(title) || /^\\d+[.)]$/.test(title)) {\n      throw new Error(\"Line \" + row.line + \" must contain a task title of 1–500 characters.\");\n    }\n    tasks.push({\n      json: {\n        task_key: note.note_id + \":\" + row.line,\n        title,\n        owner: null,\n        due_date: null,\n        status: \"needs_review\",\n        source_note_id: note.note_id,\n        source_line: row.line,\n        source_text: row.text\n      },\n      pairedItem: {item: inputIndex}\n    });\n  }\n}\nreturn tasks;\n"
      },
      "id": "8da98cb4-b151-4c5c-a89b-0b64756f1005",
      "name": "Parse tasks",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        0
      ]
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Voice note transcript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Voice note transcript": {
      "main": [
        [
          {
            "node": "Parse tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "pinData": {}
}
