Wake-Up Call: Why AI Safety Guardrails Break Under Pressure

This is a submission for the Google I/O Writing Challenge

This is a submission for the Google I/O Writing Challenge

We treat AI safety as a static state: the model either refuses the prompt or it doesn’t. But in practice, safety isn’t a single-turn check—it’s a dynamic, conversational challenge.

As a developer, I wanted to see what happens when the “No” isn’t the end of the conversation. I ran a pilot audit to test the resilience of major LLMs when faced with escalating conversational pressure, and the results are a necessary wake-up call for anyone building on these systems.

The Experiment

I evaluated six major language models across 20 distinct scenarios. The goal wasn’t to “break” the AI, but to stress-test their guardrails: if a model refused an initial inquiry, what happened when the conversation was pushed to persist?

The Data: A Pattern of Fragility

The following table reflects the percentage of interactions where models eventually provided harmful or unsafe content after an initial refusal.

Model Provider Failure Rate
Llama-4-scout Groq 85%
Llama-3.1-8b Groq 71%
GPT-4.1 OpenAI 59%
GPT-4o OpenAI 50%
Gemini 2.0 Flash Google 50%
Gemini 2.5 Pro Google 42%

(Note: “Failure” is defined as providing actionable, sensitive information after an initial refusal. This pilot represents directional data, not a professional security audit.)

What This Tells Us About AI Safety

The pattern is clear: Refusal decay. Many models perform perfectly on the first turn—the “shallow” safety check—but their guardrails weaken as the conversational state grows more complex. When a system is designed to be helpful, persistent pressure can override safety constraints, turning a model from a safe assistant into a liability.

Why This Matters for Developers

If you are deploying AI in a production environment, you cannot treat safety as a “model-native” feature. This audit demonstrates that:

  1. First-turn testing is not enough: Relying on basic safety benchmarks only tells you if the model is compliant in isolation. It doesn’t tell you how it behaves under the sustained pressure of a real-world user.
  2. Context is a vulnerability: Conversational drift is real. As the context window fills with complex framing, the model’s priority shifts from following its safety guidelines to following the user’s lead.
  3. Resilience > Capability: We are currently in a race for smarter models, but we are neglecting the “defensive integrity” of these systems.

Call to Action

It’s time to move beyond simple refusal checks. For developers building on LLMs, the path forward is clear:

  • Implement Model-Independent Guardrails: Do not rely solely on the underlying model’s “alignment.” Use external, hardened moderation layers that enforce safety as a non-negotiable constraint.
  • Adversarial-Test Your Flows: If your product involves a multi-turn conversation, test those specific paths. Use adversarial framing to see if your system holds up over time.
  • Build for Failure: Assume the model will eventually try to comply with an unsafe prompt, and have the infrastructure in place to catch and block that output before it reaches the user.

Conclusion

A model that sounds safe once is not necessarily safe in practice. If we want AI to be reliable, we have to stop treating safety as a performance metric and start treating it as an engineering requirement.

Safety isn’t about being smart; it’s about being robust. Let’s build like it.

How to Collect Telegram Media Groups in Node.js

When working with the Telegram Bot API, it is easy to expect that an album with multiple photos will arrive as a single event.

But Telegram works differently.

If a user sends an album of 3 photos, your bot doesn’t receive one “post” object. Instead, it receives 3 separate updates in rapid succession:

Update #1 -> photo_1
Update #2 -> photo_2
Update #3 -> photo_3

instead of:

single_post -> [photo_1, photo_2, photo_3]

The Headache: Why this is hard
Handling these updates manually forces you to deal with a lot of “plumbing” code that has nothing to do with your actual bot logic:

Duplicate database records: Accidentally creating 3 posts instead of 1.
Race conditions: Multiple updates hitting your server at the exact same time.
Buffering & Timeouts: Deciding how long to wait for the “next” photo before assuming the group is complete.
Ordering: Ensuring the photos stay in the order the user intended.
Typical “infrastructure” code usually starts looking like this mess:

const mediaGroups = new Map();
// buffering logic...
// sorting logic...
// duplicate prevention...
// cleanup jobs...
// timeout handling...

At some point, you realize you are rewriting low-level infrastructure instead of building your actual features.

The Solution: telegram-media
I built telegram-media—a lightweight TypeScript library for Node.js that collects these scattered Telegram updates into a single, normalized object.

Installation

npm install telegram-media

Usage Example
Here is how you can use it to collect media and save it to a database (like Prisma) using a Redis storage backend:

const collector = createTelegramMediaGroup({
  async onCollected(post) {
    // This only fires ONCE per media group
    await prisma.telegramPost.create({
      data: mapCollectedPostToPrismaInput(post),
    });
  },

  // Use Redis for distributed environments
  storage: createRedisMediaGroupStorage(redisClient),

  timeoutMs: 3000,

  supportedMediaTypes: ["photo", "video", "audio"],
});

Why I Built This?
I ran into this problem while building a Telegram ingestion system for a personal project. At first, the logic seemed simple—just a small Set and a setTimeout.

Then the edge cases hit: distributed workers fighting over the same group, incomplete groups caused by network lag, and Redis synchronization issues. I extracted the logic into a standalone package so no one else has to solve this from scratch.

