Analizar POML
Este es unEngineering, Multimodal AIflujo de automatización del dominio deautomatización que contiene 10 nodos.Utiliza principalmente nodos como Set, Code, ManualTrigger, Agent, LmChatOpenAi. Convertir POML a prompts listos para IA y mensajes de chat sin dependencias
- •Clave de API de OpenAI
Nodos utilizados (10)
Categoría
{
"id": "",
"meta": {
"instanceId": "",
"templateCredsSetupCompleted": false
},
"name": "Parse POML",
"tags": [
{
"id": "ahEMnscqU2YYEOGK",
"name": "Developer",
"createdAt": "2025-08-19T16:32:11.755Z",
"updatedAt": "2025-08-19T16:32:11.755Z"
},
{
"id": "KvVY10Qqey3YJmAq",
"name": "Real Simple Solutions",
"createdAt": "2024-12-16T19:05:05.815Z",
"updatedAt": "2024-12-16T19:05:05.815Z"
}
],
"nodes": [
{
"id": "e67f2a4a-df0c-4b7f-b267-f831e117d74d",
"name": "Set_Variables",
"type": "n8n-nodes-base.set",
"position": [
256,
176
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e268b89e-df54-471f-bf56-b2d09d36e79c",
"name": "poml",
"type": "string",
"value": "<poml> <task>You are given various potential options or approaches for a project. Convert these into a well-structured research plan.</task> <stepwise-instructions> <list listStyle=\"decimal\"> <item>Identifies Key Objectives <list listStyle=\"dash\"> <item>Clarify what questions each option aims to answer</item> <item>Detail the data/info needed for evaluation</item> </list> </item> <item>Describes Research Methods <list listStyle=\"dash\"> <item>Outline how you’ll gather and analyze data</item> <item>Mention tools or methodologies for each approach</item> </list> </item> <item>Provides Evaluation Criteria <list listStyle=\"dash\"> <item>Metrics, benchmarks, or qualitative factors to compare options </item> <item>Criteria for success or viability</item> </list> </item> <item>Specifies Expected Outcomes <list listStyle=\"dash\"> <item>Possible findings or results </item> <item>Next steps or actions following the research</item> </list> </item> </list> Produce a methodical plan focusing on clear, practical steps. </stepwise-instructions> </poml>"
},
{
"id": "98ac0a36-75ae-485b-896e-d6658810d293",
"name": "context",
"type": "object",
"value": "{ \"context\": { \"project\": { \"name\": \"Discovery Framework\" }, \"audience\": \"internal research team\", \"timeframe\": \"10 business days\", \"methods\": { \"csv\": \"Literature review, SME interviews, Data scraping\" }, \"success\": \"Clear comparison matrix, ranked recommendation, and next steps.\" } }"
},
{
"id": "9fbc3de4-2a0b-4c83-a997-02a428cf2bb6",
"name": "speakerMode",
"type": "boolean",
"value": true
},
{
"id": "a55e0e74-be4a-460b-8088-4834fc4ba7bf",
"name": "listStyle",
"type": "string",
"value": "dash"
},
{
"id": "21b92729-99b1-4ed1-83c3-3f12eaae1be6",
"name": "componentSpec",
"type": "object",
"value": "{ \"components\": { \"ai-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Audio\": [\"src\", \"base64\", \"alt\", \"type\", \"position\", \"syntax\"], \"audio\": [\"src\", \"base64\", \"alt\", \"type\", \"position\", \"syntax\"], \"b\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"br\": [\"newLineCount\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"code\": [\"inline\", \"lang\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"conversation\": [\"messages\", \"selectedMessages\"], \"cp\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"dataObj\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"Document\": [\"src\", \"buffer\", \"base64\", \"parser\", \"multimedia\", \"selectedPages\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"example\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"chat\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"examples\": [\"caption\", \"captionSerialized\", \"chat\", \"introducer\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"folder\": [\"syntax\", \"src\", \"data\", \"filter\", \"maxDepth\", \"showContent\"], \"h\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Header\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"hint\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"i\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"Image\": [\"src\", \"alt\", \"base64\", \"type\", \"position\", \"maxWidth\", \"maxHeight\", \"resize\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"img\": [\"src\", \"alt\", \"base64\", \"type\", \"position\", \"maxWidth\", \"maxHeight\", \"resize\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"input\": [\"caption\", \"captionSerialized\", \"speaker\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"name\", \"type\", \"writerOptions\"], \"introducer\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"item\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"list\": [\"listStyle\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"msg-content\": [\"content\"], \"obj\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"Object\": [\"syntax\", \"data\", \"className\", \"speaker\", \"writerOptions\"], \"output\": [\"caption\", \"captionSerialized\", \"speaker\", \"captionStyle\", \"captionTextTransform\", \"captionColon\", \"blankLine\", \"syntax\", \"className\", \"name\", \"type\", \"writerOptions\"], \"output-format\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"p\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"poml\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"qa\": [\"questionCaption\", \"answerCaption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"role\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"s\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"section\": [\"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"span\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"stepwise-instructions\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"strike\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"system-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"table\": [\"syntax\", \"records\", \"columns\", \"src\", \"parser\", \"selectedColumns\", \"selectedRecords\", \"maxRecords\", \"maxColumns\", \"className\", \"speaker\", \"writerOptions\"], \"task\": [\"caption\", \"captionSerialized\", \"captionStyle\", \"captionTextTransform\", \"captionEnding\", \"blankLine\", \"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"text\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"Tree\": [\"syntax\", \"items\", \"showContent\"], \"u\": [\"syntax\", \"className\", \"speaker\", \"writerOptions\"], \"user-msg\": [\"syntax\", \"className\", \"speaker\", \"name\", \"type\", \"writerOptions\"], \"webpage\": [\"url\", \"src\", \"buffer\", \"base64\", \"extractText\", \"selector\", \"syntax\", \"className\", \"speaker\", \"writerOptions\"] } } "
},
{
"id": "45d939a2-4aef-4a53-a090-fe61c99c5bf7",
"name": "attributeSpec",
"type": "array",
"value": "[ { \"attribute\": \"original-end-index\", \"applies_to\": [\"*\"], \"type\": \"integer\", \"description\": \"The end offset of the element corresponding to the current one in the original document\" }, { \"attribute\": \"original-start-index\", \"applies_to\": [\"*\"], \"type\": \"integer\", \"description\": \"The start offset of the element corresponding to the current one in the original document\" }, { \"attribute\": \"speaker\", \"applies_to\": [\"*\"], \"type\": \"ai/human/system\", \"description\": \"The speaker of the current content\" }, { \"attribute\": \"name\", \"applies_to\": [\"any\"], \"type\": \"string\", \"description\": \"An optional identifier for the data.\" }, { \"attribute\": \"type\", \"applies_to\": [\"any\"], \"type\": \"string\", \"description\": \"The data type of the value ('string', 'intege...lean', 'array', 'object', 'buffer', 'null', or 'undefined').\" }, { \"attribute\": \"blank-line\", \"applies_to\": [\"code\"], \"type\": \"boolean\", \"description\": \"Inserts a blank line before and after the code block if inline = false.\" }, { \"attribute\": \"inline\", \"applies_to\": [\"code\"], \"type\": \"boolean\", \"description\": \"Indicates whether the code is inline (true) or a block element (false).\" }, { \"attribute\": \"lang\", \"applies_to\": [\"code\"], \"type\": \"string\", \"description\": \"Specifies the programming language or syntax highlighting mode.\" }, { \"attribute\": \"markup-lang\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The specific markup language, required only if presentation = 'markup'.\" }, { \"attribute\": \"presentation\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The output style or format mode ('markup', 'serialize', 'free', or 'multimedia').\" }, { \"attribute\": \"serializer\", \"applies_to\": [\"env\"], \"type\": \"string\", \"description\": \"The name of the serializer, required only if presentation = 'serialize'.\" }, { \"attribute\": \"writer-options\", \"applies_to\": [\"env\"], \"type\": \"object\", \"description\": \"Optional parameters passed to the writer constructor for customizing output.\" }, { \"attribute\": \"level\", \"applies_to\": [\"h\"], \"type\": \"integer\", \"description\": \"Indicates the heading level. Typically ranges from 1 (highest level) to 6 (lowest level).\" }, { \"attribute\": \"alt\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"Alternative text describing the image.\" }, { \"attribute\": \"base64\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The base64-encoded image data.\" }, { \"attribute\": \"position\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The placement of the image relative to text, such as 'here', 'top', or 'bottom'.\" }, { \"attribute\": \"type\", \"applies_to\": [\"img\"], \"type\": \"string\", \"description\": \"The image MIME type (e.g., 'image/jpeg', 'image/png').\" }, { \"attribute\": \"list-style\", \"applies_to\": [\"list\"], \"type\": \"string\", \"description\": \"The style of the list bullets or enumeration (e.g., 'star', 'dash', 'decimal').\" }, { \"attribute\": \"count\", \"applies_to\": [\"nl\"], \"type\": \"integer\", \"description\": \"Specifies how many newline characters to insert.\" }, { \"attribute\": \"data\", \"applies_to\": [\"obj\"], \"type\": \"object\", \"description\": \"A valid JSON object containing the structured data.\" }, { \"attribute\": \"blank-line\", \"applies_to\": [\"p\"], \"type\": \"boolean\", \"description\": \"Inserts a blank line before and after the paragraph when true.\" } ] "
}
]
}
},
"typeVersion": 3.4
},
{
"id": "be281179-0d5e-4937-bee1-b9bcfa09c4b7",
"name": "Parse_POML",
"type": "n8n-nodes-base.code",
"position": [
576,
176
],
"parameters": {
"jsCode": "// n8n Code node (Function) — POML → Prompt/Messages (zero-deps, schema-driven)\n//\n// Supported tags (subset):\n// <poml|text|p>, <h level=N>, <b>/<strong>, <i>/<em>,\n// <list list-style=...> <item>, <code inline|lang|blank-line>,\n// <img alt|src|base64|type>, <audio alt|src>, <br newLineCount=N>,\n// <table records|columns>, <Conversation>, <SystemMessage>/<HumanMessage>/<AiMessage>,\n// <system-msg>/<user-msg>/<ai-msg>\n// Unknown tags render their children.\n//\n// How to pass your JSON specs:\n// - Set node A → set \"componentSpec\" = (your components JSON object)\n// - Set node B → set \"attributeSpec\" = (your attributes JSON object or array)\n// This node will auto-detect common shapes.\n\nfunction get(obj, path, fallback='') {\n if (!path) return fallback;\n const segs = String(path).split('.').map(s => s.trim()).filter(Boolean);\n let cur = obj;\n for (const s of segs) {\n if (cur && Object.prototype.hasOwnProperty.call(cur, s)) cur = cur[s];\n else return fallback;\n }\n return (cur == null ? fallback : cur);\n}\n\nfunction substitute(str, ctx) {\n if (!str) return '';\n return String(str).replace(/\\{\\{\\s*([a-zA-Z0-9_.$[\\]-]+)\\s*\\}\\}/g, (_, p1) => {\n const v = get(ctx, p1, '');\n return (v == null ? '' : String(v));\n });\n}\n\n// Minimal XML-ish tokenizer → AST\nfunction parsePoml(input) {\n const src = String(input);\n const root = { type: 'element', name: 'root', attrs: {}, children: [] };\n const stack = [root];\n let i = 0;\n const attrRe = /([:@a-zA-Z_][\\w:.-]*)\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'>/]+))/g;\n\n while (i < src.length) {\n const lt = src.indexOf('<', i);\n if (lt < 0) { const text = src.slice(i); if (text) stack[stack.length-1].children.push({ type:'text', text }); break; }\n if (lt > i) { const text = src.slice(i, lt); if (text) stack[stack.length-1].children.push({ type:'text', text }); i = lt; }\n\n if (src.startsWith('<!--', i)) { const end = src.indexOf('-->', i+4); i = (end >= 0 ? end+3 : src.length); continue; }\n\n const closeMatch = src.slice(i).match(/^<\\/\\s*([a-zA-Z_][\\w:.-]*)\\s*>/);\n if (closeMatch) {\n const tag = closeMatch[1].toLowerCase();\n for (let j = stack.length - 1; j >= 0; j--) {\n if (stack[j].name.toLowerCase() === tag) { stack.length = j; break; }\n }\n i += closeMatch[0].length;\n continue;\n }\n\n const openMatch = src.slice(i).match(/^<\\s*([a-zA-Z_][\\w:.-]*)([^>]*)>/);\n if (openMatch) {\n const tag = openMatch[1];\n let rest = openMatch[2] || '';\n const selfClosing = /\\/\\s*$/.test(rest);\n if (selfClosing) rest = rest.replace(/\\/\\s*$/, '');\n const attrs = {};\n attrRe.lastIndex = 0;\n let m;\n while ((m = attrRe.exec(rest))) {\n const key = m[1];\n const val = m[3] ?? m[4] ?? m[5] ?? '';\n attrs[key] = val;\n }\n const el = { type:'element', name: tag, attrs, children: [] };\n stack[stack.length-1].children.push(el);\n i += openMatch[0].length;\n if (!selfClosing) stack.push(el);\n continue;\n }\n\n // literal '<'\n stack[stack.length-1].children.push({ type:'text', text:'<' });\n i += 1;\n }\n return root.children;\n}\n\n// Spec normalization — accepts multiple shapes\nfunction normalizeComponentSpec(spec) {\n const map = {};\n if (!spec) return map;\n // shape A: { components: { tag: [attrs...] } }\n if (spec.components && typeof spec.components === 'object') {\n for (const [k, v] of Object.entries(spec.components)) map[k.toLowerCase()] = new Set((v||[]).map(a => String(a).toLowerCase()));\n return map;\n }\n // shape B: { tag: [attrs...] }\n for (const [k, v] of Object.entries(spec)) {\n if (Array.isArray(v)) map[k.toLowerCase()] = new Set(v.map(a => String(a).toLowerCase()));\n }\n return map;\n}\n\nfunction normalizeAttributeSpec(spec) {\n // return { attrNameLower: { type: 'boolean'|'integer'|'string'|..., ... } }\n const out = {};\n if (!spec) return out;\n if (Array.isArray(spec)) {\n for (const a of spec) {\n const name = (a && (a.attribute || a.name || a.key)) ? String(a.attribute || a.name || a.key).toLowerCase() : null;\n if (name) out[name] = a;\n }\n return out;\n }\n if (typeof spec === 'object') {\n for (const [k, v] of Object.entries(spec)) out[String(k).toLowerCase()] = (typeof v === 'object' ? v : { type: String(v) });\n }\n return out;\n}\n\n// hyphen/camel resolver with type coercion\nfunction readAttr(attrs, name, attrSpecItem, fallback) {\n const keys = [name, name.replace(/-([a-z])/g, (_,c)=>c.toUpperCase()), name.replace(/[A-Z]/g, c=>'-'+c.toLowerCase())];\n let raw; for (const k of keys) if (attrs[k] !== undefined) { raw = attrs[k]; break; }\n if (raw === undefined) return fallback;\n\n const t = (attrSpecItem && attrSpecItem.type || '').toLowerCase();\n if (t === 'boolean') return /^(true|1|yes)$/i.test(String(raw));\n if (t === 'integer' || t === 'number') { const n = Number.parseInt(raw,10); return Number.isFinite(n) ? n : fallback; }\n if (t === 'ai/human/system') return String(raw).toLowerCase();\n // object/array as JSON\n if (t === 'object' || t === 'array') {\n if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return fallback; } }\n return raw;\n }\n return raw; // string / enum / unknown\n}\n\nconst md = {\n fence(code, lang='', addBlank=false){return `${addBlank?'\\n':''}\\`\\`\\`${lang}\\n${String(code).replace(/\\s+$/,'')}\\n\\`\\`\\`${addBlank?'\\n':''}`;},\n};\n\nconst trimLines = (s) => String(s).replace(/[ \\t]+\\n/g, '\\n');\n\nfunction compileNodes(nodes, ctx, roleCtx, options) {\n let out = '';\n for (const n of nodes) out += compileNode(n, ctx, roleCtx, options);\n return out;\n}\n\nfunction compileNode(node, ctx, roleCtx, options) {\n if (node.type === 'text') return substitute(node.text, ctx);\n\n const name = node.name.toLowerCase();\n const attrs = node.attrs || {};\n const attrSpec = options.attrSpec || {};\n const componentMap = options.componentMap || {};\n\n // (Optional) component attribute whitelist (non-fatal)\n const allow = componentMap[name];\n if (allow) {\n for (const k of Object.keys(attrs)) {\n if (!allow.has(String(k).toLowerCase())) {\n // drop unknown attrs silently (or collect warnings in options.warnings)\n // delete attrs[k];\n }\n }\n }\n\n // speaker override\n const speakerAttr = (readAttr(attrs, 'speaker', attrSpec['speaker'] || { type:'ai/human/system' }, '') || '').toLowerCase();\n let nextRoleCtx = roleCtx;\n if (speakerAttr === 'system') nextRoleCtx = 'system';\n if (speakerAttr === 'human') nextRoleCtx = 'user';\n if (speakerAttr === 'ai') nextRoleCtx = 'assistant';\n\n switch (name) {\n case 'poml':\n case 'text':\n case 'p':\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n\n case 'h':\n case 'header': {\n let level = readAttr(attrs, 'level', attrSpec['level'] || { type:'integer' }, 1);\n level = Math.min(6, Math.max(1, level || 1));\n const inner = trimLines(compileNodes(node.children, ctx, nextRoleCtx, options)).trim();\n return `\\n\\n${'#'.repeat(level)} ${inner}\\n\\n`;\n }\n\n case 'b':\n case 'bold':\n case 'strong': {\n const inner = compileNodes(node.children, ctx, nextRoleCtx, options).trim();\n return `**${inner}**`;\n }\n\n case 'i':\n case 'italic':\n case 'em': {\n const inner = compileNodes(node.children, ctx, nextRoleCtx, options).trim();\n return `_${inner}_`;\n }\n\n case 'list':\n case 'ul': {\n const style = readAttr(attrs, 'list-style', attrSpec['list-style'] || { type:'string' }, options.listStyle || 'dash')\n || readAttr(attrs, 'listStyle', attrSpec['list-style'] || { type:'string' }, options.listStyle || 'dash');\n const bullet = style==='decimal' ? '1.' : style==='star' ? '*' : style==='plus' ? '+' : style==='latin' ? 'a.' : '-';\n const lines = [];\n for (const ch of node.children) {\n if (ch.type === 'element' && (ch.name.toLowerCase() === 'item' || ch.name.toLowerCase() === 'li')) {\n const t = trimLines(compileNodes(ch.children, ctx, nextRoleCtx, options)).replace(/\\n+/g, ' ').trim();\n if (t) lines.push(`${bullet} ${t}`);\n }\n }\n return `\\n${lines.join('\\n')}\\n`;\n }\n\n case 'item':\n case 'li': {\n const t = trimLines(compileNodes(node.children, ctx, nextRoleCtx, options)).replace(/\\n+/g, ' ').trim();\n return `- ${t}\\n`;\n }\n\n case 'code': {\n const inline = readAttr(attrs, 'inline', attrSpec['inline'] || { type:'boolean' }, true);\n const lang = readAttr(attrs, 'lang', attrSpec['lang'] || { type:'string' }, '');\n const blankA = readAttr(attrs, 'blank-line', attrSpec['blank-line'] || { type:'boolean' }, false);\n const blankB = readAttr(attrs, 'blankLine', attrSpec['blank-line'] || { type:'boolean' }, false);\n const addBlank = Boolean(blankA || blankB);\n const body = compileNodes(node.children, ctx, nextRoleCtx, options);\n return inline ? '`' + body.replace(/`/g, '\\\\`') + '`' : md.fence(body, lang, addBlank);\n }\n\n case 'img':\n case 'image': {\n const alt = readAttr(attrs, 'alt', attrSpec['alt'] || { type:'string' }, '');\n let src = readAttr(attrs, 'src', attrSpec['src'] || { type:'string' }, '');\n const b64 = readAttr(attrs, 'base64', attrSpec['base64'] || { type:'string' }, '');\n const mime = readAttr(attrs, 'type', attrSpec['type'] || { type:'string' }, 'application/octet-stream');\n if (!src && b64) src = `data:${mime};base64,${b64}`;\n return src ? `\\n\\n\\n\\n` : (alt ? `\\n\\n${alt}\\n\\n` : '');\n }\n\n case 'audio': {\n const alt = readAttr(attrs, 'alt', attrSpec['alt'] || { type:'string' }, '');\n const src = readAttr(attrs, 'src', attrSpec['src'] || { type:'string' }, '');\n return `\\n\\n[Audio: ${alt || src || 'audio'}]\\n\\n`;\n }\n\n case 'br': {\n const n = Math.max(1, readAttr(attrs, 'newLineCount', attrSpec['newLineCount'] || { type:'integer' }, 1));\n return '\\n'.repeat(n);\n }\n\n case 'table': {\n let records = readAttr(attrs, 'records', attrSpec['records'] || { type:'object' }, null);\n if (typeof records === 'string') { try { records = JSON.parse(records); } catch {} }\n const columns = readAttr(attrs, 'columns', attrSpec['columns'] || { type:'object' }, null);\n if (!Array.isArray(records) || records.length === 0) return '';\n let headers;\n if (Array.isArray(columns) && columns.length) headers = columns.map(String);\n else if (typeof records[0] === 'object' && !Array.isArray(records[0])) headers = Object.keys(records[0]);\n else headers = records[0].map((_,i)=>`col${i+1}`);\n const rows = records.map(r => Array.isArray(r) ? r : headers.map(h => r[h]));\n const head = `| ${headers.join(' | ')} |`;\n const sep = `| ${headers.map(()=> '---').join(' | ')} |`;\n const body = rows.map(row => `| ${row.map(v => String(v ?? '')).join(' | ')} |`).join('\\n');\n return `\\n${head}\\n${sep}\\n${body}\\n`;\n }\n\n // Message components (speakerMode aware)\n case 'systemmessage':\n case 'humanmessage':\n case 'aimessage': {\n const role = name === 'systemmessage' ? 'system' : name === 'humanmessage' ? 'user' : 'assistant';\n const txt = trimLines(compileNodes(node.children, ctx, role, options)).trim();\n if (options.speakerMode) { options.messages.push({ role, content: txt }); return ''; }\n return `\\n\\n**${role.toUpperCase()}:**\\n${txt}\\n`;\n }\n\n case 'system-msg':\n case 'user-msg':\n case 'ai-msg': {\n const role = name === 'system-msg' ? 'system' : name === 'user-msg' ? 'user' : 'assistant';\n const txt = trimLines(compileNodes(node.children, ctx, role, options)).trim();\n if (options.speakerMode) { options.messages.push({ role, content: txt }); return ''; }\n return `\\n\\n**${role.toUpperCase()}:**\\n${txt}\\n`;\n }\n\n case 'conversation':\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n\n default:\n // Unknown tag → render children\n return trimLines(compileNodes(node.children, ctx, nextRoleCtx, options));\n }\n}\n\n// ---- main per-item loop\nconst items = $input.all();\nconst out = [];\n\nfor (const item of items) {\n try {\n const i = item.json || {};\n const poml = i.poml;\n if (typeof poml !== 'string' || !poml.trim()) {\n out.push({ json: { error: 'json.poml (string) is required' } });\n continue;\n }\n\n const componentMap = normalizeComponentSpec(i.componentSpec);\n const attrSpec = normalizeAttributeSpec(i.attributeSpec);\n\n const ctx = i.context || {};\n const ast = parsePoml(poml);\n const options = {\n listStyle: i.listStyle || 'dash',\n speakerMode: Boolean(i.speakerMode),\n messages: [],\n componentMap,\n attrSpec,\n };\n\n if (options.speakerMode) {\n compileNodes(ast, ctx, 'system', options); // messages collected\n if (options.messages.length === 0) {\n const txt = trimLines(compileNodes(ast, ctx, 'user', { ...options, speakerMode: false })).trim();\n if (txt) options.messages.push({ role: 'user', content: txt });\n }\n const prompt = options.messages.map(m => `<<${m.role.toUpperCase()}>>\\n${m.content}`).join('\\n\\n');\n out.push({ json: { prompt, messages: options.messages } });\n } else {\n const prompt = trimLines(compileNodes(ast, ctx, 'system', options)).trim();\n out.push({ json: { prompt } });\n }\n } catch (err) {\n out.push({ json: { error: err?.message || String(err) } });\n }\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "8680ce53-e4da-475d-b694-cc2ebb235369",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
784,
176
],
"parameters": {
"text": "={{ $json.prompt }}",
"options": {},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "c78e40de-b82a-43a9-8557-15262c79c74c",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
656,
384
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "0NBgNTGVJWuQAji0",
"name": "SNPT - OpenAi account 3"
}
},
"typeVersion": 1.2
},
{
"id": "21df62e0-4746-405e-bd1f-2410e25adb17",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
-768
],
"parameters": {
"color": 7,
"width": 768,
"height": 864,
"content": "# **README**\n### _POML → Prompt/Messages (No-Deps)_\n\n## What this does\n\nTurns **POML** markup into either a single Markdown **prompt** or chat-style **messages\\[]** — using a **zero-dependency** n8n Code node. It supports variable substitution (via `context`), basic components (headings, lists, code, images, tables, line breaks), and optional schema-driven validation using `componentSpec` + `attributeSpec`.\n\n## When to use\n\n* You need a **compile step** that converts structured authoring (POML) into model-ready text.\n* You can’t rely on external npm modules in templates.\n\n---\n\n## Inputs (per item)\n\n* `poml` *(string, required)* – the POML markup.\n* `context` *(object, optional)* – values for `{{dot.path}}` substitution (objects/arrays OK).\n* `speakerMode` *(boolean, optional)* – `true` → emits `messages[]` (`system|user|assistant`).\n* `listStyle` *(string, optional)* – one of `dash | star | plus | decimal | latin`.\n* `componentSpec` *(object, optional)* – map of **tag → allowed attributes** (or `{ components:{…} }`).\n* `attributeSpec` *(object|array, optional)* – attribute typings (boolean/integer/string/object, enums, etc).\n\n> Tip: Tags/attributes are **case-insensitive**; both `listStyle` and `list-style` work.\n> Unknown tags still render their children; the specs only guide validation/coercion.\n\n## Outputs (per item)\n\n* `prompt` *(string)* – compiled Markdown.\n* `messages` *(array, present when `speakerMode: true`)* – e.g. `[{ role:\"user\", content:\"…\" }]`.\n"
},
"typeVersion": 1
},
{
"id": "b0937fbd-1741-4582-8048-fee463d00fa9",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-768
],
"parameters": {
"color": 7,
"width": 784,
"height": 864,
"content": "## Quick start (copy-paste)\n\nFill in the **Set** `[Set_Variables]` node before the Code node and drop in:\n\n**Example `context`** (edit to taste)\n\n```json\n{\n \"project\": { \"name\": \"Discovery Framework\" },\n \"audience\": \"internal research team\",\n \"timeframe\": \"10 business days\",\n \"methods\": { \"csv\": \"Literature review, SME interviews, Data scraping\" },\n \"success\": \"Comparison matrix + ranked recommendation + next steps\"\n}\n```\n\n## Supported tags (subset)\n\n* Structure: `<poml>`, `<p>`/`<text>`, `<h level=\"1..6\">`\n* Emphasis: `<b>/<strong>`, `<i>/<em>`\n* Lists: `<list list-style=\"…\">` + `<item>`\n* Code: `<code inline=\"true|false\" lang=\"…\"> … </code>`\n* Media: `<img alt|src|base64|type>`, `<audio alt|src>`\n* Breaks: `<br newLineCount=\"N\" />`\n* Tables: `<table records='[ … ]' columns='[ … ]' />` (JSON strings)\n* Chat: `<system-msg>`, `<user-msg>`, `<ai-msg>` (use with `speakerMode: true`)\n\n## Behaviors & tips\n\n* **Variables:** `{{dot.path}}` substitution works in text nodes. (Attributes don’t resolve placeholders in this template; pass JSON directly for things like `<table records='…'/>`.)\n* **Speaker mode:** If no message tags are present, the entire render becomes **one `user` message**.\n* **Attribute forms:** Both `listStyle` and `list-style` are accepted.\n* **Images:** If `base64` is set and `src` is empty, a `data:` URL is emitted.\n* **Tables:** `records` can be an array of objects or arrays. `columns` is optional; defaults to object keys."
},
"typeVersion": 1
},
{
"id": "e0787480-85c0-4254-8466-aefd93db87f6",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
480
],
"parameters": {
"color": 7,
"width": 1072,
"height": 736,
"content": "## More Examples\n\n**Input item (trimmed):**\n\n```json\n{\n \"poml\": \"<poml> <task>You are given various potential options or approaches for a project. Convert these into a well-structured research plan.</task> <stepwise-instructions> <list listStyle=\\\"decimal\\\"> <item>Identifies Key Objectives <list listStyle=\\\"dash\\\"><item>Clarify what questions each option aims to answer</item><item>Detail the data/info needed for evaluation</item></list></item> … </list> Produce a methodical plan focusing on clear, practical steps. </stepwise-instructions> </poml>\",\n \"context\": {},\n \"speakerMode\": true,\n \"listStyle\": \"dash\",\n \"componentSpec\": { \"...\": \"as above\" },\n \"attributeSpec\": [ \"... as above ...\" ]\n}\n```\n\n**Output item (shape):**\n\n```json\n{\n \"prompt\": \"<<USER>>\\nYou are given various potential options or approaches for a project. Convert these into a well-structured research plan.\\n1. Identifies Key Objectives - Clarify what questions each option aims to answer - Detail the data/info needed for evaluation\\n1. Describes Research Methods …\\n Produce a methodical plan focusing on clear, practical steps.\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"You are given various potential options … steps.\" }\n ]\n}\n```\n\n> Note: For `decimal` lists, Markdown renders `1.` on each line but auto-numbers in viewers — this is intentional."
},
"typeVersion": 1
},
{
"id": "34004c9c-c711-453d-94a5-e9381204fc10",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
112
],
"parameters": {
"color": 3,
"width": 768,
"height": 352,
"content": "## Limitations (by design)\n* No external libraries (template-safe).\n* No POML file includes, scraping, or environment stylesheets.\n* Attribute-level `{{ }}` substitution is **not** enabled by default.\n* The parser is a pragmatic XML-ish tokenizer, not a full POML spec engine.\n\n## Troubleshooting\n* **“json.poml is required”** → Provide a string in the `poml` field.\n* **Got only one `user` message** → Add `<system-msg>`, `<user-msg>`, `<ai-msg>` tags **and** set `speakerMode: true`.\n* **Tables not rendering** → Ensure `records`/`columns` are **valid JSON strings** in the tag attributes.\n* **Unexpected token / syntax** → Ensure you pasted the **entire** Code node (switch/cases included), not partial snippets."
},
"typeVersion": 1
},
{
"id": "4dbba9f0-d648-4e7d-8891-21e68aaf0ed7",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
112
],
"parameters": {
"width": 288,
"height": 352,
"content": "## Credits\n\nAuthored by  [**_Real Simple Solutions_**](https://realsimple.dev) as an n8n **template-library-friendly** POML compiler (no dependencies). For full POML feature parity, Contact us for the template with the use Microsoft’s official SDK in a custom workflow.\n\n### View more of our [templates here](https://joeperes.gumroad.com/)"
},
"typeVersion": 1
},
{
"id": "afcd4577-5357-469a-b31b-f13465fea17b",
"name": "‘Execute workflow’",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-32,
176
],
"parameters": {},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "eaac3e3e-35f8-4170-83ac-752a68e3d883",
"connections": {
"be281179-0d5e-4937-bee1-b9bcfa09c4b7": {
"main": [
[
{
"node": "8680ce53-e4da-475d-b694-cc2ebb235369",
"type": "main",
"index": 0
}
]
]
},
"e67f2a4a-df0c-4b7f-b267-f831e117d74d": {
"main": [
[
{
"node": "be281179-0d5e-4937-bee1-b9bcfa09c4b7",
"type": "main",
"index": 0
}
]
]
},
"c78e40de-b82a-43a9-8557-15262c79c74c": {
"ai_languageModel": [
[
{
"node": "8680ce53-e4da-475d-b694-cc2ebb235369",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"afcd4577-5357-469a-b31b-f13465fea17b": {
"main": [
[
{
"node": "e67f2a4a-df0c-4b7f-b267-f831e117d74d",
"type": "main",
"index": 0
}
]
]
}
}
}¿Cómo usar este flujo de trabajo?
Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.
¿En qué escenarios es adecuado este flujo de trabajo?
Intermedio - Ingeniería, IA Multimodal
¿Es de pago?
Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.
Flujos de trabajo relacionados recomendados
RealSimple Solutions
@joeperesCompartir este flujo de trabajo