Nodes:
name: When chat message received
type: @n8n/n8n-nodes-langchain.chatTrigger
name: AI Agent
type: @n8n/n8n-nodes-langchain.agent
parameters: options: maxIterations: 15
systemMessage: =You are a helpful assistant.
Current timestamp is {{ $now }}
name: OpenAI Chat Model
type: @n8n/n8n-nodes-langchain.lmChatOpenAi
parameters: model: __rl: yes
mode: list
value: gpt-4o
cachedResultName: gpt-4o
options: temperature: 0.2
name: Calculator
type: @n8n/n8n-nodes-langchain.toolCalculator
name: Code
type: n8n-nodes-base.code
parameters: jsCode: // Ensure there's at least one input item.
if (!items || items.length === 0) {
  throw new Error("No input items found.");
}

// Our input is expected to have a 'data' property containing the JSONP string.
const input = items[0].json;

if (!input.data) {
  throw new Error("Input JSON does not have a 'data' property.");
}

const rawData = input.data;

// Use a regex to extract the JSON content from the Google Visualization JSONP response.
const regex = /google\.visualization\.Query\.setResponse\((.*)\);?$/s;
const match = rawData.match(regex);

if (!match) {
  throw new Error("Input data does not match the expected Google Visualization JSONP format.");
}

const jsonString = match[1];

// Parse the extracted JSON string.
let parsed;
try {
  parsed = JSON.parse(jsonString);
} catch (error) {
  throw new Error("Failed to parse JSON: " + error.message);
}

// Verify that the parsed JSON has the expected 'table' structure with 'cols' and 'rows'.
if (!parsed.table || !Array.isArray(parsed.table.cols) || !Array.isArray(parsed.table.rows)) {
  throw new Error("Parsed JSON does not have the expected 'table' structure with 'cols' and 'rows'.");
}

const cols = parsed.table.cols;
const rows = parsed.table.rows;

// Helper function to convert date string from "Date(YYYY,M,D)" to "YYYY-MM-DD"
function formatDate(dateStr) {
  const match = dateStr.match(/^Date\((\d+),(\d+),(\d+)\)$/);
  if (!match) return dateStr;
  const year = parseInt(match[1], 10);
  const month = parseInt(match[2], 10) + 1; // JavaScript months are 0-indexed
  const day = parseInt(match[3], 10);
  // Format with leading zeros
  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}

// Map each row into an object using the column labels as keys.
const newItems = rows.map(row => {
  const obj = {};
  cols.forEach((col, index) => {
    let value = row.c && row.c[index] ? row.c[index].v : null;
    // If the column type is "date" and the value is a string that looks like "Date(YYYY,M,D)"
    if (col.type === "date" && typeof value === "string") {
      value = formatDate(value);
    }
    obj[col.label] = value;
  });
  return { json: obj };
});

// Return the new array of items.
return newItems;

name: When Executed by Another Workflow
type: n8n-nodes-base.executeWorkflowTrigger
parameters: workflowInputs: values: start_date
end_date
status
name: Records by date
type: @n8n/n8n-nodes-langchain.toolWorkflow
parameters: name: records_by_date_and_or_status
workflowId: __rl: yes
mode: list
value: a2BIIjr2gLBay06M
cachedResultName: Template | Your first AI Data Analyst
description: Use this tool to get records filtered by date. You can also filter by status at the same time, if you want.
workflowInputs: value: status: ={{ $fromAI("status", "Status of the transaction. Can be Completed, Refund or Error. Leave empty if you don't need this now.", "string") }}
end_date: ={{ $fromAI("end_date", "End date in format YYYY-MM-DD", "string") }}
start_date: ={{ $fromAI("start_date", "Start date in format YYYY-MM-DD", "string") }}
schema: type: string
display: yes
required: no
displayName: start_date
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: end_date
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
removed: no
required: no
displayName: status
defaultMatch: no
canBeUsedToMatch: yes
mappingMode: defineBelow
attemptToConvertTypes: no
convertFieldsToString: no
name: Aggregate
type: n8n-nodes-base.aggregate
parameters: aggregate: aggregateAllItemData
name: Sticky Note
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 220
height: 400
content: To send all the items back to the AI, we need to finish with everything aggregated into one single item.

Otherwise it will respond with one item at a time, and the AI will only get the first item that arrives.
name: Sticky Note1
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 300
height: 400
content: This node sends a custom HTTP Request to the Google Sheets API.

Filtering by date range in the Google Sheets API is very complicated.

This node solves that problem.

But doing the same in a database is much simpler. A tool could do it without needing a sub-workflow.
name: Sticky Note2
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 220
height: 400
content: The output from this complex request is also messy.