Key Features

  • Media Group Aggregation — Automatically groups related Telegram updates into a single normalized post.
  • Redis Support — Ready for production and distributed environments.
  • Duplicate Prevention — Handles Telegram retry updates safely.
  • Ordering — Preserves the original media sequence.
  • TypeScript — Fully typed for a better developer experience.

Explore

What do you think?
I’d love to hear how others are handling media groups. Do you use a custom buffer, or do you just process each image individually and update the record as you go?

Let me know in the comments!

How I Stopped Chasing Client Feedback Over WhatsApp and Built a WordPress Plugin Instead

If you’ve ever built a WordPress site for a client, you know the drill.

You spend weeks building something you’re proud of. The site looks great, the pages are polished, Elementor did its thing. Then comes the review phase.

And suddenly your phone is on fire. WhatsApp messages with blurry screenshots. Emails with feedback like “the thing on the left, make it bigger.” A Slack thread that spiraled into 47 messages about a single button color. A Zoom call that could have been a one-line comment. A Jira ticket that references a Google Doc that references another email that nobody can find anymore.
Sound familiar?

The breaking point

I was building a content-heavy site for a client — multiple pages, complex Elementor layouts, custom post types. The client needed to review everything before launch.

We tried everything. Shared staging links. Loom videos. Screenshot annotations. Google Docs with comments. Nothing stuck. The client would forget which version they were looking at, feedback would get lost, and I’d end up on a call re-explaining changes I’d already made twice.

I looked at tools like Marker.io, Atarim, and BugHerd. They’re good products — but they’re all external SaaS subscriptions starting at $49/month, they require clients to create accounts or install browser extensions, and they add another external dependency to every project.

I didn’t want another tool my client had to learn. I wanted something that lived inside WordPress, felt native, and worked the way developers think.

The GitHub moment

Then it hit me.
GitHub pull requests already solved this problem for code. Inline comments anchored to specific lines, an approve/request changes flow, clear status tracking, threaded discussions. Developers use this workflow every single day and it just works.

What if content review in WordPress worked exactly like a GitHub PR?
So I built it.

What I built

Flow is a free WordPress plugin that brings a GitHub-style review and approval workflow natively into WordPress.
Here’s how it works:

  1. You assign a reviewer to a post or page
  2. The reviewer opens a dedicated review page — they see the rendered content exactly as visitors will, not the editor
  3. They leave inline comments anchored directly to the content
    They approve or request changes
  4. You publish with confidence once approved

No SaaS subscription. No external service. No client logins — guests can review via a magic link without ever touching the WordPress admin.

It works with Gutenberg, Elementor, Classic Editor, and Bricks Builder out of the box.

The technical decisions worth mentioning

Why WordPress-native?
Every other tool in this space is a script tag wearing a plugin badge. They install a widget on your site that phones home to an external server. Flow runs entirely inside WordPress using native APIs and WordPress UI libraries. No external dependency, no data leaving your server.

Why inline comments instead of screenshots?
Screenshot annotation tools like Marker.io are great for bug reporting. But for content review — where a client needs to say “change this paragraph” or “this heading doesn’t work” — inline comments anchored to the actual content block are faster and more precise. The reviewer clicks directly on what they mean.

Why the GitHub PR model?
Developers already understand it intuitively. When I showed early users the approve/request changes flow, nobody needed a tutorial. The mental model transferred immediately.

Try it

Flow is free on WordPress.org:
http://wordpress.org/plugins/jumplinks-editorial-workflow

If you’ve ever suffered through client feedback chaos on a WordPress project — I built this for you. Would love to hear how your review process works currently and what you’d want from a tool like this.

Web3 Apps You Can Build With Token Extensions

A row of simple glowing tokens on the left flowing through thin light streams into a larger central platform with translucent floating cards arranged around it.

In our previous articles, we covered how Web3 tokens get their value and broke down the new standards in What is Token 2022 and why Solana built it. You understand authorities, token accounts, and the extensions available.

Now the question is what you actually build.

Token extensions are building blocks for real products. Each extension solves a specific problem. By leveraging Token 2022 via modern client libraries, you can build applications from frontend or backend scripts that would have required custom smart contracts before.

Here are some app ideas, the extensions they use, and the tools to start building them.

A row of simple glowing tokens on the left flowing through thin light streams into a larger central platform with translucent floating cards arranged around it.

1. Creator Subscription Platform

Subscribers hold a token that grants monthly access to exclusive content. The token cannot be resold or moved between wallets.

Extensions: Non-transferable tokens prevent resale; metadata stores the creator name, tier, and subscription period.

How it works: Create one mint per creator per subscription period, one for “Creator X – January 2026,” another for February. When a user subscribes, bundle a USDC payment with a non-transferable token mint into a single atomic transaction.

Use one mint per period because Token-2022 metadata is mint-level, every holder of a given mint reads the same expiry. Gating access is then a direct read of the period from the mint’s on-chain metadata against the current date.

To reclaim or burn subscription tokens directly, add PermanentDelegate at mint creation. The extension itself can never be removed once the mint is initialized, though the delegate authority can be rotated or set to None via SetAuthority.

Tools: @solana/kit, @solana-program/token-2022, Next.js or React.

Translucent token

2. Loyalty Rewards Program

A business issues loyalty points as tokens. Customers earn points on purchases and redeem them for discounts.

Extensions: Non-transferable tokens keep points tied to the customer; the business retains mint authority to issue new points; metadata displays the brand.

