3.1 Agent n8n
Simple demos are the best way to understand the core of agentic apps. For details see the lab notes doc #603.docx on my Gdrive.
TOC
- N1 cloud n8n / TTBO gmail 26.0408 (BINGO)
- N2 n8n_local install / test (json) 26.0409 (BINGO)
- N3 n8n_local (ttbo) / Gmail (jk) read many (set’d up GCP_jk) (BINGO)
- N4 py script replaces n8n app (Win11/jk) (BINGO)
- N5 py script adds PAL (BINGO) 26.0409
- NX TODO
GPT:
My recollection of the individual demos:
Demo Main idea
N1 First successful cloud n8n + Gmail workflow
N2 Local n8n installation and JSON workflow testing
N3 Local n8n reading multiple Gmail messages through GCP credentials
N4 Replace the n8n workflow with a Python script
N5 Add PAL logic to the Python workflow
So the progression was roughly:
Cloud n8n
↓
Local n8n
↓
Gmail integration
↓
Replace n8n with Python
↓
Add PAL intelligence
Which is actually a nice story because it shows the evolution from no-code automation → custom automation → AI-assisted automation.
N1 cloud n8n / TTBO gmail 26.0408 (BINGO)
Yes. This is a good first Gmail demo, and the cleanest way to start is a local Python desktop app that uses OAuth, reads a small set of messages, and prints subject/from/snippet. Google’s Gmail Python quickstart is specifically for a Python command-line app, requires Python 3.10.7+, a Google Cloud project, Gmail enabled on the account, and says its auth setup is fine for testing but simplified for production.
For your use case, use a Desktop app OAuth client, not a service account. Google’s current credential docs say that for end-user data access you create an OAuth 2.0 client, and for a desktop app the path is
Google Auth Platform → Clients → Create Client → Application type = Desktop app.
For this first demo, request only gmail.readonly. Google’s scope docs say you should choose the narrowest scope possible, and https://www.googleapis.com/auth/gmail.readonly allows viewing email messages and settings. The same page also shows gmail.send and gmail.modify exist for later demos, but they grant broader access.
Also, don’t worry about verification yet. Google says apps in development or test builds do not need to complete verification before you build and test, though sensitive or restricted scopes can still trigger the unverified-app warning if your code’s scopes and consent-screen scopes do not match.
gmail_demo_1_read_search.py:
from __future__ import annotations
import base64
import os
from typing import List, Dict, Optional
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# Demo 1: read/search only
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
def get_gmail_service():
creds: Optional[Credentials] = None
# token.json stores the user's access + refresh tokens after first login
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# If no valid creds, run local OAuth flow
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json",
SCOPES,
)
creds = flow.run_local_server(port=0)
with open("token.json", "w", encoding="utf-8") as token_file:
token_file.write(creds.to_json())
service = build("gmail", "v1", credentials=creds)
return service
def get_header(headers: List[Dict[str, str]], name: str) -> str:
for h in headers:
if h.get("name", "").lower() == name.lower():
return h.get("value", "")
return ""
def search_messages(service, query: str, max_results: int = 10):
result = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
return result.get("messages", [])
def read_message_metadata(service, msg_id: str):
msg = (
service.users()
.messages()
.get(
userId="me",
id=msg_id,
format="metadata",
metadataHeaders=["From", "Subject", "Date"],
)
.execute()
)
return msg
def main():
service = get_gmail_service()
# Change this to whatever you want
# Examples:
# query = "newer_than:30d"
# query = 'from:amazon newer_than:30d'
# query = 'label:inbox is:unread'
query = "newer_than:30d"
messages = search_messages(service, query=query, max_results=10)
print(f"query: {query}")
print(f"matches: {len(messages)}")
print("-" * 80)
if not messages:
print("No messages found.")
return
for i, item in enumerate(messages, start=1):
msg = read_message_metadata(service, item["id"])
payload = msg.get("payload", {})
headers = payload.get("headers", [])
subject = get_header(headers, "Subject")
sender = get_header(headers, "From")
date = get_header(headers, "Date")
snippet = msg.get("snippet", "")
print(f"[{i}] Subject: {subject}")
print(f" From: {sender}")
print(f" Date: {date}")
print(f" Snippet: {snippet}")
print("-" * 80)
if __name__ == "__main__":
main()
[10] Subject: xxxxxxxxxxxxxxxxxxxxxx and save.
From: xxxxxxxxxxxxxxxxx>
Date: xxxxxxxxxxxxxxxxxxxxxx (EDT)
Snippet: In store and online xxxxxxxxxxxxxxxxxxxxxxxxxxxors
--------------------------------------------------------------------------------
(venv)
terry@LAPTOP-HKPDHF7M MINGW64 ~/Downloads/d1_agent (main)
N2 n8n_local install / test (json) 26.0409 (BINGO)
- Make sure you have: Docker Desktop installed (Windows OK with WSL2)
- run the n8n command in what? win11 powershell? Yes — Windows 11 PowerShell works fine 👍
Since you already use WSL:
docker run -it --rm \
-p 5678:5678 \
-v n8n_data:/home/node/.n8n \
n8nio/n8n
Editor is now accessible via:
http://localhost:5678
9) Key mindset (important)
Don’t try to “use n8n long-term”
Use it to answer:
“What building blocks do I need in my own system?”
10) One-line summary
Run n8n locally with Docker, use it to prototype workflows, then rebuild the useful parts in your own PAL-style app.
No — local Docker n8n is NOT limited by the trial.
N3 n8n_local (ttbo) / Gmail (jk) read many (set’d up GCP_jk) (BINGO)
#511 setup GCP for JK (follow #505)
Google Auth Platform → Clients → Create Client → Application type = Desktop app.
2. Enable the Gmail API.
3. Configure the OAuth consent screen / Google Auth Platform branding.
4. Create an OAuth client with Application type = Desktop app.
-------------------------
PROMPT: get emails
RESPONSE: Here are the latest emails:
xxxxxx
2. Subject: Want to Know What Technical Writers Are Actually Doing With AI? Start Here!
o From: Scott Abel from The Content Wrangler
xxxxxxxxxxxxx
5. Subject: Your free n8n license key to unlock selected paid features
o From: n8n.io
If you need more details about a specific email, let me know!
-----------------------
[
{
"id": "19d72d13966a2709",
"threadId": "19d72c5e2eaf7007",
"snippet": "On Thu, Apr 9, 2026 at 11:04 AM Scott Abel from The Content Wrangler <thecontentwrangler@substack.com> wrote: Grab your free copy of the \"2026 State of AI in Technical Documentation\"",
"payload": {
"mimeType": "multipart/alternative"
},
"sizeEstimate": 77804,
"historyId": "64902",
"internalDate": "1775747806000",
"labels": [
{
"id": "DRAFT",
"name": "DRAFT"
}
],
"Subject": "Re: Want to Know What Technical Writers Are Actually Doing With AI? Start Here!",
"From": xxxxxxxxxxxxxxxxxxx
},
{
"id": "19d72c5e2eaf7007",
"threadId": "19d72c5e2eaf7007",
........................................................
Excellent. That means the important part works:
• Gmail auth works
• n8n can read mailbox data
• AI node can summarize results
• raw message JSON is available underneath
So now you’ve identified a very realistic pattern:
tool gets data → model formats/summarizes it
That is useful.
What this also shows is the split you care about:
• n8n/tool layer = fetches real emails
• LLM layer = turns them into readable output
N4 py script replaces n8n app (Win11/jk) (BINGO)
#513 replicating the n8n_read_many_gmails app without n8n. just with python
(be brief)
(1) Now n8n is working locally. so the subscription for n8n running out ... that problem is solved.
(2) very simple n8n_read_many_gmails app is working.
(3) your suggestions for future n8n_local demos is good, but....
(4) i'd like to focus on replicating the n8n_read_many_gmails app without n8n. just with python.
(5) i think we only need following 3 things from n8n demo:
1- agent python.
2- openAI api.
3- gmail access.
(we dont need and memory)
(6) is this possible?
Yes — absolutely possible.
You only need:
1. Python app
2. OpenAI API
3. Gmail API
No memory needed.
Minimal architecture:
user prompt
→ Python agent
→ Gmail API reads emails
→ Python selects relevant fields
→ OpenAI formats/summarizes
→ return answer
That is basically your n8n demo, rebuilt cleanly.
Best next step:
• keep Gmail read-only
• do read many emails
• pass only:
o subject
o from
o date
o snippet
to OpenAI
So yes:
n8n_read_many_gmails can be replicated with plain Python very easily.
Next file should be something like:
gmail_demo_2_agent_read_many.py
with flow:
prompt → Gmail query → fetch N emails → OpenAI summary
# gmail_demo_2_agent_read_many.py
import os
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from openai import OpenAI
from dotenv import load_dotenv
import os
load_dotenv()
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
# -----------------------------
# Gmail auth
# -----------------------------
def get_gmail_service():
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as f:
f.write(creds.to_json())
return build("gmail", "v1", credentials=creds)
# -----------------------------
# Gmail read
# -----------------------------
def get_header(headers, name):
for h in headers:
if h["name"].lower() == name.lower():
return h["value"]
return ""
def read_emails(service, query="newer_than:7d", max_results=10):
res = service.users().messages().list(
userId="me", q=query, maxResults=max_results
).execute()
msgs = res.get("messages", [])
results = []
for m in msgs:
msg = service.users().messages().get(
userId="me",
id=m["id"],
format="metadata",
metadataHeaders=["From", "Subject", "Date"],
).execute()
headers = msg["payload"]["headers"]
results.append({
"subject": get_header(headers, "Subject"),
"from": get_header(headers, "From"),
"date": get_header(headers, "Date"),
"snippet": msg.get("snippet", "")
})
return results
# -----------------------------
# OpenAI summarize
# -----------------------------
def summarize_emails(emails):
# client = OpenAI()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
text = "\n".join([
f"From: {e['from']}\nSubject: {e['subject']}\nSnippet: {e['snippet']}\n"
for e in emails
])
prompt = f"""
Summarize these emails briefly:
{text}
"""
resp = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role": "user", "content": prompt}]
)
return resp.choices[0].message.content
# -----------------------------
# Main agent
# -----------------------------
def main():
service = get_gmail_service()
emails = read_emails(service)
print("\n--- RAW EMAILS ---\n")
for e in emails:
print(e["subject"], "|", e["from"])
summary = summarize_emails(emails)
print("\n--- AI SUMMARY ---\n")
print(summary)
if __name__ == "__main__":
main()
$ python gmail_demo_2_agent_read_many.py
--- RAW EMAILS ---
Learn by doing: Make Foundation course 🎓 | Make <info@make.com>
Must-Know Cross-Cutting Concerns in API Development | ByteByteGo <bytebytego@substack.com>
Documentation Isn’t Dying, But Bad Production Habits Should Be Very Nervous | Scott Abel from The Content Wrangler <thecontentwrangler@substack.com>
..........................
--- AI SUMMARY ---
Here is a brief summary of the emails:
1. **Make**: Invitation to a beginner-friendly Make Foundation course to build your first automation scenario with guided lessons.
2. **ByteByteGo**: Overview of essential cross-cutting concerns in API development, including authentication, logging, rate limiting, and input validation.
............................
N5 py script adds PAL (BINGO) 26.0409
What you already have (PAL-like)
You now have:
prompt → tool (Gmail) → data → LLM → answer
That is:
✔ tool use
✔ external data
✔ structured pipeline
________________________________________
What is missing (true PAL behavior)
PAL-style systems add:
1) Planning layer
Right now:
always → read emails → summarize
PAL:
LLM decides:
- search unread?
- last 7 days?
- filter sender?
________________________________________
2) Structured plan (JSON)
Instead of direct code:
{
"action": "read_emails",
"query": "label:inbox is:unread",
"limit": 10
}
________________________________________
3) Deterministic execution
Your code executes plan:
plan → Gmail API → results
(not LLM doing everything)
________________________________________
4) Separation (VERY important)
LLM = planner + explainer
Python = execution engine
Next upgrade (minimal PAL step)
Add:
user prompt → LLM → JSON plan → Python executes → result → LLM explains
# gmail_demo_3_pal_plan.py
import os
import json
from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from openai import OpenAI
load_dotenv()
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
# ============================================================
# ##1 Gmail auth
# ============================================================
def get_gmail_service():
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("token.json", "w", encoding="utf-8") as f:
f.write(creds.to_json())
return build("gmail", "v1", credentials=creds)
# ============================================================
# ##2 Gmail helpers
# ============================================================
def get_header(headers, name):
for h in headers:
if h.get("name", "").lower() == name.lower():
return h.get("value", "")
return ""
def read_emails(service, query="newer_than:7d", max_results=10):
res = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
msgs = res.get("messages", [])
out = []
for m in msgs:
msg = (
service.users()
.messages()
.get(
userId="me",
id=m["id"],
format="metadata",
metadataHeaders=["From", "Subject", "Date"],
)
.execute()
)
headers = msg.get("payload", {}).get("headers", [])
out.append(
{
"subject": get_header(headers, "Subject"),
"from": get_header(headers, "From"),
"date": get_header(headers, "Date"),
"snippet": msg.get("snippet", ""),
}
)
return out
# ============================================================
# ##3 OpenAI client
# ============================================================
def get_openai_client():
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("OPENAI_API_KEY not found in .env")
return OpenAI(api_key=api_key)
# ============================================================
# ##4 Planner: user prompt -> JSON plan
# ============================================================
def make_plan(client, user_prompt):
system = """
You are a planner for a Gmail read-only agent.
Return ONLY valid JSON.
No markdown. No explanation.
Allowed schema:
{
"action": "read_emails",
"query": "<gmail search query>",
"limit": <integer 1..20>
}
Examples:
User: get emails
{"action":"read_emails","query":"newer_than:7d","limit":10}
User: get unread emails
{"action":"read_emails","query":"is:unread newer_than:7d","limit":10}
User: get emails from amazon
{"action":"read_emails","query":"from:amazon newer_than:30d","limit":10}
User: summarize latest promotional emails
{"action":"read_emails","query":"category:promotions newer_than:14d","limit":10}
""".strip()
resp = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user_prompt},
],
temperature=0,
)
text = resp.choices[0].message.content.strip()
plan = json.loads(text)
if plan.get("action") != "read_emails":
raise ValueError("Only action=read_emails is allowed")
limit = int(plan.get("limit", 10))
if limit < 1 or limit > 20:
raise ValueError("limit must be between 1 and 20")
query = str(plan.get("query", "newer_than:7d")).strip()
if not query:
query = "newer_than:7d"
return {"action": "read_emails", "query": query, "limit": limit}
# ============================================================
# ##5 Summarizer: # emails -> natural language answer
# ============================================================
def summarize_emails(client, user_prompt, emails):
text = "\n\n".join(
[
f"From: {e['from']}\nSubject: {e['subject']}\nDate: {e['date']}\nSnippet: {e['snippet']}"
for e in emails
]
)
prompt = f"""
User request:
{user_prompt}
Emails:
{text}
Give a brief helpful answer.
""".strip()
resp = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
)
return resp.choices[0].message.content
# ============================================================
# ##6 Main
# ============================================================
def main():
user_prompt = input("PROMPT: ").strip()
if not user_prompt:
user_prompt = "get emails"
client = get_openai_client()
service = get_gmail_service()
plan = make_plan(client, user_prompt)
print("\n--- PLAN ---")
print(json.dumps(plan, indent=2))
emails = read_emails(
service,
query=plan["query"],
max_results=plan["limit"],
)
print(f"\n--- RAW EMAILS ({len(emails)}) ---")
for i, e in enumerate(emails, start=1):
print(f"[{i}] {e['subject']} | {e['from']}")
answer = summarize_emails(client, user_prompt, emails)
print("\n--- AI RESPONSE ---")
print(answer)
if __name__ == "__main__":
main()
Before:
always read same emails
Now:
user prompt → LLM builds plan → Python executes
That is the first real PAL-style step.
Later we can add:
• sender filter whitelist
• stricter JSON schema
• draft email action
But this version is enough to demonstrate planning.
$ python gmail_demo_3_pal_plan.py
PROMPT: summarize latest promotional emails
--- PLAN ---
{
"action": "read_emails",
"query": "category:promotions newer_than:7d",
"limit": 10
}
--- RAW EMAILS (5) ---
[1] Learn by doing: Make Foundation course 🎓 | Make <info@make.com>
[2] Stuck getting started, joseph? | Make <info@make.com>
[3] See what others like you are automating 🔥 | Make <info@make.com>
[4] Automate in minutes with ready-made templates ⚡ | Make <info@make.com>
[5] Not sure where to start? See this tutorial 👉 | Make <info@make.com>
--- AI RESPONSE ---
The latest promotional emails from Make focus on helping users get started with automation through various resources: a beginner-friendly Foundation course, expert guidance for first-time automation, inspiration from real user workflows, ready-made automation templates for quick results, and tutorials on working smarter.
(venv)
terry@LAPTOP-HKPDHF7M MINGW64 ~/Downloads/d1_agent (main)
NX TODO
My suggestion: use n8n now as a feature discovery tool, not as your final platform. n8n is popular for workflow automation, has broad app integrations, AI-agent features, webhook/API support, and can also be self-hosted, so it’s a good way to see what patterns are worth recreating in your own stack.
For job relevance, the most valuable functions to study are:
Gmail / Outlook
Read, search, classify, draft, escalate. This is close to real operations work. n8n has Gmail integrations, and job postings increasingly mention automation tools plus REST APIs and AI integrations together.
Slack / Teams
Alerts, approvals, summaries, human-in-the-loop. This is one of the most common enterprise patterns. n8n supports Slack-style workflow patterns directly.
Google Sheets / Airtable / simple DB
Very useful because many companies still run ops from tables. Pattern: ingest → analyze → flag → update table. n8n has first-class support for this kind of flow.
HTTP Request / REST APIs
This is the most important technical skill because it generalizes to any enterprise system. n8n explicitly supports raw HTTP/API workflows, which is exactly what you would later rebuild yourself.
Webhooks
Trigger on external events, then fan out to analysis and action. This is very job-relevant because many real systems are event-driven.
Best near-term plan:
• Use n8n to prototype 2–3 flows fast.
• Pick the best one.
• Rebuild that one in your own Python/FastAPI stack with OAuth, rules, and approval gates.
The best demo ecosystem for you is:
n8n for fast prototyping + PAL-style self-hosted app for secure/serious version
That combination makes sense because n8n is good for discovering workflow patterns, while your own app is better for security, custom logic, and “Palantir-style” controlled analysis. n8n itself says self-hosting is possible, but also warns self-hosting requires real server/security skill.
Most job-relevant first demo, in my view:
Gmail → analyze emails → group by sender/topic → draft summary → require approval
That gives you API access, data analysis, AI usage, and guarded action in one compact demo.
26.0617 (v1 0617)