So we use some code generated by ChatGPT to transform the data into JSON objects.
name: Google Sheets request
type: n8n-nodes-base.httpRequest
parameters: url: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/gviz/tq
sendQuery: yes
authentication: predefinedCredentialType
queryParameters: parameters: name: sheet
value: Sheet1
name: tq
value: =SELECT * WHERE A >= DATE "{{ $json.start_date }}" AND A <= DATE "{{ $json.end_date }}"
nodeCredentialType: googleSheetsOAuth2Api
name: Get transactions by product name
type: n8n-nodes-base.googleSheetsTool
parameters: filtersUI: values: lookupValue: ={{ $fromAI("product_name", "The product name", "string") }}
lookupColumn: Product
sheetName: __rl: yes
mode: list
value: gid=0
cachedResultUrl: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit#gid=0
cachedResultName: Sheet1
documentId: __rl: yes
mode: url
value: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit?usp=sharing
descriptionType: manual
toolDescription: Find transactions by product.
Our products are:
- Widget A
- Widget B
- Widget C
- Widget D
name: Get all transactions
type: n8n-nodes-base.googleSheetsTool
parameters: sheetName: __rl: yes
mode: list
value: gid=0
cachedResultUrl: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit#gid=0
cachedResultName: Sheet1
documentId: __rl: yes
mode: url
value: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit?usp=sharing
descriptionType: manual
toolDescription: Only use this as last resort, because it will pull all data at once.
name: Sticky Note3
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 320
height: 340
content: ## Some questions to try
There's a red button on this page that you can click to chat with the AI.

Try asking it these questions:

- How many refunds in January and what was the amount refunded?

- How many successful sales did we have in January 2025 and what was the final income of those?

- What is the most frequent reason for refunds?
name: Sticky Note4
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 400
content: ## Copy this Sheets file to your Google Drive
https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit?gid=0#gid=0
name: Sticky Note5
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 200
height: 140
content: ### 👈
The Calculator is a tool that allows an agent to run mathematical calculations.
name: Sticky Note6
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 400
content: ### How to connect to Google Sheets?
To connect your n8n to your Google Sheets you're gonna need Google OAuth credentials

See documentation **[here](https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/)**
name: Sticky Note7
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 170
height: 260
content: ## 👆
You can use many models here, including the free Google Gemini options.

Make sure to test it thoroughly. Some models are better for data analysis.
name: Sticky Note8
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 150
height: 260
content: ## 👆
This is a short term memory. It will remember the 5 previous interactions during the chat
name: Buffer Memory
type: @n8n/n8n-nodes-langchain.memoryBufferWindow
name: Sticky Note9
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 340
height: 180
content: The **AI Tools Agent** has access to all the tools at the same time. It uses the name and description to decide when to use each tool.

Notice I'm using `$fromAI` function in all of them.

See documentations **[here](https://docs.n8n.io/advanced-ai/examples/using-the-fromai-function/)**
name: Sticky Note10
type: n8n-nodes-base.stickyNote
parameters: color: 5
width: 740
height: 100
content: ## Want more templates like this?
### 👉 **[Check out all my templates](https://n8n.io/creators/solomon/)**
name: Sticky Note11
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 340
height: 320
content: ## 👈 This is a special tool
It is used to call another workflow.
This concept is called sub-workflow.

See documentation [here](https://docs.n8n.io/flow-logic/subworkflows/).

Instead of running a completely separate workflow, we are calling the one below.

It's contained in the same workflow, but we are using the trigger to define it will run only when called by this tool.
name: Sticky Note12
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 1380
height: 520
content: # Sub-workflow
The AI can call this sub-workflow anytime,
by using the **Records by date** tool.

The sub-workflow automatically return
 the result of the last executed node to the AI.
name: Filter by status
type: n8n-nodes-base.filter
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: string
operation: contains
leftValue: ={{ $json.Status }}
rightValue: ={{ $('When Executed by Another Workflow').item.json.status }}
name: Get transactions by status
type: n8n-nodes-base.googleSheetsTool
parameters: filtersUI: values: lookupValue: ={{ $fromAI("transaction_status", "Transaction status can be Refund, Completed or Error", "string") }}
lookupColumn: Status
sheetName: __rl: yes
mode: list
value: gid=0
cachedResultUrl: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit#gid=0
cachedResultName: Sheet1
documentId: __rl: yes
mode: url
value: https://docs.google.com/spreadsheets/d/18A4d7KYrk8-uEMbu7shoQe_UIzmbTLV1FMN43bjA7qc/edit?usp=sharing
descriptionType: manual
toolDescription: Find transactions by status
name: Sticky Note13
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 300
content: ## Change the URL of the Sheets file in all the Sheets tools 👇
name: Sticky Note14
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 260
height: 100
content: ## 👆 Change the URL of the Sheets file

Connections:
Code: main: node: Filter by status
type: main
Calculator: ai_tool: node: AI Agent
type: ai_tool
Buffer Memory: ai_memory: node: AI Agent
type: ai_memory
Records by date: ai_tool: node: AI Agent
type: ai_tool
Filter by status: main: node: Aggregate
type: main
OpenAI Chat Model: ai_languageModel: node: AI Agent
type: ai_languageModel
Get all transactions: ai_tool: node: AI Agent
type: ai_tool
Google Sheets request: main: node: Code
type: main
Get transactions by status: ai_tool: node: AI Agent
type: ai_tool
When chat message received: main: node: AI Agent
type: main
Get transactions by product name: ai_tool: node: AI Agent
type: ai_tool
When Executed by Another Workflow: main: node: Google Sheets request
type: main