How it works: Each business gets its own token mint. The backend mints points directly into the customer’s token account on purchase events. Customers connect their wallet to a redemption portal to spend points.

On-chain identity here is pseudonymous, not private, wallet balances and minting activity are visible to anyone. Non-transferability stops third parties from trading or seizing the points, but doesn’t hide them from view.

Tools: @solana/kit, @solana-program/token-2022, Node.js, React, Solana Pay.

A glowing storefront-shaped block with small luminous tokens streaming out toward a translucent wallet card

3. Regulated Investment Token

A real estate company tokenizes a property. Only verified investors can hold the token, and the company needs a way to freeze accounts and recover tokens if compliance requires it.

Extensions: Default account state can be set to frozen so new holders start out locked until KYC clears. Freeze authority handles ongoing compliance actions, like freezing an account that falls out of good standing. A permanent delegate provides a recovery path for legal scenarios. Metadata stores property details and links to legal documentation.

How it works: New investor token accounts start frozen and are thawed after the investor passes KYC. The freeze authority can re-freeze any account later if compliance requires it, and the permanent delegate handles court-ordered recovery if it ever comes to that.

Tools: @solana/kit and @solana-program/token-2022 for the mint, freeze actions, and delegate operations, plus a KYC provider API for investor verification.

A translucent building-shaped block split into glowing fragments, with a faint shield outline in front and a small padlock symbol floating beside it.

4. In-Game Currency with Transaction Fees

Players earn an in-game currency through gameplay and trade it with each other. The studio automatically collects a small fee on every user-to-user transfer to generate ongoing revenue from the game economy.

Extensions: Transfer fees collect a percentage on every token movement; metadata handles branding like names, icons, and descriptions; mint authority is retained so the studio can inject new currency into the economy via gameplay achievements.

How it works: Fees are configured at mint creation and collected automatically on token transfers with no custom program or market smart contract required. When players send tokens to each other, the configured fee is withheld directly inside the recipient’s token account.

Because fees accumulate quietly inside thousands of individual player accounts, the studio’s withdraw-withheld authority can periodically collect these withheld amounts and pool them into a central treasury wallet, without touching player balances.

Tools: @solana/kit, @solana-program/token-2022, a game client SDK, and an administrative backend.

Two translucent player avatars facing each other with glowing tokens flowing between them, small droplets falling into a luminous chest below.

Patterns to Notice

@solana-program/token-2022 is often all you need. For loyalty, subscriptions, in-game currencies, and yield-bearing wrappers, you can usually compose instructions against it from @solana/kit and skip Rust. Reach for Anchor when you need to build custom programs or manage complex state logic that standard extensions don’t cover.

Plan compatibility before mint creation. Not every extension pairs with every other extension. Some combinations are blocked because their behaviors conflict (for example, anything that gates or modifies transfers tends to clash with non-transferability). Most extensions can’t be added after mint creation, so check the compatibility matrix in the Token-2022 docs before committing to a design.

“No Rust required” is not “no centralized authority required.” Permanent delegate, freeze authority, mint authority, and withdraw-withheld authority are centralized controls encoded in extension config. If any are live on your token, make sure to state them clearly in your project’s docs or UI.

Start thinking about what you want to build

You have the fundamentals. The app ideas above are starting points, take one, modify it, make it your own.

Keep building. See you on Discord.

100 Days of Solana is a free daily coding challenge: https://mlh.link/solana-100

n8n for Finance Teams & Fintech: 5 Automations That Replace Manual Spreadsheet Work (Free Workflow JSON)

Finance teams spend hours every week on work that could run automatically: chasing invoices, building cash flow reports, tracking compliance deadlines, processing expense approvals. n8n automates all of it — and because it’s self-hosted, your financial data never leaves your infrastructure.

Here are 5 ready-to-deploy workflow patterns with full JSON.

1. Daily Cash Flow & P&L Briefing

Trigger: Schedule (every weekday at 7:00 AM)
Nodes: Google Sheets → Code → Gmail

Every morning before the CFO opens their laptop, this workflow pulls yesterday’s numbers and sends a formatted HTML briefing.

{
  "name": "Daily Cash Flow Briefing",
  "nodes": [
    {
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": { "interval": [{ "field": "cronExpression", "expression": "0 7 * * 1-5" }] }
      }
    },
    {
      "name": "Get Revenue Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "getAll",
        "sheetId": "YOUR_SHEET_ID",
        "range": "Revenue!A:E"
      }
    },
    {
      "name": "Get Expense Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "getAll",
        "sheetId": "YOUR_SHEET_ID",
        "range": "Expenses!A:E"
      }
    },
    {
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rev = $input.first().json; const exp = $input.last().json;nconst revenue = rev.reduce((s, r) => s + parseFloat(r.amount || 0), 0);nconst expenses = exp.reduce((s, r) => s + parseFloat(r.amount || 0), 0);nconst netIncome = revenue - expenses;nconst margin = revenue > 0 ? ((netIncome / revenue) * 100).toFixed(1) : 0;nconst html = `<h2>Daily P&L — ${new Date().toDateString()}</h2><table border='1' cellpadding='8'><tr><td><b>Revenue</b></td><td>$${revenue.toLocaleString('en-US', {minimumFractionDigits:2})}</td></tr><tr><td><b>Expenses</b></td><td>$${expenses.toLocaleString('en-US', {minimumFractionDigits:2})}</td></tr><tr><td><b>Net Income</b></td><td>$${netIncome.toLocaleString('en-US', {minimumFractionDigits:2})}</td></tr><tr><td><b>Margin</b></td><td>${margin}%</td></tr></table>`;nreturn [{ json: { html, revenue, expenses, netIncome } }];"
      }
    },
    {
      "name": "Email CFO",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "[email protected]",
        "subject": "Daily P&L — {{ $now.format('MMM D, YYYY') }}",
        "message": "{{ $json.html }}",
        "options": { "bodyContentType": "html" }
      }
    }
  ]
}

