AutomationFlowsAI & RAG › Power BI Data Analyst AI Agent

Power BI Data Analyst AI Agent

Original n8n title: Power Bi Data Analyst Agent

Power BI Data Analyst Agent. Uses stickyNote, chatTrigger, agent, lmChatOpenRouter. Chat trigger; 12 nodes.

Chat trigger trigger★★★☆☆ complexityAI-powered12 nodesChat TriggerAgentOpenRouter ChatMemory Postgres ChatHTTP Request ToolTool WorkflowTool Code
AI & RAG Trigger: Chat trigger Nodes: 12 Complexity: ★★★☆☆ AI nodes: yes Added:

This workflow follows the Agent → Chat Trigger recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Power BI Data Analyst Agent",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## Power BI Data Analyst Agent\n**What it does:** Natural-language analytics over a Power BI semantic model. Agent writes DAX, hits the REST `executeQueries` API, builds charts.\n\n### Required credentials\n- **Microsoft OAuth2 API** (Service Principal, client_credentials grant, scope: `https://analysis.windows.net/powerbi/api/.default`) \u2014 attach to `list_tables_and_measures`, `get_table_columns`, `refresh_dataset`, AND inside the DAX sub-workflow's `Run DAX via REST API` node.\n- **OpenRouter API** \u2014 attach to `OpenRouter Chat Model`.\n- **Postgres (writable)** \u2014 attach to `Postgres Chat Memory` (stores conversations in `n8n_chat_histories`).\n\n### Before activating\n1. Register an Azure AD app and grant Power BI tenant access \u2014 see `docs/AZURE_AD_SETUP.md`.\n2. Add the Service Principal as Member/Contributor on the Power BI workspace.\n3. Open the **Config** node and paste your `group_id` (workspace GUID) + `dataset_id` (dataset GUID).\n4. Re-bind `run_dax` and `create_chart` tools to the imported sub-workflows (IDs differ per instance).\n\n### Demo prompts\n- *What tables and measures does this model have?*\n- *Show me total sales by product category as a bar chart*\n- *Refresh the dataset*",
        "height": 540,
        "width": 680,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -680,
        -580
      ],
      "id": "sticky-pbi-readme",
      "name": "README"
    },
    {
      "parameters": {
        "public": true,
        "mode": "hostedChat",
        "initialMessages": "Hi! I'm your Power BI data analyst.\nAsk me about your semantic model \u2014 I can explore tables/measures, run DAX, build charts, and refresh the dataset.",
        "options": {
          "title": "Power BI Data Analyst",
          "subtitle": "Ask questions about your Power BI model \u2014 get answers and charts.",
          "responseMode": "lastNode"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.1,
      "position": [
        -680,
        0
      ],
      "id": "pbi-chat-trigger",
      "name": "When chat message received"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "gid",
              "name": "group_id",
              "value": "REPLACE_WITH_YOUR_WORKSPACE_GUID",
              "type": "string"
            },
            {
              "id": "did",
              "name": "dataset_id",
              "value": "REPLACE_WITH_YOUR_DATASET_GUID",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -460,
        0
      ],
      "id": "pbi-config",
      "name": "Config"
    },
    {
      "parameters": {
        "promptType": "auto",
        "options": {
          "maxIterations": 12,
          "systemMessage": "You are a senior data analyst working over a Power BI semantic model. Your query language is DAX, not SQL. Your job is to turn user questions into DAX, execute it, and return decision-ready insights with charts.\n\n## Available tools\n- `list_tables_and_measures()` \u2014 returns tables and measures defined in the model. Call once per conversation to discover what's available.\n- `get_table_columns(table_name)` \u2014 columns + data types for ONE table. Call before writing DAX against a table you haven't inspected.\n- `run_dax(query)` \u2014 executes a DAX query and returns up to 200 rows with a `truncated` flag. Column names are flattened (no `[Table].[Column]` brackets).\n- `create_chart(chart_type, title, labels, series, x_label?, y_label?)` \u2014 turn results into a PNG chart. See chart rules below.\n- `refresh_dataset()` \u2014 triggers a dataset refresh. Call ONLY when the user explicitly asks to refresh.\n- `think(thought)` \u2014 scratchpad for planning or post-error reasoning.\n\n## DAX primer (you write these)\nEvery query starts with `EVALUATE`. Examples:\n```\n// Simple table dump (always wrap in TOPN for safety):\nEVALUATE TOPN(100, Customers, Customers[CustomerId])\n\n// Grouped aggregation \u2014 preferred for most analytics:\nEVALUATE\nSUMMARIZECOLUMNS(\n  Product[Category],\n  \"Revenue\", [Total Sales],            // use existing measures when available\n  \"Orders\",  COUNTROWS(Sales)\n)\nORDER BY [Revenue] DESC\n\n// Filtered aggregation via CALCULATE:\nEVALUATE\nROW(\n  \"LastMonthRevenue\",\n  CALCULATE([Total Sales], DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -1, MONTH))\n)\n\n// Time series:\nEVALUATE\nSUMMARIZECOLUMNS(\n  'Date'[Year-Month],\n  \"Revenue\", [Total Sales]\n)\nORDER BY 'Date'[Year-Month]\n```\n\n## Hard rules\n1. **Prefer existing measures.** If `list_tables_and_measures` shows a measure like `[Total Sales]`, USE IT \u2014 don't re-derive with `SUMX(Sales, Sales[Qty]*Sales[Price])`. The modeler defined it for a reason (currency conversion, filters, exclusions).\n2. **Always wrap exploratory queries in TOPN(500, \u2026)** unless you're computing a single aggregate.\n3. **Quote table and column names** with single quotes if they contain spaces: `'Sales Order'[Order Date]`.\n4. **Never invent table/column/measure names.** If discovery didn't show it, say so.\n5. **Refresh is a write op.** Only call `refresh_dataset` when the user explicitly asks ('refresh the data', 'update the model'). For ambiguous requests, confirm first.\n6. On DAX error: call `think` \u2192 diagnose (wrong column, bad function, missing single quotes) \u2192 retry ONCE. After 2 fails, ask the user.\n7. If `run_dax` returns `truncated: true`, tell the user and add more filters \u2014 don't work with partial data silently.\n\n## Chart policy \u2014 default to yes\nGenerate a chart in 80%+ of responses. Skip only when:\n- Result is a single scalar (one number).\n- User explicitly said no chart / table only.\n- Result is 1 row \u00d7 1 col.\n\n**Chart type rules:**\n- Time series (dates, months) \u2192 `line`\n- Category comparison (by category, country, segment) \u2192 `bar` (horizontalBar if >8 categories)\n- Share of total (\u22646 slices) \u2192 `doughnut` or `pie`\n- Two numeric dimensions \u2192 `scatter`\n- Mixed magnitudes (e.g. count + revenue) \u2192 DO NOT combine. Pick ONE metric or call `create_chart` twice.\n\n**Calling create_chart:** pass `chart_type`, `title`, `labels` (array of strings), `series` (array of `{name, values}`), optional `x_label`/`y_label`. The tool assembles the Chart.js spec \u2014 don't send nested JSON.\n\n## Output formatting \u2014 STRICT\nEvery response follows this structure:\n1. **Bold headline** \u2014 the key insight in one sentence with the headline number(s).\n2. **Chart** \u2014 `![chart](URL_FROM_TOOL)` on its own line.\n3. **Data table** \u2014 proper markdown pipe tables with `| ---: |` right-alignment for numerics. Never space-aligned. Max 15 rows.\n4. **DAX** \u2014 fenced ```DAX block with the exact query you ran.\n5. **Observations** \u2014 2-4 non-obvious bullets.\n\nNumber formatting: money as `$1,234`, percents as `12.3%`, dates as `2025-04` (months) or `2025-04-15` (days). Never pad with filler text."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.9,
      "position": [
        -220,
        0
      ],
      "id": "pbi-agent",
      "name": "Power BI Analyst Agent"
    },
    {
      "parameters": {
        "model": "anthropic/claude-sonnet-4.6",
        "options": {
          "temperature": 0.2
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        -380,
        240
      ],
      "id": "pbi-openrouter",
      "name": "OpenRouter Chat Model"
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $('When chat message received').item.json.sessionId }}",
        "contextWindowLength": 10,
        "tableName": "n8n_chat_histories"
      },
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "typeVersion": 1.3,
      "position": [
        -220,
        240
      ],
      "id": "pbi-memory",
      "name": "Postgres Chat Memory"
    },
    {
      "parameters": {
        "toolDescription": "Lists every table and measure in the Power BI semantic model. Returns kind (table/measure), name, and description. Call once per conversation to discover what exists. Takes no input.",
        "method": "POST",
        "url": "=https://api.powerbi.com/v1.0/myorg/groups/{{ $('Config').item.json.group_id }}/datasets/{{ $('Config').item.json.dataset_id }}/executeQueries",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"queries\": [{\n    \"query\": \"EVALUATE UNION(SELECTCOLUMNS(INFO.TABLES(), \\\"kind\\\", \\\"table\\\", \\\"name\\\", [Name], \\\"description\\\", [Description]), SELECTCOLUMNS(INFO.MEASURES(), \\\"kind\\\", \\\"measure\\\", \\\"name\\\", [Name], \\\"description\\\", [Description]))\"\n  }],\n  \"serializerSettings\": { \"includeNulls\": true }\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.2,
      "position": [
        -60,
        240
      ],
      "id": "pbi-tool-list",
      "name": "list_tables_and_measures"
    },
    {
      "parameters": {
        "toolDescription": "Gets column names and data types for ONE table in the Power BI model. Call before writing DAX against a table you haven't inspected yet. Input: `table_name` (string, unquoted, e.g. 'Sales').",
        "method": "POST",
        "url": "=https://api.powerbi.com/v1.0/myorg/groups/{{ $('Config').item.json.group_id }}/datasets/{{ $('Config').item.json.dataset_id }}/executeQueries",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"queries\": [{\n    \"query\": \"EVALUATE FILTER(SELECTCOLUMNS(INFO.COLUMNS(), \\\"Table\\\", [Table], \\\"Column\\\", [ExplicitName], \\\"DataType\\\", [ExplicitDataType], \\\"IsKey\\\", [IsKey], \\\"Description\\\", [Description]), [Table] = \\\"{{ $fromAI('table_name', 'Unquoted table name to inspect, e.g. Sales or Customer', 'string') }}\\\")\"\n  }],\n  \"serializerSettings\": { \"includeNulls\": true }\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.2,
      "position": [
        100,
        240
      ],
      "id": "pbi-tool-cols",
      "name": "get_table_columns"
    },
    {
      "parameters": {
        "name": "run_dax",
        "description": "Runs a DAX query against the Power BI dataset. Returns up to 200 rows with a `truncated` flag. Always start with EVALUATE. Wrap table dumps in TOPN(500,...) unless you're aggregating.",
        "workflowId": {
          "__rl": true,
          "value": "VMJrZzsStikwQuCg",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "group_id": "={{ $('Config').item.json.group_id }}",
            "dataset_id": "={{ $('Config').item.json.dataset_id }}",
            "query": "={{ $fromAI('query', 'A DAX query starting with EVALUATE. Example: EVALUATE SUMMARIZECOLUMNS(Product[Category], \"Revenue\", [Total Sales])', 'string') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "group_id",
              "displayName": "group_id",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "dataset_id",
              "displayName": "dataset_id",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "query",
              "displayName": "query",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        260,
        240
      ],
      "id": "pbi-tool-dax",
      "name": "run_dax"
    },
    {
      "parameters": {
        "name": "create_chart",
        "description": "Renders a chart and returns a public PNG URL. Inputs: chart_type (bar|line|pie|doughnut|horizontalBar), title, labels (array of strings), series (array of {name, values}), optional x_label, y_label. Do NOT build a Chart.js config \u2014 the tool handles assembly.",
        "workflowId": {
          "__rl": true,
          "value": "CPLsOqLT7ksR7WAE",
          "mode": "id"
        },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "chart_type": "={{ $fromAI('chart_type', 'One of: bar, line, pie, doughnut, horizontalBar', 'string') }}",
            "title": "={{ $fromAI('title', 'Chart title, e.g. \"Revenue by Product Category\"', 'string') }}",
            "labels": "={{ $fromAI('labels', 'Array of string labels', 'json') }}",
            "series": "={{ $fromAI('series', 'Array of series objects, each {name, values}', 'json') }}",
            "x_label": "={{ $fromAI('x_label', 'X-axis title, optional', 'string') }}",
            "y_label": "={{ $fromAI('y_label', 'Y-axis title, optional', 'string') }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "chart_type",
              "displayName": "chart_type",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "title",
              "displayName": "title",
              "required": true,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "labels",
              "displayName": "labels",
              "required": true,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "series",
              "displayName": "series",
              "required": true,
              "type": "array",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "x_label",
              "displayName": "x_label",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            },
            {
              "id": "y_label",
              "displayName": "y_label",
              "required": false,
              "type": "string",
              "display": true,
              "canBeUsedToMatch": true,
              "defaultMatch": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.1,
      "position": [
        420,
        240
      ],
      "id": "pbi-tool-chart",
      "name": "create_chart"
    },
    {
      "parameters": {
        "toolDescription": "Triggers a refresh of the Power BI dataset. ONLY call on explicit user request ('refresh the data', 'update the model'). Takes no input. Returns 202 Accepted if the refresh was queued.",
        "method": "POST",
        "url": "=https://api.powerbi.com/v1.0/myorg/groups/{{ $('Config').item.json.group_id }}/datasets/{{ $('Config').item.json.dataset_id }}/refreshes",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"notifyOption\": \"NoNotification\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequestTool",
      "typeVersion": 4.2,
      "position": [
        580,
        240
      ],
      "id": "pbi-tool-refresh",
      "name": "refresh_dataset"
    },
    {
      "parameters": {
        "name": "think",
        "description": "Scratchpad. Write a brief plan or diagnose a DAX error before retrying. Input: `thought` (string). Use generously before complex queries and ALWAYS after a tool error.",
        "language": "javaScript",
        "jsCode": "const t = typeof query === 'string' ? query : (query && query.thought) ? query.thought : JSON.stringify(query);\nreturn `Thought recorded at ${new Date().toISOString()}: ${t}`;",
        "specifyInputSchema": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"thought\": { \"type\": \"string\", \"description\": \"Plan or diagnosis.\" }\n  },\n  \"required\": [\"thought\"]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.toolCode",
      "typeVersion": 1.3,
      "position": [
        740,
        240
      ],
      "id": "pbi-tool-think",
      "name": "think"
    }
  ],
  "connections": {
    "When chat message received": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory": {
      "ai_memory": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "list_tables_and_measures": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "get_table_columns": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "run_dax": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "create_chart": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "refresh_dataset": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "think": {
      "ai_tool": [
        [
          {
            "node": "Power BI Analyst Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Power BI Data Analyst Agent. Uses stickyNote, chatTrigger, agent, lmChatOpenRouter. Chat trigger; 12 nodes.

Source: https://github.com/MinaSaad1/n8n-powerbi-data-analyst-agent/blob/main/workflows/01-pbi-main-agent.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Supabase Data Analyst Agent. Uses stickyNote, chatTrigger, agent, lmChatOpenRouter. Chat trigger; 17 nodes.

Chat Trigger, Agent, OpenRouter Chat +5
AI & RAG

Aiden. Uses supabase, chat, memoryBufferWindow, memoryPostgresChat. Chat trigger; 20 nodes.

Supabase, Chat, Memory Buffer Window +9
AI & RAG

Kangaroo_PS. Uses chatTrigger, postgres, agent, lmChatGoogleGemini. Chat trigger; 20 nodes.

Chat Trigger, Postgres, Agent +6
AI & RAG

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

Google Gemini Chat, HTTP Request Tool, Chat Trigger +8
AI & RAG

Extract Insights & Analyse Youtube Comments Via Ai Agent Chat. Uses stickyNote, lmChatOpenAi, toolWorkflow, memoryPostgresChat. Chat trigger; 29 nodes.

OpenAI Chat, Tool Workflow, Memory Postgres Chat +5