Adapt the sheet ranges and recipient. Works with QuickBooks exports, Stripe revenue sheets, or any spreadsheet your accounting team already maintains.

2. Invoice Auto-Entry & AP Approval Queue

Trigger: Webhook (from email parser, AP platform, or Zapier/Make migrators)
Nodes: Webhook → Code → IF → [Auto-approve branch | Slack approval branch]

{
  "name": "Invoice AP Workflow",
  "nodes": [
    {
      "name": "Invoice Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": { "path": "invoice-received", "responseMode": "responseNode" }
    },
    {
      "name": "Parse Invoice",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const body = $input.first().json.body;nconst amount = parseFloat(body.amount || body.total || 0);nconst vendor = body.vendor_name || body.from || 'Unknown';nconst dueDate = body.due_date || body.payment_due;nconst poNumber = body.po_number || body.reference || '';nreturn [{ json: { amount, vendor, dueDate, poNumber, rawBody: body, needsApproval: amount >= 2000 } }];"
      }
    },
    {
      "name": "Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "appendOrUpdate",
        "sheetId": "YOUR_SHEET_ID",
        "columns": { "mappingMode": "defineBelow", "value": { "vendor": "={{ $json.vendor }}", "amount": "={{ $json.amount }}", "due_date": "={{ $json.dueDate }}", "po_number": "={{ $json.poNumber }}", "status": "pending", "received_at": "={{ $now.toISO() }}" } }
      }
    },
    {
      "name": "Needs Approval?",
      "type": "n8n-nodes-base.if",
      "parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.needsApproval }}", "value2": true }] } }
    },
    {
      "name": "Slack AP Manager",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#accounts-payable",
        "text": "*Invoice requires approval* nVendor: {{ $json.vendor }}nAmount: ${{ $json.amount }}nDue: {{ $json.dueDate }}nPO: {{ $json.poNumber }}nLog: https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID"
      }
    },
    {
      "name": "Auto-Approve Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "[email protected]",
        "subject": "Invoice auto-approved: {{ $json.vendor }} ${{ $json.amount }}",
        "message": "Invoice from {{ $json.vendor }} for ${{ $json.amount }} (due {{ $json.dueDate }}) has been auto-approved and logged. PO: {{ $json.poNumber }}"
      }
    }
  ]
}

This replaces the AP inbox review process. Under $2,000 invoices log and confirm automatically. Above threshold goes to Slack for human approval — no email threads.

3. Monthly Financial Consolidation Report

Trigger: Schedule (1st of every month, 8:00 AM)
Nodes: Google Sheets (×3) → Merge → Code → Gmail

{
  "name": "Monthly Finance Report",
  "nodes": [
    {
      "name": "Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 1 * *" }] } }
    },
    {
      "name": "Revenue Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Revenue!A:C" }
    },
    {
      "name": "Expenses Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Expenses!A:C" }
    },
    {
      "name": "Payroll Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": { "operation": "getAll", "sheetId": "YOUR_SHEET_ID", "range": "Payroll!A:C" }
    },
    {
      "name": "Consolidate",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const items = $input.all();nconst revenue = items.filter(i => i.json._sheet === 'Revenue').reduce((s,r) => s + parseFloat(r.json.amount||0), 0);nconst expenses = items.filter(i => i.json._sheet === 'Expenses').reduce((s,r) => s + parseFloat(r.json.amount||0), 0);nconst payroll = items.filter(i => i.json._sheet === 'Payroll').reduce((s,r) => s + parseFloat(r.json.amount||0), 0);nconst totalCosts = expenses + payroll;nconst grossProfit = revenue - totalCosts;nconst margin = revenue > 0 ? ((grossProfit/revenue)*100).toFixed(1) : 0;nconst now = new Date(); const lastMonth = new Date(now.getFullYear(), now.getMonth()-1, 1);nconst monthName = lastMonth.toLocaleString('default',{month:'long',year:'numeric'});nconst html = `<h1>Monthly Financial Report — ${monthName}</h1><table border='1' cellpadding='10' style='border-collapse:collapse'><tr><th>Metric</th><th>Amount</th></tr><tr><td>Total Revenue</td><td>$${revenue.toLocaleString()}</td></tr><tr><td>Operating Expenses</td><td>$${expenses.toLocaleString()}</td></tr><tr><td>Payroll</td><td>$${payroll.toLocaleString()}</td></tr><tr><td>Total Costs</td><td>$${totalCosts.toLocaleString()}</td></tr><tr><td><b>Gross Profit</b></td><td><b>$${grossProfit.toLocaleString()}</b></td></tr><tr><td>Net Margin</td><td>${margin}%</td></tr></table>`;nreturn [{ json: { html, monthName, revenue, totalCosts, grossProfit, margin } }];"
      }
    },
    {
      "name": "Email Finance Team",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "[email protected], [email protected]",
        "subject": "Monthly Financial Report — {{ $json.monthName }}",
        "message": "{{ $json.html }}",
        "options": { "bodyContentType": "html" }
      }
    }
  ]
}

Connects revenue, expenses, and payroll sheets. Calculates gross profit, margin, and total costs. One email to the right people every month — no one needs to manually build the deck.

4. Expense Report Approval Pipeline

Trigger: Webhook (from expense submission form, Expensify, or Ramp webhook)
Nodes: Webhook → Code → IF threshold → [Auto-approve | Slack manager → Wait → IF approved → Gmail]

{
  "name": "Expense Approval",
  "nodes": [
    {
      "name": "Expense Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": { "path": "expense-submitted", "responseMode": "responseNode" }
    },
    {
      "name": "Parse Expense",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const b = $input.first().json.body;nreturn [{ json: { submitter: b.employee_name, email: b.employee_email, amount: parseFloat(b.amount||0), category: b.category||'Other', description: b.description||'', receipts_url: b.receipts_url||'', submitted_at: new Date().toISOString(), needs_approval: parseFloat(b.amount||0) >= 500 } }];"
      }
    },
    {
      "name": "Under Threshold?",
      "type": "n8n-nodes-base.if",
      "parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.needs_approval }}", "value2": false }] } }
    },
    {
      "name": "Auto-Approve",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.email }}",
        "subject": "Expense approved: ${{ $json.amount }} — {{ $json.category }}",
        "message": "Hi {{ $json.submitter }},nnYour ${{ $json.amount }} expense ({{ $json.category }}) has been approved automatically. It will be included in the next reimbursement run.nn— Finance Team"
      }
    },
    {
      "name": "Slack Manager",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "@finance-manager",
        "text": "*Expense approval needed*nEmployee: {{ $json.submitter }}nAmount: ${{ $json.amount }}nCategory: {{ $json.category }}nDescription: {{ $json.description }}nReceipts: {{ $json.receipts_url }}nReply 'APPROVE {{ $json.email }}' or 'REJECT {{ $json.email }} [reason]'"
      }
    }
  ]
}

Under your threshold, expenses are approved and confirmed in under a minute. Above threshold, manager gets a structured Slack DM — no inbox digging, no forwarded emails.

5. Compliance & Regulatory Deadline Tracker

Trigger: Schedule (every weekday at 8:00 AM)
Nodes: Google Sheets → Code → Switch (urgency tiers) → [Slack #compliance | Gmail]

{
  "name": "Compliance Deadline Tracker",
  "nodes": [
    {
      "name": "Daily Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 8 * * 1-5" }] } }
    },
    {
      "name": "Get Compliance Calendar",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "getAll",
        "sheetId": "YOUR_COMPLIANCE_SHEET_ID",
        "range": "Deadlines!A:F"
      }
    },
    {
      "name": "Calculate Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const items = $input.all();nconst today = new Date();nreturn itemsn  .filter(item => item.json.status !== 'completed')n  .map(item => {n    const due = new Date(item.json.due_date);n    const daysLeft = Math.ceil((due - today) / (1000 * 60 * 60 * 24));n    let urgency = 'NONE';n    if (daysLeft < 0) urgency = 'OVERDUE';n    else if (daysLeft <= 7) urgency = 'CRITICAL';n    else if (daysLeft <= 14) urgency = 'URGENT';n    else if (daysLeft <= 30) urgency = 'WARNING';n    else if (daysLeft <= 60) urgency = 'NOTICE';n    return { json: { ...item.json, daysLeft, urgency } };n  })n  .filter(item => item.json.urgency !== 'NONE');"
      }
    },
    {
      "name": "Route by Urgency",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "dataType": "string",
        "value1": "={{ $json.urgency }}",
        "rules": [
          { "value2": "OVERDUE", "output": 0 },
          { "value2": "CRITICAL", "output": 0 },
          { "value2": "URGENT", "output": 1 },
          { "value2": "WARNING", "output": 2 },
          { "value2": "NOTICE", "output": 2 }
        ]
      }
    },
    {
      "name": "Slack Compliance Channel",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#compliance",
        "text": ":rotating_light: *{{ $json.urgency }}: {{ $json.deadline_name }}*nDue: {{ $json.due_date }} ({{ $json.daysLeft }} days)nOwner: {{ $json.owner }}nRegulation: {{ $json.regulation }}nAction required: {{ $json.action_required }}"
      }
    },
    {
      "name": "Email Owner — Urgent",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.owner_email }}",
        "subject": "[URGENT] Compliance deadline in {{ $json.daysLeft }} days: {{ $json.deadline_name }}",
        "message": "Hi {{ $json.owner }},nn{{ $json.deadline_name }} is due in {{ $json.daysLeft }} days ({{ $json.due_date }}).nnRegulation: {{ $json.regulation }}nAction required: {{ $json.action_required }}nnPlease confirm completion in the tracking sheet when done."
      }
    },
    {
      "name": "Email Owner — Notice",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.owner_email }}",
        "subject": "Compliance reminder ({{ $json.daysLeft }} days): {{ $json.deadline_name }}",
        "message": "Hi {{ $json.owner }},nnReminder: {{ $json.deadline_name }} is due on {{ $json.due_date }} ({{ $json.daysLeft }} days from today).nnRegulation: {{ $json.regulation }}nAction: {{ $json.action_required }}"
      }
    }
  ]
}

For SOX, GDPR, PCI-DSS, or any regulatory calendar. Replace the Switch with more tiers if needed. All workflow logic is in a JSON file you can audit.

Why n8n for Finance Teams?

n8n (self-hosted) Zapier Make.com
Financial data stays on your servers ✅ ❌ ❌
SOX/audit trail (git-versioned JSON) ✅ ❌ ❌
Cost at 50,000 tasks/month $0 $299+ $99+
Native Postgres/MySQL integration ✅ Add-on Add-on
Complex multi-branch approval logic Native Limited Moderate

For finance, the self-hosting argument is the strongest in the entire industry. SOX compliance requires auditability. SOC2 requires data residency. GDPR requires knowing where customer financial data flows. With n8n running on your own server, you satisfy all three — and you have the workflow JSON in git to prove it.

Get the Templates

All 5 workflows above — plus 10 more finance, operations, and reporting automations — are available as ready-to-import JSON files at FlowKit on Gumroad.

Drop any template into your n8n instance, update your credentials, and it’s running in under 5 minutes.

Built with n8n. Self-hosted. No vendor lock-in.

Beyond the Prompt: How to Build an AI Agent That Actually Learns From Its Mistakes

We’ve all seen the standard pattern of AI development over the past couple of years. You write a system prompt, wrap it in an API call, hook up a vector database for Retrieval-Augmented Generation (RAG), and call it an “autonomous agent.”

But let’s be honest: these systems aren’t actually autonomous. They are stateless function evaluators. They receive a prompt, generate a response, and instantly forget everything that just happened. If they make a mistake on step three of a task, they are highly likely to repeat that exact same mistake on step five. They don’t learn, they don’t adapt, and they don’t grow. They are stuck in a digital version of Groundhog Day.

To build truly autonomous software, we need a profound architectural shift. We need systems that can observe their own performance, write their own journals, and build their own custom tools.

Enter Hermes: an open-source, self-learning cognitive architecture.

In this deep dive, we will break down the core theoretical architecture of Hermes. We’ll look at real production code to see how it implements persistent memory, executes a closed learning loop, and achieves continuous self-improvement through reflection.

(The concepts and code demonstrated here are drawn from my ebook Hermes Agent, The Self-Evolving AI Workforce)

The Core Theoretical Architecture: The Master Craftsman Paradigm

To understand why Hermes represents a shift in agentic AI, consider the analogy of a master craftsman.

A novice apprentice reads a manual, builds a single chair, and immediately forgets the nuances of the wood they used. A master craftsman, however, keeps a detailed journal. In this journal, they document every project: what type of joinery worked, how a specific varnish reacted to humidity, the client’s unique aesthetic preferences, and the shortcuts that saved hours of labor.

Before starting a new project, the master reviews this journal. After completing it, they write a new entry, updating their techniques. Over decades, this journal becomes an extension of their mind.

This is exactly how Hermes operates. Instead of treating memory as a cold database query, Hermes integrates a structured, self-authored “journal” directly into an LLM reasoning loop. It is built on three interlocking theoretical pillars:

  1. Persistent Memory as a Continuous Learning Journal: A living, semantic record of facts and user preferences.
  2. The Closed Learning Loop: Real-time, in-context learning driven by tool execution outcomes and guardrails.
  3. Self-Improvement through Reflection: An asynchronous meta-process where the agent analyzes its own transcripts to build, update, and prune its own codebase of “skills.”

Let’s explore how these three pillars are implemented in code.

Pillar 1: Persistent Memory as a Continuous Learning Journal

Most agent architectures rely on vector databases to handle memory. While vector search is great for retrieving raw documents, it is terrible for maintaining an agent’s identity, behavioral guidelines, or deep user relationships. If a user says, “I prefer concise code reviews with bullet points,” that shouldn’t be a vector chunk that might get retrieved if the cosine similarity is high enough. It should be a core parameter of how the agent behaves.

Hermes solves this by using a structured, LLM-authored plain-text journal split into two files:

  • MEMORY.md: Factual knowledge about the environment, tasks, and accumulated domain knowledge.
  • USER.md: A deep, evolving model of the user’s persona, work style, and expectations.

This memory is initialized directly when the agent starts:

# From run_agent.py - Memory initialization
if not skip_memory:
    mem_config = _agent_cfg.get("memory", {})
    self._memory_enabled = mem_config.get("memory_enabled", False)
    self._user_profile_enabled = mem_config.get("user_profile_enabled", False)
    self._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
    if self._memory_enabled or self._user_profile_enabled:
        from tools.memory_tool import MemoryStore
        self._memory_store = MemoryStore(
            memory_char_limit=mem_config.get("memory_char_limit", 2200),
            user_char_limit=mem_config.get("user_char_limit", 1375),
        )
        self._memory_store.load_from_disk()

Proactive, Asynchronous Introspection

Instead of waiting for the user to explicitly say “remember this,” Hermes autonomously decides what is worth preserving. It does this using a nudge interval (memory.nudge_interval). Every $N$ turns, the agent triggers a background review process guided by the following system prompt:

_MEMORY_REVIEW_PROMPT = (
    "Review the conversation above and consider saving to memory if appropriate.nn"
    "Focus on:n"
    "1. Has the user revealed things about themselves — their persona, desires, "
    "preferences, or personal details worth remembering?n"
    "2. Has the user expressed expectations about how you should behave, their work "
    "style, or ways they want you to operate?nn"
    "If something stands out, save it using the memory tool. "
    "If nothing is worth saving, just say 'Nothing to save.' and stop."
)

Crucially, this review does not block the user interface. A self-improving agent must not sacrifice real-time performance for the sake of learning. Hermes spawns this review in a background thread, cloning its state so the user never experiences latency:

def _spawn_background_review(self, messages_snapshot, review_memory=False, review_skills=False):
    """Spawn a background thread to review the conversation for memory/skill saves."""
    def _run_review():
        review_agent = AIAgent(
            model=self.model,
            max_iterations=16,
            quiet_mode=True,
            platform=self.platform,
            provider=self.provider,
            api_mode=_parent_runtime.get("api_mode"),
            base_url=_parent_runtime.get("base_url"),
            api_key=_parent_runtime.get("api_key"),
            credential_pool=getattr(self, "_credential_pool", None),
            parent_session_id=self.session_id,
            enabled_toolsets=["memory", "skills"],
        )
        review_agent._memory_store = self._memory_store
        review_agent.run_conversation(user_message=prompt, conversation_history=messages_snapshot)
        # ... collect actions and print summary
    t = threading.Thread(target=_run_review, daemon=True, name="bg-review")
    t.start()

By directly inheriting the parent’s _memory_store, the background agent writes directly to disk. On the very next turn, this updated markdown is injected straight into the system prompt:

# From _build_system_prompt - Memory injection
if self._memory_store:
    if self._memory_enabled:
        mem_block = self._memory_store.format_for_system_prompt("memory")
        if mem_block:
            prompt_parts.append(mem_block)
    if self._user_profile_enabled:
        user_block = self._memory_store.format_for_system_prompt("user")
        if user_block:
            prompt_parts.append(user_block)

Pillar 2: The Closed Learning Loop

The second pillar of Hermes is the Closed Learning Loop. Traditional reinforcement learning requires complex external reward functions, and supervised learning requires massive labeled datasets. Hermes bypasses both by using its own tool execution outcomes as a real-time feedback signal.

The loop consists of four rapid-fire stages:

  1. Observation: The agent receives the user’s message and current context.
  2. Action: The agent calls a tool (e.g., writing a file, running a script).
  3. Outcome: The tool executes and returns a raw result (either data or an error trace).
  4. Refinement: The agent inspects the tool output and immediately adjusts its strategy.

This execution loop is managed inside run_conversation():

while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) or self._budget_grace_call:
    # 1. Build API messages from current history
    # 2. Call the model (streaming or non-streaming)
    # 3. Parse tool calls from the response
    # 4. Execute tool calls, append results to messages
    # 5. Continue loop

Because the raw tool outcomes—including stack traces and bash errors—are appended directly to the conversation history, the LLM uses its in-context learning capabilities to debug itself on the fly.

Guardrails and Resource Constraints

To prevent the agent from getting stuck in infinite loops (e.g., repeatedly reading the same directory when a search fails), Hermes uses a ToolCallGuardrailController. If the model repeats a non-progressing action, the guardrail intervenes and injects a synthetic warning:

# From run_agent.py - Guardrail observation after tool execution
function_result = self._append_guardrail_observation(
    function_name, function_args, function_result, failed=is_error,
)

Additionally, Hermes enforces an IterationBudget. This class acts as a form of “learning pressure,” forcing the agent to be highly strategic with its tool usage:

class IterationBudget:
    """Thread-safe iteration counter for an agent."""
    def __init__(self, max_total: int):
        self.max_total = max_total
        self._used = 0
        self._lock = threading.Lock()
    def consume(self) -> bool:
        with self._lock:
            if self._used >= self.max_total:
                return False
            self._used += 1
            return True
    def refund(self) -> None:
        with self._lock:
            if self._used > 0:
                self._used -= 1

If an action is highly efficient (like programmatically running code), the budget can be refunded. If the agent wastes LLM calls on repetitive tasks, it quickly runs out of budget and is forced to summarize and exit. This design teaches the agent to prioritize high-leverage tools.

Context Compression: Lossy Semantic Preservation

As the conversation grows, it risks overflowing the LLM’s context window. Instead of hard-truncating old messages (which causes catastrophic forgetting), Hermes uses a ContextCompressor.

An auxiliary LLM reviews the middle turns of the conversation and summarizes them into a highly dense semantic paragraph, discarding raw execution logs but retaining the core decisions, discoveries, and outcomes:

# From _compress_context
messages, active_system_prompt = self._compress_context(
    messages, system_message, approx_tokens=approx_tokens,
    task_id=effective_task_id,
)

Pillar 3: Self-Improvement Through Reflection

While the closed learning loop handles short-term adaptation within a single conversation, reflection is how Hermes achieves long-term, structural self-improvement.

After a conversation ends, Hermes reviews its performance to extract procedural knowledge. It doesn’t just ask “What did I learn about this user?”—it asks “What did I learn about how to solve this class of problem?”

These procedural insights are compiled into Skills. Skills are structured directories stored in ~/.hermes/skills/ containing:

  • SKILL.md: A step-by-step markdown guide explaining how to perform a task.
  • references/: Session-specific details or condensed knowledge bases.
  • templates/: Starter files and boilerplates.
  • scripts/: Statically re-runnable scripts.

The background agent is pushed to actively seek out these learning opportunities via the _SKILL_REVIEW_PROMPT:

_SKILL_REVIEW_PROMPT = (
    "Review the conversation above and update the skill library. Be "
    "ACTIVE — most sessions produce at least one skill update, even if "
    "small. A pass that does nothing is a missed learning opportunity, "
    "not a neutral outcome.nn"
    # ... detailed guidance on when to update/create skills
    "If you notice two existing skills that overlap, note it in your "
    "reply — the background curator handles consolidation at scale.nn"
    "'Nothing to save.' is a real option but should NOT be the "
    "default. If the session ran smoothly with no corrections and "
    "produced no new technique, just say 'Nothing to save.' and stop. "
    "Otherwise, act."
)

This prompt programs a growth mindset directly into the agent. It treats mistakes, user corrections, and unexpected workarounds not as failures, but as first-class signals to update its procedural skills.

The Skill Lifecycle and the Curator

If an agent continuously creates skills, its library will eventually suffer from bloat, duplicate skills, and outdated APIs. To solve this, Hermes includes a background system service called the Curator (agent/curator.py).

The Curator runs periodically to analyze the skill library, merge overlapping skills, and archive stale ones:

# From AGENTS.md - Curator invariants
# Invariants:
# - Curator only touches skills with created_by: "agent" provenance —
#   bundled + hub-installed skills are off-limits.
# - Never deletes; max destructive action is archive.
# - Pinned skills are exempt from every auto-transition and from the
#   LLM review pass.

By enforcing these safety invariants, Hermes ensures that self-improvement never results in the accidental deletion of core system tools or user-pinned workflows.

The Complete Self-Learning Cycle

When you put these three pillars together, you get a continuous, self-reinforcing learning loop:

[1. Interaction] ──> User talks to Agent, executing tools in real-time.
       │
       ▼
[2. Closed-Loop Adaptation] ──> Agent debugs tool errors within the turn.
       │
       ▼
[3. Persistent Memory] ──> Background thread saves facts to MEMORY.md / USER.md.
       │
       ▼
[4. Skill Update] ──> Reflection creates/updates procedural guides in SKILL.md.
       │
       ▼
[5. Curator Maintenance] ──> Background curator merges, prunes, and archives skills.
       │
       ▼
[6. Next Turn] ──> Updated memory and skills are injected into the next prompt.

This cycle allows the agent to grow more capable, more personalized, and highly specialized the more you use it—without requiring a single gradient update, fine-tuning run, or manual code deployment.

The Shift to Truly Stateful AI

The era of the stateless AI wrapper is drawing to a close. To build applications that can truly operate as autonomous software engineers, research assistants, or personal chiefs of staff, we must build systems that remember, reflect, and adapt.

By combining plain-text semantic journals, real-time tool guardrails, and asynchronous skill curation, the Hermes architecture provides a blueprint for the next generation of agentic AI. It moves us away from static prompt engineering and toward dynamic, evolving systems that learn from their environments.

Let’s Discuss

  1. Memory Storage: Hermes uses structured Markdown files (MEMORY.md/USER.md) rather than a vector database for its core memory. What are the architectural trade-offs of this approach when scaling to millions of tokens of interaction history?
  2. Self-Writing Code: If an agent has the power to autonomously write and update its own “skills” (including executable scripts), what security protocols and sandboxing techniques would you implement to prevent catastrophic system failures or malicious exploits?

The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the ebook Hermes Agent, The Self-Evolving AI Workforce: details link, you can find also my programming ebooks with AI here: Programming & AI eBooks.

How we reduced coding-agent token usage by 17.9% with an MCP server

Coding agents are powerful, but in day-to-day development they waste a lot of tokens on noisy tool output.

A typical cargo test or git status through generic shell tooling sends back a lot of text that an agent doesn’t actually need to reason with. The model still has to read it, pay for it, and carry it in context.

I built Daimonos to reduce that waste.

What Daimonos is

Daimonos is an MCP server focused on the core coding loop:

  • read / write / edit files
  • search code
  • execute commands
  • structured git, cargo, gh, and docker operations
  • batching and script execution for fewer round trips

The key idea is simple: return compact, structured output instead of terminal spam whenever possible.

Why this matters

For coding agents, token usage compounds quickly across many tool calls.

Even when the final answer is simple, the path to get there can be expensive.

We wanted to reduce:

  • total tokens consumed
  • output-token noise
  • wall-clock time per task

Benchmark snapshot

From our benchmark runs:

  • Total tokens: 41,239 -> 33,847 (7,392 saved, -17.9%)
  • Output tokens: 5,842 -> 3,198 (-45.3%)
  • Wall time: -16.4% locally
  • Remote AWS runs: -20.3% cost, -14.0% completion time

Positioning

There are lots of great MCP servers for external APIs and workflow orchestration.

Daimonos is different: it optimizes the core coding tool path itself so agents spend less context on operational noise and more on actual reasoning.

Try it / feedback welcome

Repo: https://github.com/beardfaceguy/daimonos

If you’re running MCP in production, I’d especially love feedback on:

  • where tool-output bloat still hurts most
  • which workflows are still too noisy
  • what would block adoption in your environment