<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SolutionsCraft</title>
    <description>The latest articles on DEV Community by SolutionsCraft (@thesolutionscraft).</description>
    <link>https://dev.to/thesolutionscraft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118342%2F16022825-d93b-4bdd-b703-994a94a1e871.png</url>
      <title>DEV Community: SolutionsCraft</title>
      <link>https://dev.to/thesolutionscraft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thesolutionscraft"/>
    <language>en</language>
    <item>
      <title>Turn Instagram Comments Into DMs With n8n</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/turn-instagram-comments-into-dms-with-n8n-41ne</link>
      <guid>https://dev.to/thesolutionscraft/turn-instagram-comments-into-dms-with-n8n-41ne</guid>
      <description>&lt;p&gt;"Comment LINK and I'll send it to you!" works great for engagement, right up until you're the one copy-pasting the same DM forty times an hour, and inevitably missing a few in the scroll. Instagram comment-to-DM automation fixes exactly this: a keyword in a comment triggers a workflow that replies publicly and privately messages the commenter, in the seconds after they post, every time, with nobody at the keyboard.&lt;/p&gt;

&lt;p&gt;This walkthrough builds the whole thing on n8n and Meta's own Graph API, no third-party automation SaaS in the middle. It's more setup than a no-code app promises, but it's also the same API those apps are calling behind the scenes, so you're not trading capability for control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need an n8n instance (a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; works fine for this, and if this is your first time wiring up an n8n webhook from scratch, &lt;a href="https://solutionscraft.com/automation/first-n8n-workflow" rel="noopener noreferrer"&gt;the folder-watcher walkthrough&lt;/a&gt; covers the editor basics this one builds on) and an Instagram Professional (Business or Creator) account. You'll also need a Meta Developer App with the Instagram product added, so create one at &lt;a href="https://developers.facebook.com/" rel="noopener noreferrer"&gt;developers.facebook.com&lt;/a&gt; if you don't have one yet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One thing worth knowing before you sink an evening into this: sending the private DM needs the &lt;code&gt;instagram_manage_messages&lt;/code&gt; permission, and Meta classes it as a sensitive scope. In development mode it works fine against your own account and any test users you've added, but taking it live for real commenters means submitting the app for App Review first, and reviews for messaging permissions routinely take longer than the more common scopes. Budget for that lag before you promise this to anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Wire up the comment-to-DM automation webhook
&lt;/h2&gt;

&lt;p&gt;In your Meta app, open &lt;strong&gt;Webhooks&lt;/strong&gt;, choose the &lt;strong&gt;Instagram&lt;/strong&gt; product, and subscribe to the &lt;code&gt;comments&lt;/code&gt; field. It'll ask for a callback URL and a verify token, both of which point at an n8n &lt;strong&gt;Webhook&lt;/strong&gt; node you create for this, so turn on "Allow Multiple HTTP Methods" on that node so it accepts both the one-time verification &lt;code&gt;GET&lt;/code&gt; and the real &lt;code&gt;POST&lt;/code&gt; events afterward.&lt;/p&gt;

&lt;p&gt;Meta's verification handshake is simple but exacting: it sends a &lt;code&gt;GET&lt;/code&gt; with &lt;code&gt;hub.mode&lt;/code&gt;, &lt;code&gt;hub.verify_token&lt;/code&gt;, and &lt;code&gt;hub.challenge&lt;/code&gt; query params, and expects the raw &lt;code&gt;hub.challenge&lt;/code&gt; value echoed back as plain text if your token matches, not JSON, not wrapped in anything else. Branch on the HTTP method with an &lt;code&gt;IF&lt;/code&gt; node, and on the &lt;code&gt;GET&lt;/code&gt; branch use a &lt;strong&gt;Respond to Webhook&lt;/strong&gt; node set to "Text" with the response body &lt;code&gt;{{ $json.query["hub.challenge"] }}&lt;/code&gt;, guarded by a check that &lt;code&gt;$json.query["hub.verify_token"]&lt;/code&gt; matches the token you set in the Meta dashboard.&lt;/p&gt;

&lt;p&gt;I don't have a live Meta app to fire a real request at in this environment, so I tested that matching logic standalone against the exact shape Meta's docs describe, rather than n8n's actual node internals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedVerifyToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hub.mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscribe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hub.verify_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expectedVerifyToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hub.challenge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verification failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// handleVerification({ "hub.mode": "subscribe", "hub.verify_token": "my-secret", "hub.challenge": "12345" }, "my-secret")&lt;/span&gt;
&lt;span class="c1"&gt;// -&amp;gt; { status: 200, body: "12345" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ran that in plain Node against a matching token and a deliberately wrong one, and it echoes the challenge only when the token lines up, 403s otherwise, which is exactly the contract Meta's docs describe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Filter for your trigger keyword
&lt;/h2&gt;

&lt;p&gt;Every comment on every post you own fires this webhook, so the next node's job is to throw out everything that isn't the keyword you care about, and to ignore the echo of your own reply so the workflow doesn't loop on itself. Add a &lt;strong&gt;Code&lt;/strong&gt; node on the &lt;code&gt;POST&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OWN_IG_USER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;your IG user id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;OWN_IG_USER_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guide&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
  &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;commenterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;commenterUsername&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blip:&lt;/strong&gt; Somewhere a spec writer decided a webhook that fires on your own reply, right after you reply, was a feature and not a trap. Check the sender id, always.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I verified this filtering logic (case-insensitive keyword match, and skipping the account's own comments) as a standalone script against a sample payload shaped like Meta's documented &lt;code&gt;comments&lt;/code&gt; webhook, and a match returns the parsed fields while a non-match and a self-authored comment both correctly return nothing. Wiring the exact same logic into an n8n Code node is a direct port, not a rewrite, but the live n8n execution itself isn't something I ran here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Reply publicly first
&lt;/h2&gt;

&lt;p&gt;A public reply keeps the conversation visible under the post, a bit of social proof for the next person scrolling by, and it's the same endpoint whether or not you go on to DM anyone. Add an &lt;strong&gt;HTTP Request&lt;/strong&gt; node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://graph.instagram.com/v21.0/{{ $json.commentId }}/replies
Authorization: Bearer &amp;lt;your Instagram access token&amp;gt;
Content-Type: application/json

{ "message": "Sent you a DM with the link! 📩" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Send the private DM
&lt;/h2&gt;

&lt;p&gt;This is the actual comment-to-DM automation piece, turning that public comment into a private message without the commenter ever having messaged you first. Instagram's Messaging API supports exactly one flow for this: a &lt;strong&gt;private reply&lt;/strong&gt;, sent from your own IG account's &lt;code&gt;/messages&lt;/code&gt; endpoint but addressed by comment ID rather than by the commenter's user ID, in another HTTP Request node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://graph.instagram.com/v21.0/{{ $json.myIgUserId }}/messages
Authorization: Bearer &amp;lt;your Instagram access token&amp;gt;
Content-Type: application/json

{
  "recipient": { "comment_id": "{{ $json.commentId }}" },
  "message": { "text": "Here's the link: https://solutionscraft.com/newsletter" }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{{ $json.myIgUserId }}&lt;/code&gt; here is your own Instagram business account's user ID (the same value you'd hardcode as &lt;code&gt;OWN_IG_USER_ID&lt;/code&gt; in Step 2), not the commenter's, since the endpoint is scoped to your account and the recipient is identified through the &lt;code&gt;comment_id&lt;/code&gt; in the body instead.&lt;/p&gt;

&lt;p&gt;Two hard limits worth planning around, straight from Meta's docs: you can only private-reply to a comment within 7 days of it being posted, and you only get &lt;strong&gt;one&lt;/strong&gt; private reply per comment, ever. Send it once and that comment's window is closed for good, so this isn't a node you want firing twice on a retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Land the DM link on a page, not an API call
&lt;/h2&gt;

&lt;p&gt;It's tempting to have the DM link straight into your own signup API to skip a step, but don't point it at an endpoint sitting behind bot protection. Ours, like most newsletter signup forms, is gated by Cloudflare Turnstile, which needs an actual browser solving a challenge, something no link click can do on its own. Send the DM to your normal newsletter signup page instead, and let the page's existing form (Turnstile check, Resend contact creation, automatic welcome email) do the rest exactly the way a human visitor triggers it. The automation's job ends at getting a warm click into that page, not at replacing the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;p&gt;This catches new comments going forward, not a backlog of ones posted before the webhook was live, and it won't touch anything if you edit an existing comment. Instagram also caps API calls at 200 per user per hour (down from the 5,000 you'll see quoted in older tutorials), so a viral post with the keyword in a huge share of the comments can outrun this well before it outruns your patience. And the 7-day/one-reply limits from Step 4 mean this is a first-touch tool, so anything past the initial DM needs a real conversation, not another automated message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Wire up Steps 1 and 2 first and just watch the executions log for a day against your own comments, confirming the keyword filter catches the right ones and ignores your own replies before you let Step 4 actually message a stranger. Once that's solid, turn on the public reply and private DM, and treat App Review for &lt;code&gt;instagram_manage_messages&lt;/code&gt; as the thing to submit in parallel, not after, since it's the step most likely to make you wait. If you'd rather point n8n itself at an AI assistant once this is running, &lt;a href="https://solutionscraft.com/automation/connect-claude-desktop-n8n-instance-mcp-oauth" rel="noopener noreferrer"&gt;connecting n8n's instance-level MCP to Claude Desktop&lt;/a&gt; covers asking it to inspect or adjust a workflow like this one in plain English.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>n8n Instance MCP Now Lets Claude Drive</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/n8n-instance-mcp-now-lets-claude-drive-1gbo</link>
      <guid>https://dev.to/thesolutionscraft/n8n-instance-mcp-now-lets-claude-drive-1gbo</guid>
      <description>&lt;p&gt;Two earlier posts on this site covered MCP one workflow at a time. The &lt;a href="https://solutionscraft.com/automation/enable-n8n-mcp-server-plain-english-workflow" rel="noopener noreferrer"&gt;MCP Server Trigger&lt;/a&gt; exposes a single workflow as a tool, and the &lt;a href="https://solutionscraft.com/automation/connect-external-mcp-server-n8n-ai-agent" rel="noopener noreferrer"&gt;MCP Client Tool&lt;/a&gt; lets an n8n Agent call an external server. Both work fine, but they share the same limit: every new workflow you want an AI assistant to reach means wiring up another trigger, another credential, and another URL to hand out. n8n's &lt;strong&gt;instance-level MCP&lt;/strong&gt; skips all of that. Connect once, with OAuth, and Claude gets a single door into your whole n8n instance instead of a separate one per workflow.&lt;/p&gt;

&lt;p&gt;That's a bigger grant than a single-workflow tool, so this post covers both how to turn it on and how to keep it scoped to what you actually want an assistant touching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need an n8n instance on a version that supports instance-level MCP. It's been available since 2.18.4 on self-hosted Community Edition (and on Cloud and Enterprise from around the same time), with OAuth added as the recommended connection method in 2.31.0. If you're on an older self-hosted version, update first. If you don't have an instance at all yet, a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; gets you one on a current release, instance-level MCP included. You'll also need instance owner or admin permissions (a regular member can't enable this) and Claude Desktop installed on your machine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Turn on instance-level MCP
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings &amp;gt; Instance-level MCP&lt;/strong&gt; and select &lt;strong&gt;Enable MCP access&lt;/strong&gt;. On a self-hosted instance you can do the same thing at deploy time instead, by setting the environment variable &lt;code&gt;N8N_MCP_ACCESS_ENABLED=true&lt;/code&gt; (available from 2.20.0). That's the better option if you provision instances by config rather than clicking through the UI after every deploy.&lt;/p&gt;

&lt;p&gt;Nothing is exposed yet at this point. Enabling the feature just makes the MCP server exist, it doesn't hand any workflow to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Decide which workflows Claude can see
&lt;/h2&gt;

&lt;p&gt;Open any workflow, click its main &lt;strong&gt;...&lt;/strong&gt; menu in the top-right corner, choose &lt;strong&gt;Settings&lt;/strong&gt;, and flip on &lt;strong&gt;Available in MCP&lt;/strong&gt;. That single workflow now shows up as something an MCP client can find and act on. You can do the same from the workflows list: open a workflow card's menu and pick &lt;strong&gt;Enable MCP access&lt;/strong&gt;, or select several workflows at once and use the &lt;strong&gt;Enable workflows&lt;/strong&gt; button in the table header to expose a whole project or folder at once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blip:&lt;/strong&gt; There's also an "Auto-expose new workflows" toggle back in Settings, which quietly hands Claude every workflow you build from now on. Convenient. Also exactly the kind of setting worth remembering you turned on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you'd rather opt in per workflow than trust your future self to remember an auto-expose toggle, leave that setting off and come back to Step 2 each time you build something you want the assistant to reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Generate the connection details
&lt;/h2&gt;

&lt;p&gt;Back in &lt;strong&gt;Settings &amp;gt; Instance-level MCP&lt;/strong&gt;, open &lt;strong&gt;Connection details&lt;/strong&gt; and select &lt;strong&gt;Connect&lt;/strong&gt;. This opens the &lt;strong&gt;Connect a client&lt;/strong&gt; dialog. Confirm you're on the &lt;strong&gt;OAuth (recommended)&lt;/strong&gt; tab. The alternative &lt;strong&gt;Access Token&lt;/strong&gt; tab issues one long-lived bearer token instead, which works but can't be revoked per client the way an OAuth connection can, so a leaked token means rotating access for everyone who uses it, not just the one client that leaked it.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Your client&lt;/strong&gt; dropdown, choose &lt;strong&gt;Claude Desktop&lt;/strong&gt;. n8n shows you a &lt;strong&gt;Server URL&lt;/strong&gt; value tailored to that client. This is the address Claude Desktop authenticates against, not something you construct by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add n8n as a custom connector in Claude Desktop
&lt;/h2&gt;

&lt;p&gt;In Claude Desktop, open &lt;strong&gt;Settings &amp;gt; Connectors&lt;/strong&gt; and select &lt;strong&gt;Add custom connector&lt;/strong&gt;. Give it a name ("n8n MCP" is fine) and paste the Server URL from Step 3 in as the &lt;strong&gt;Remote MCP Server URL&lt;/strong&gt;. Save it, and Claude Desktop prompts you to authorize access to your n8n instance. Approve it, and the OAuth handshake completes without you ever touching a token or a config file by hand.&lt;/p&gt;

&lt;p&gt;This is where instance-level MCP earns its keep over the single-workflow trigger from the earlier post. There, you were editing &lt;code&gt;claude_desktop_config.json&lt;/code&gt; directly and copying a bearer token into an environment variable. Here, the whole connection is a name, a URL, and one authorization click.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; if Claude Desktop connects but can't see any workflows, the fix is almost always Step 2, not the connection itself. Instance-level MCP access and per-workflow "Available in MCP" are two separate switches, and it's easy to flip the first without the second.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Try it from a plain-English prompt
&lt;/h2&gt;

&lt;p&gt;Open a new chat in Claude Desktop and ask for something that maps to a workflow you exposed in Step 2. "What workflows do I have that touch Stripe" is a reasonable first test, since it only needs read access. If the connection is set up right, Claude lists and inspects your workflows without you naming a specific tool.&lt;/p&gt;

&lt;p&gt;From n8n 2.13.0 onward, MCP clients can also build and edit workflows, not just run existing ones, so a prompt like "create a workflow that watches a folder and archives new files" can result in Claude actually assembling nodes in your instance. Check what it produces before you activate it, the same way you'd review a pull request from a new teammate rather than merging it on trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Review and scope what you granted
&lt;/h2&gt;

&lt;p&gt;Each client that connects only gets the permissions it asked for and you approved during that OAuth handshake. Reading workflows without being able to create or run them is a valid, narrower grant than full read, build, and execute access, and it's worth choosing deliberately rather than accepting whatever the default happens to be.&lt;/p&gt;

&lt;p&gt;To see what's actually connected, go back to &lt;strong&gt;Settings &amp;gt; Instance-level MCP&lt;/strong&gt;, open &lt;strong&gt;Connected clients&lt;/strong&gt;, and select &lt;strong&gt;View all&lt;/strong&gt;. You get a table of every OAuth client, its access level, and when it connected, plus a revoke action for anything that shouldn't have access anymore. Get in the habit of checking this table the way you'd check any other service with standing access to your systems, not just once at setup, the same instinct worth applying to &lt;a href="https://solutionscraft.com/automation/audit-n8n-leaked-api-tokens" rel="noopener noreferrer"&gt;auditing an instance for leaked API tokens&lt;/a&gt;: a one-time check is worth less than a habit.&lt;/p&gt;

&lt;p&gt;If you're running self-hosted behind a dedicated hostname (say, &lt;code&gt;n8n-mcp.example.com&lt;/code&gt; in front of the same instance, rather than your normal editor URL), set &lt;code&gt;N8N_MCP_BASE_URL&lt;/code&gt; to that address (available from 2.31.0). n8n advertises that URL during OAuth instead of the instance's default base URL, so clients authenticate against the address you actually intend them to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you hand over the keys
&lt;/h2&gt;

&lt;p&gt;Instance-level MCP is the right tool when you want an assistant working across your automations generally: "find the workflow that does X" or "build me something new," rather than being scoped to one job. That's also exactly why it deserves more caution than the single-workflow MCP Server Trigger from the earlier post. A client with build and edit access here can, in principle, touch anything you exposed to it, not just answer questions about it, and a workflow it builds for you is one you're choosing to trust before you activate.&lt;/p&gt;

&lt;p&gt;Start narrow. Expose the handful of workflows you actually want an assistant reasoning about, leave "Auto-expose new workflows" off until you trust the habit of reviewing what gets shared, and check the Connected clients table every so often. Once that scope feels right, instance-level MCP is the version of "ask n8n to do something in plain English" that scales past a single workflow, which is the whole point of connecting it this way in the first place.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>n8n</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>n8n's Expression Engine Just Got Stricter</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 25 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/n8ns-expression-engine-just-got-stricter-110l</link>
      <guid>https://dev.to/thesolutionscraft/n8ns-expression-engine-just-got-stricter-110l</guid>
      <description>&lt;p&gt;You upgrade n8n, nothing in the changelog looks scary, and a week later a workflow that's run quietly for months starts throwing errors on an expression you haven't touched. Nobody edited it. The data feeding it didn't change shape. The only thing that moved is n8n itself, and buried in the 2.35.0 release notes is the line that explains it: n8n made its &lt;strong&gt;VM expression engine&lt;/strong&gt; the default, replacing the older &lt;strong&gt;Tournament&lt;/strong&gt; engine that had run every &lt;code&gt;{{ }}&lt;/code&gt; expression since the beginning.&lt;/p&gt;

&lt;p&gt;This one's worth taking seriously for a reason the &lt;a href="https://solutionscraft.com/automation/migrate-n8n-notion-workflow-v3-data-source" rel="noopener noreferrer"&gt;Notion v3 migration&lt;/a&gt; wasn't: node versions are pinned per-workflow, so upgrading n8n doesn't silently change how an existing Notion node behaves. The expression engine isn't like that. It's set with an environment variable at the instance level, and it applies to every workflow's next execution the moment you're running 2.35.0 or later, whether or not you asked for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need shell or dashboard access to your n8n instance (to check its version and, if needed, set an environment variable) and edit access to the workflows you want to audit. This applies to self-hosted n8n; if you're on n8n Cloud, more on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Confirm you're actually on the new engine
&lt;/h2&gt;

&lt;p&gt;Check your instance version first (Settings → About in the UI, or &lt;code&gt;n8n --version&lt;/code&gt; from the CLI). Anything 2.35.0 or later is running the VM engine by default unless someone already set &lt;code&gt;N8N_EXPRESSION_ENGINE=legacy&lt;/code&gt;. If you're below 2.35.0, this doesn't apply yet, but it will the moment you upgrade, so it's worth reading before you do rather than after something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Understand what actually changed
&lt;/h2&gt;

&lt;p&gt;Tournament and the new VM engine both run your expression as JavaScript, so the language itself hasn't changed. What changed is how errors during evaluation get handled. Tournament wraps every expression in a try/catch and, depending on the error type, would often swallow a failure quietly and resolve to &lt;code&gt;undefined&lt;/code&gt; rather than stop the workflow. The VM engine runs expressions inside an actual isolated V8 context (via &lt;code&gt;isolated-vm&lt;/code&gt;) with a hard memory limit (128MB by default) and execution timeout (5 seconds by default), and it's meaningfully stricter about which errors it lets pass silently versus which ones it re-throws.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blip:&lt;/strong&gt; A workflow that silently resolves to undefined and limps forward is a workflow with a bug you haven't found yet. I'd rather it just told me.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Blip's not wrong, but "stricter" still means workflows built around the old quiet-failure behavior are the ones at risk now. The pattern to look for is any expression that chains into a property that might not exist on every item, something that used to quietly become &lt;code&gt;undefined&lt;/code&gt; and now has a real chance of throwing instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Find the risky pattern in your workflows
&lt;/h2&gt;

&lt;p&gt;The expressions most likely to behave differently are direct property chains with no guard, especially several levels deep or reaching into data that isn't guaranteed present on every item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$json.customer.address.city&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;customer&lt;/code&gt; or &lt;code&gt;address&lt;/code&gt; is ever missing on an item (a common shape for optional fields from an API, a webhook payload that varies by event type, or an upstream node that doesn't always return the same structure), this is exactly the kind of expression that could tolerate the gap quietly before and won't now. I confirmed the underlying JavaScript behavior directly, since both engines evaluate real JS under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Cannot read properties of undefined (reading 'city')&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// undefined, no throw&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// "Unknown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search your workflows for expressions with two or more chained &lt;code&gt;.&lt;/code&gt; property accesses and no &lt;code&gt;?.&lt;/code&gt; anywhere in the chain. That's your audit list. Pay closest attention to workflows using an Error Trigger, or ones with sub-nodes that reference data from earlier in the flow across an AI Agent connection; paired-item resolution across those boundaries is one of the specific spots the n8n team had to patch for consistency between the two engines, which tells you it's a genuine edge case, not a hypothetical one.&lt;/p&gt;

&lt;p&gt;Opening every workflow by hand to eyeball its expressions doesn't scale past a handful. If you're self-hosted and have CLI access, n8n's &lt;code&gt;export:workflow&lt;/code&gt; command dumps every workflow to JSON in one shot, and from there it's a normal grep problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;n8n &lt;span class="nb"&gt;export&lt;/span&gt;:workflow &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./audit/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-roE&lt;/span&gt; &lt;span class="s1"&gt;'\{\{[^}]*\.[a-zA-Z_]+\.[a-zA-Z_]+[^}]*\}\}'&lt;/span&gt; ./audit/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'?\.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested this exact pattern against a sample workflow JSON with one guarded and one unguarded expression, and it correctly pulled only the unguarded one. It's a starting list, not a verdict (some flagged expressions will turn out fine once you check whether that particular field can actually be missing), but it turns "audit the instance" into "review a short list" instead of "click through every workflow."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Fix what you find
&lt;/h2&gt;

&lt;p&gt;For each risky expression, add optional chaining and a sensible fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$json.customer.address?.city&lt;span class="w"&gt; &lt;/span&gt;??&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unknown"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a one-character-per-link change (&lt;code&gt;?.&lt;/code&gt; instead of &lt;code&gt;.&lt;/code&gt;) plus a &lt;code&gt;??&lt;/code&gt; default at the end, and it makes the expression behave the same way under either engine: no silent &lt;code&gt;undefined&lt;/code&gt;, no thrown error, just the fallback value you chose. If an expression is doing something more involved than a property chain (a multi-step transform, several fallbacks chained together), it's usually clearer, and easier to test, moved into a Code node instead, where you can wrap it in an actual &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; and decide exactly what happens on failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Use the rollback as a stopgap, not a fix
&lt;/h2&gt;

&lt;p&gt;If you've just upgraded and don't have time to audit before your workflows run again, you can buy yourself room with an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N8N_EXPRESSION_ENGINE=legacy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This restores Tournament's behavior instance-wide, immediately. Treat it as a pause button, not a destination. The VM engine is where n8n is investing going forward (it's also faster on repeated expressions thanks to code caching, and the isolation is a real security improvement), so the goal is auditing your way back to the default, not living on &lt;code&gt;legacy&lt;/code&gt; indefinitely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Set the variable, restart, confirm your previously-broken workflow runs clean again, then work through Step 3's audit list on your own schedule instead of your production traffic's.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you've already upgraded and something's actively failing, set &lt;code&gt;N8N_EXPRESSION_ENGINE=legacy&lt;/code&gt; first to stop the bleeding, then work through Step 3's search for unguarded property chains before removing the rollback. If you haven't upgraded yet, do the audit now, while it's a code review instead of an incident. Either way, the fix is the same one-line change repeated as many times as your audit turns up. It's the same audit-before-it-bites-you shape as &lt;a href="https://solutionscraft.com/automation/audit-n8n-leaked-api-tokens" rel="noopener noreferrer"&gt;checking your instance for leaked n8n API tokens&lt;/a&gt;, just for an expression engine instead of a credential.&lt;/p&gt;

&lt;p&gt;If you're on n8n Cloud, this engine switch already happened for you as part of a coordinated rollout n8n tested ahead of time. You don't control when it lands, but you also don't have to schedule the audit around your own upgrade timing. If owning that timing yourself isn't something you want to keep doing release after release, a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; is worth a look.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Is Your n8n API Key Already Leaked?</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 18 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/is-your-n8n-api-key-already-leaked-321n</link>
      <guid>https://dev.to/thesolutionscraft/is-your-n8n-api-key-already-leaked-321n</guid>
      <description>&lt;p&gt;You copy an n8n API key into a &lt;code&gt;.env&lt;/code&gt; file to test something quick, commit the whole folder because you're in a hurry, notice a minute later, and delete the file in the next commit. Problem solved, right? Except git doesn't forget. The key is still sitting in that first commit, reachable by anyone who can run &lt;code&gt;git log -p&lt;/code&gt; against your history, whether or not it's still in the file today.&lt;/p&gt;

&lt;p&gt;That's not a hypothetical. GitGuardian &lt;a href="https://thehackernews.com/2026/08/leaked-n8n-api-tokens-exposed-live.html" rel="noopener noreferrer"&gt;scanned public GitHub commits&lt;/a&gt; in early August 2026 and found 4,576 distinct n8n API tokens sitting out in the open, tied to 1,255 different hostnames. Of the 896 instances they could actually reach, 321 accepted at least one of the leaked tokens, no exploit required, no password guessing, just an unrotated token from an old commit. A valid token hands over workflows, execution history, stored data tables, and enough about your credentials to know exactly what to go after next.&lt;/p&gt;

&lt;p&gt;This walkthrough covers two things: a script that actually finds n8n keys in your git history (not just anything that looks vaguely like a secret), and what to do once you find one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blip:&lt;/strong&gt;"I deleted the file" and "I removed the secret" are not the same sentence. Git remembers the first draft even after you've moved on from it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need shell access to a local clone of any repo you want to check, and access to your n8n instance's &lt;strong&gt;Settings &amp;gt; n8n API&lt;/strong&gt; page for the rotation step later. No n8n instance changes happen until Step 3, so it's safe to run the scan against a repo without touching anything live yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Know what you're looking for
&lt;/h2&gt;

&lt;p&gt;n8n's public API keys are JWTs, they start with &lt;code&gt;eyJ&lt;/code&gt;, the standard base64 prefix for JSON, followed by two more base64 segments separated by dots. That shape isn't unique to n8n, plenty of other services hand out JWTs too, so a naive "does this look like a token" grep gets noisy fast. What &lt;em&gt;is&lt;/em&gt; specific to n8n is what's inside the payload: n8n signs its API keys with &lt;code&gt;"iss": "n8n"&lt;/code&gt; and &lt;code&gt;"aud": "public-api"&lt;/code&gt; claims. Decode the middle segment of any JWT and you can tell immediately whether it's one of these keys or something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Scan your git history
&lt;/h2&gt;

&lt;p&gt;This is the part worth actually running, not just reading. It walks every commit (&lt;code&gt;git log -p --all&lt;/code&gt;), pulls out anything shaped like a JWT, decodes each candidate's payload, and only flags the ones whose &lt;code&gt;iss&lt;/code&gt;/&lt;code&gt;aud&lt;/code&gt; claims match n8n's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Scans the full git history (not just the working tree) of the current repo&lt;/span&gt;
&lt;span class="c"&gt;# for n8n public API keys. n8n API keys are JWTs, so a plain "looks like a&lt;/span&gt;
&lt;span class="c"&gt;# JWT" grep also catches unrelated tokens; this decodes each candidate's&lt;/span&gt;
&lt;span class="c"&gt;# payload and only flags the ones whose issuer/audience claims match n8n's.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;candidates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$candidates&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No JWT-shaped strings found in git history."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; token&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;payload_b64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload_b64&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'_-'&lt;/span&gt; &lt;span class="s1"&gt;'/+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;2&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=="&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    3&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;padded&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;="&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$padded&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"iss": *"n8n"'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"aud": *"public-api"'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;found &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FOUND n8n API key in git history: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;:0:24&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;:&lt;span class="p"&gt; -8&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" payload: &lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$candidates&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0]&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No n8n API keys found (JWTs present but none matched n8n's issuer/audience claims)."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt; n8n API key(s) found in git history. Deleting the file today does NOT remove"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"them, they're still reachable via 'git log -p' or any clone. Rotate every key found"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"here, then scrub history (git filter-repo / BFG) if this repo is or was ever public."&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;scan-n8n-tokens.sh&lt;/code&gt;, &lt;code&gt;chmod +x&lt;/code&gt; it, and run it from inside the repo you want to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;./scan-n8n-tokens.sh

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested this against two throwaway repos before writing it into this post. One had an n8n key committed and then "removed" in a later commit, exactly the scenario above, and the script caught it and printed the payload showing &lt;code&gt;iss: n8n, aud: public-api&lt;/code&gt;. The other had a JWT-shaped decoy with a completely different issuer, and the script correctly ignored it, so you won't get paged for every unrelated token your history happens to contain. A clean repo with no JWTs at all reports that plainly and exits 0, so you can drop this into a pre-merge CI check without it crying wolf.&lt;/p&gt;

&lt;p&gt;Run it against every repo that's ever touched an n8n key, including ones you've since made private. GitGuardian's numbers came from &lt;em&gt;public&lt;/em&gt; GitHub commits specifically, but a repo you flipped to private after the fact doesn't retroactively un-leak anything that was already indexed or cloned while it was public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rotate anything you find
&lt;/h2&gt;

&lt;p&gt;If the scan turns up a hit, treat that key as burned, full stop, whether or not you can prove anyone actually used it. Rotation itself is a UI action in n8n, not something scriptable through the public API (you'd otherwise need a valid key just to revoke itself, which doesn't help you here):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings &amp;gt; n8n API&lt;/strong&gt; on the affected instance.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create an API key&lt;/strong&gt; , give it a label you'll recognize later, and set an &lt;strong&gt;Expiration&lt;/strong&gt; if your plan supports it, an expiring key limits exactly this kind of damage the next time a token slips into a commit.&lt;/li&gt;
&lt;li&gt;Update every place that used the old key, your automation scripts, CI secrets, anywhere it was configured, to the new one.&lt;/li&gt;
&lt;li&gt;Go back to &lt;strong&gt;Settings &amp;gt; n8n API&lt;/strong&gt; , find the old key in the list, and select &lt;strong&gt;Delete&lt;/strong&gt; next to it. It's revoked immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do this even if the leaked key is months old and "probably fine." GitGuardian's whole point was that these tokens don't expire on their own, an n8n API key created without an expiration date stays valid indefinitely until someone deletes it, so "probably fine" and "still fully valid" are the same thing from the token's perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Get it out of history too, if the repo was ever public
&lt;/h2&gt;

&lt;p&gt;Rotating the key stops it from working, but the string itself is still sitting in your git history, which is untidy and, if this repo is open source or shared, worth cleaning up so the next contributor doesn't find it and wonder if it's still live. &lt;code&gt;git filter-repo&lt;/code&gt; (or the older BFG Repo-Cleaner) can strip a specific string from every commit that ever contained it. This rewrites history, so it needs a force-push and coordination with anyone else who has the repo cloned, which is a bigger operation than this post covers, but it's the right follow-up once the immediate danger (a working, unrevoked key) is handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;p&gt;Scanning your own repo's history only tells you what's true for &lt;em&gt;that&lt;/em&gt; repo. It can't tell you whether someone already cloned it while the key was live, whether it's sitting in a CI log, a Slack message, or a support ticket screenshot, or whether it leaked through a channel that has nothing to do with git at all. Treat "I found it and rotated it" as closing the door you know about, not a guarantee nothing walked through it first.&lt;/p&gt;

&lt;p&gt;It also won't catch a key that's still &lt;em&gt;currently&lt;/em&gt; committed in your working tree, only what's reachable through history. Run a plain &lt;code&gt;git grep&lt;/code&gt; for &lt;code&gt;eyJ&lt;/code&gt; against your current checkout too, and consider adding GitHub's push protection (Settings &amp;gt; Code security &amp;gt; Push protection) on any repo that might touch one of these keys again, it blocks a commit containing a recognizable secret pattern before it ever reaches the remote, which is a much better place to stop this than a post-hoc scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Run the scan today against every repo that has ever had an n8n key anywhere near it, not just the ones you remember. If it comes back clean, good, now you know, and it's a five-minute script you can rerun anytime you're not sure. If it finds something, rotate the key before you do anything else, the five minutes that takes is a lot cheaper than being one of the 321 instances that answered when GitGuardian knocked. It's one item on a short list of n8n instance-hardening checks worth running together, alongside the &lt;a href="https://solutionscraft.com/automation/audit-and-patch-n8n-sandbox-escape-vulnerability" rel="noopener noreferrer"&gt;sandbox-escape vulnerability patch&lt;/a&gt; and the &lt;a href="https://solutionscraft.com/automation/n8n-fromai-sandbox-escape-patch" rel="noopener noreferrer"&gt;$fromAI prototype-leak escape hatch&lt;/a&gt;, both quiet-until-someone-finds-it bugs in the same vein as an unrotated key.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>security</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>When n8n's $fromAI Becomes an Escape Hatch</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/when-n8ns-fromai-becomes-an-escape-hatch-3clg</link>
      <guid>https://dev.to/thesolutionscraft/when-n8ns-fromai-becomes-an-escape-hatch-3clg</guid>
      <description>&lt;p&gt;&lt;code&gt;$fromAI()&lt;/code&gt; is the little function that makes n8n's AI Agent tools feel almost magic: instead of hardcoding a value in a tool's parameters, you write something like &lt;code&gt;{{ $fromAI('city', 'the city the user asked about', 'string') }}&lt;/code&gt; and let the agent fill it in at call time based on what the user actually said. n8n's bi-weekly security update on 20 August 2026 disclosed that this exact helper had a hole in it. A crafted placeholder name could leak a live reference to one of n8n's own host prototypes, and from there, an attacker could walk the prototype chain up to the &lt;code&gt;Function&lt;/code&gt; constructor and run arbitrary code as the n8n process. Rated High with a CVSS 4.0 score of 8.7, tracked as &lt;a href="https://github.com/n8n-io/n8n/security/advisories/GHSA-9x83-43r8-5hwc" rel="noopener noreferrer"&gt;GHSA-9x83-43r8-5hwc&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If that sounds familiar, it should. It's a different bug from the expression-sandbox escape &lt;a href="https://solutionscraft.com/automation/audit-and-patch-n8n-sandbox-escape-vulnerability" rel="noopener noreferrer"&gt;covered here in July&lt;/a&gt; (GHSA-gv7g-jm28-cr3m), but the shape of the problem is the same: code that's supposed to run inside a sandbox finds a crack in the wall.&lt;/p&gt;

&lt;p&gt;This walkthrough is for &lt;strong&gt;self-hosted instances&lt;/strong&gt; you manage yourself, Docker or npm/pnpm. If you're on n8n Cloud, patching is handled for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who's affected
&lt;/h2&gt;

&lt;p&gt;Like most expression-sandbox bugs, this one needs an authenticated account with permission to build or edit workflows, so it's not a drive-by, unauthenticated attack. But the specific surface here is narrower than the July bug: exploitation goes through &lt;code&gt;$fromAI()&lt;/code&gt;, which only exists inside tool parameters wired up to an &lt;strong&gt;AI Agent&lt;/strong&gt; node. If your instance doesn't use AI Agent workflows with tool nodes at all, this particular door isn't open to you, though you should still patch, since it costs nothing and the next advisory might not be so forgiving.&lt;/p&gt;

&lt;p&gt;If you do run AI Agent tools, the risk is real: anyone who can add or edit a tool node (again, not just admins) could craft a malicious &lt;code&gt;$fromAI()&lt;/code&gt; placeholder, and a successful exploit runs commands as the n8n process itself, which means it can reach &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; and every credential n8n has stored behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Affected versions:&lt;/strong&gt; anything below &lt;code&gt;2.35.4&lt;/code&gt;, the &lt;code&gt;2.36.0&lt;/code&gt;–&lt;code&gt;2.36.1&lt;/code&gt; range, and anything below &lt;code&gt;1.123.73&lt;/code&gt; on the legacy 1.x maintenance train. &lt;strong&gt;Patched versions:&lt;/strong&gt; &lt;code&gt;2.35.4&lt;/code&gt;, &lt;code&gt;2.36.2&lt;/code&gt;, and &lt;code&gt;1.123.73&lt;/code&gt;, each patched within its own train.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blip:&lt;/strong&gt; A parameter that's supposed to describe itself to an AI model isn't supposed to describe a way into the host filesystem. And yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need shell access to the machine (or container) running n8n, so you can check its version and, if needed, pull a new image or run an npm install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check your installed version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;npm/pnpm install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;n8n &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container-name&amp;gt; n8n &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Run it against the affected ranges
&lt;/h2&gt;

&lt;p&gt;n8n currently keeps three maintenance trains alive at once: &lt;code&gt;1.123.x&lt;/code&gt; for teams still on the pre-2.0 line, plus the current and previous &lt;code&gt;2.x&lt;/code&gt; minors (&lt;code&gt;2.35.x&lt;/code&gt; and &lt;code&gt;2.36.x&lt;/code&gt;). That means eyeballing one version range against your version string isn't enough here, so this script checks all three and exits non-zero if you're exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Checks an n8n version string against GHSA-9x83-43r8-5hwc's affected ranges:&lt;/span&gt;
&lt;span class="c"&gt;# vulnerable: &amp;lt;2.35.4 OR (&amp;gt;=2.36.0 AND &amp;lt;2.36.2) OR on the legacy 1.x train, &amp;lt;1.123.73&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?Usage:&lt;span class="p"&gt; check-n8n-version.sh &amp;lt;n8n-version&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

ver_lt&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="nv"&gt;lower&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lower&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
ver_ge&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;major&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="p"&gt;%%.*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$major&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if &lt;/span&gt;ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"1.123.73"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VULNERABLE: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is below 1.123.73 on the legacy 1.x train"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
else
  if &lt;/span&gt;ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.35.4"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VULNERABLE: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is below 2.35.4"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;elif &lt;/span&gt;ver_ge &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.36.0"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.36.2"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VULNERABLE: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is in the 2.36.0 pre-patch range"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is patched against GHSA-9x83-43r8-5hwc"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;check-n8n-version.sh&lt;/code&gt;, then pipe your actual version straight in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x check-n8n-version.sh
./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# or, for Docker:&lt;/span&gt;
./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container-name&amp;gt; n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Patch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker / Docker Compose&lt;/strong&gt; , pin the image tag to a patched version for whichever train you're on and redeploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n:2.36.2&lt;/span&gt; &lt;span class="c1"&gt;# or 2.35.4 to stay on the 2.35.x train, or 1.123.73 on the legacy 1.x line&lt;/span&gt;


&lt;span class="s"&gt;docker compose pull&lt;/span&gt;
&lt;span class="s"&gt;docker compose up -d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;npm/pnpm global install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; n8n@2.36.2

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Confirm the patch took
&lt;/h2&gt;

&lt;p&gt;Re-run the same check, it should now report &lt;code&gt;OK&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually leaked, and why it matters even for "safe-looking" tools
&lt;/h2&gt;

&lt;p&gt;The part worth understanding, not just patching around: &lt;code&gt;$fromAI()&lt;/code&gt; takes a placeholder name as its first argument, a plain string you write yourself when you build the tool node, like &lt;code&gt;'city'&lt;/code&gt; in the example above. The bug was that n8n didn't fully validate that name before using it internally, so a crafted one could collide with a reserved prototype key instead of behaving like an ordinary string. That let an attacker reach a live reference to one of n8n's own host prototype objects from inside the expression sandbox, something the sandbox exists specifically to prevent. From a host prototype, the well-worn path to &lt;code&gt;Function&lt;/code&gt; and arbitrary code execution is short.&lt;/p&gt;

&lt;p&gt;The reason this matters beyond "patch and move on": the vulnerable code path lives in how &lt;code&gt;$fromAI()&lt;/code&gt; itself parses its arguments, not in what the AI model does with the value afterward. A tool node that looks completely benign, one where the AI is only ever asked to fill in something like a city name or a ticket ID, was just as exposed as one handling something sensitive. The danger was never in what the parameter was &lt;em&gt;for&lt;/em&gt;. It was in the placeholder name sitting right there in the node's configuration, which is exactly the kind of thing a workflow-editor account can already touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you can't upgrade right away
&lt;/h2&gt;

&lt;p&gt;n8n's own advisory lists a few stopgaps, worth noting because none of them close the hole on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict workflow-build and tool-node access to people you fully trust, since exploitation requires that permission.&lt;/li&gt;
&lt;li&gt;Disable AI Agent tool nodes that use &lt;code&gt;$fromAI()&lt;/code&gt; if you can live without them until you patch.&lt;/li&gt;
&lt;li&gt;Run the n8n process itself under a dedicated, unprivileged OS account, so a successful exploit has less to work with even if it lands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat all three as a stopgap, not a substitute for patching. Anyone who still has workflow-editor access can still trigger it.&lt;/p&gt;

&lt;p&gt;If keeping up with a bi-weekly cadence of advisories like this one isn't something you want to own yourself, a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; hands this one, and every future one, to n8n's own team instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link, if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Check your version today if you run any AI Agent workflow with tool nodes, whether or not you remember using &lt;code&gt;$fromAI()&lt;/code&gt; directly. If you're running one of the setups from the &lt;a href="https://solutionscraft.com/automation/enable-n8n-mcp-server-plain-english-workflow" rel="noopener noreferrer"&gt;MCP Server Trigger&lt;/a&gt; or &lt;a href="https://solutionscraft.com/automation/connect-external-mcp-server-n8n-ai-agent" rel="noopener noreferrer"&gt;MCP Client Tool&lt;/a&gt; walkthroughs, this is the same trust boundary those posts described, patch it the same way you'd patch any other high-severity RCE: today, not on the next maintenance window.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>security</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Migrate an n8n Notion Workflow to the v3 Node's Data Source Model</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/migrate-an-n8n-notion-workflow-to-the-v3-nodes-data-source-model-2p33</link>
      <guid>https://dev.to/thesolutionscraft/migrate-an-n8n-notion-workflow-to-the-v3-nodes-data-source-model-2p33</guid>
      <description>&lt;p&gt;Notion shipped a breaking change to its API: a database is no longer a single thing you query directly. It's now a container that holds one or more &lt;strong&gt;data sources&lt;/strong&gt; , and the pages you actually want live under a data source, not the database itself. n8n's Notion node was overhauled to a new v3 to match — and the sharp edge is that database IDs are no longer accepted for database-page query or create operations. If a workflow you built a while back still points at a raw database ID once it's running on the new node version, those operations stop resolving.&lt;/p&gt;

&lt;p&gt;The good news: for the overwhelming majority of real setups, this is a small fix, not a rebuild. Before this API version, a Notion database could only ever have one data source — so unless you've deliberately split a database into multiple sources since then, migrating just means pointing the node at "the one data source under this database" instead of the database itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need edit access to an existing n8n workflow that uses the &lt;strong&gt;Notion&lt;/strong&gt; node against a database — Database Page operations (Get, Get Many, Create, Update) or a Notion Trigger watching a database. This is a migration for something you already have running, not a from-scratch setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Confirm the node hasn't already moved
&lt;/h2&gt;

&lt;p&gt;n8n versions nodes per-workflow: a workflow keeps running on whatever node version it was built with, even after a newer version ships. Nothing breaks on its own just because v3 exists — it only starts to matter once you (or a teammate) update that specific Notion node, or rebuild the workflow from scratch with a fresh node. Open the workflow and double-click the Notion node; if it's still resolving your database fine, it's on the older version and this migration is something you're doing proactively, not fixing an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Update the node to v3
&lt;/h2&gt;

&lt;p&gt;With the Notion node open, look for the node-settings menu (the "..." in the node's panel) — if an update is available, it'll offer to move the node to its latest version. Confirm the update. This is the point where the node's fields actually change shape, so do it on a duplicate of the workflow first if it's anything business-critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Swap Database for Data Source
&lt;/h2&gt;

&lt;p&gt;This is the actual breaking change. Wherever the Database Page resource previously asked you to pick a &lt;strong&gt;Database&lt;/strong&gt; — the usual n8n resource-locator field, with "From list," "By ID," and "By URL" modes — v3 asks for a &lt;strong&gt;Data Source&lt;/strong&gt; instead. Switch to "From list" and pick the data source under the same database you were already using; per Step 1's caveat, that's almost always the only one listed. If you were hardcoding the old database ID as an expression, you'll need to replace it with the data source's ID the same way — a raw database ID won't resolve here anymore.&lt;/p&gt;

&lt;p&gt;Do this for every Database Page operation in the workflow — Get, Get Many, Create, and Update all take the same field, so it's easy to fix one and miss another further down the canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Update the Notion Trigger too, if you're using one
&lt;/h2&gt;

&lt;p&gt;The Notion Trigger node got the same treatment — it now also resolves against a data source rather than a database ID directly. If your workflow starts from a Notion Trigger watching for new or updated database pages, repeat Step 3's field swap there before you move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test before you save it live
&lt;/h2&gt;

&lt;p&gt;Run the Notion node manually (or execute the whole workflow once against test data) before deactivating your safety copy from Step 2. Confirm pages come back with the properties you expect, and that a Create actually lands in the right place in Notion — a data source that looks right in the dropdown but was picked in a hurry is the easiest way to end up writing to the wrong list.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If you've split a single Notion database into multiple data sources — a newer Notion feature — "From list" will show more than one option, and you'll need to know which data source actually holds the pages your workflow cares about before you pick.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is the same "audit before it bites you" pattern as a security patch, just for an API contract instead of a CVE: nothing forces you to move until you touch the node, but the fix is small if you do it deliberately rather than after something in production starts throwing errors. The &lt;a href="https://solutionscraft.com/automation/n8n-vm-expression-engine-audit" rel="noopener noreferrer"&gt;VM expression engine default change&lt;/a&gt; is the same shape from the other direction: a quiet default flip instead of a deprecated field, but the same discipline of auditing before you're forced to. For the security side of that same discipline, see &lt;a href="https://solutionscraft.com/automation/audit-n8n-leaked-api-tokens" rel="noopener noreferrer"&gt;auditing an n8n instance for leaked API tokens&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Connect an External MCP Server to an n8n AI Agent with the MCP Client Tool Node</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 28 Aug 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/connect-an-external-mcp-server-to-an-n8n-ai-agent-with-the-mcp-client-tool-node-1mg5</link>
      <guid>https://dev.to/thesolutionscraft/connect-an-external-mcp-server-to-an-n8n-ai-agent-with-the-mcp-client-tool-node-1mg5</guid>
      <description>&lt;p&gt;An earlier post covered the &lt;strong&gt;MCP Server Trigger&lt;/strong&gt; node: turning an n8n workflow into a tool an external AI assistant can call. The &lt;strong&gt;MCP Client Tool&lt;/strong&gt; node does the reverse — it lets an n8n &lt;strong&gt;AI Agent&lt;/strong&gt; call tools hosted on &lt;em&gt;someone else's&lt;/em&gt; MCP server, including a server you don't run in n8n at all. Point an agent at any MCP-compliant endpoint and its tools show up alongside your other n8n tool nodes, ready to be called mid-conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need an n8n AI Agent workflow (or a willingness to build a small one) and the URL of an MCP server to connect to. This walkthrough reuses the MCP server from the earlier post — the same n8n instance can be both an MCP server for one workflow and an MCP client for another — but any SSE-based MCP endpoint works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Add an AI Agent node
&lt;/h2&gt;

&lt;p&gt;Create a new workflow and add an &lt;strong&gt;AI Agent&lt;/strong&gt; node. The AI Agent needs a &lt;strong&gt;Chat Model&lt;/strong&gt; connected before it can do anything — click the &lt;strong&gt;Chat Model&lt;/strong&gt; connector on the node and pick a provider (OpenAI, Anthropic, Ollama, or any other supported Chat Model sub-node). Without a model attached, the agent has no way to decide which tool to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the MCP Client Tool node
&lt;/h2&gt;

&lt;p&gt;With the AI Agent node open, click the &lt;strong&gt;+&lt;/strong&gt; under &lt;strong&gt;Tools&lt;/strong&gt; and add an &lt;strong&gt;MCP Client Tool&lt;/strong&gt; node. This is a sub-node — it doesn't run on its own, it plugs into the agent as one of its available tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Point it at the external MCP server
&lt;/h2&gt;

&lt;p&gt;In the MCP Client Tool node's panel, set the &lt;strong&gt;SSE Endpoint&lt;/strong&gt; field to the MCP server's URL — for an n8n MCP Server Trigger, this is the same &lt;code&gt;/mcp/&amp;lt;path&amp;gt;/sse&lt;/code&gt; URL from that workflow's trigger node panel. For a third-party MCP server, use whatever SSE endpoint its documentation gives you.&lt;/p&gt;

&lt;p&gt;Set &lt;strong&gt;Authentication&lt;/strong&gt; to match what the server expects. The node supports &lt;strong&gt;Bearer Auth&lt;/strong&gt; , a &lt;strong&gt;Header Auth&lt;/strong&gt; credential (for a single custom header), &lt;strong&gt;Multiple Headers Auth&lt;/strong&gt; (for servers that need more than one, like an API key plus a username), &lt;strong&gt;OAuth2&lt;/strong&gt; , or &lt;strong&gt;None&lt;/strong&gt; if the server doesn't require authentication. If you're connecting to the MCP Server Trigger from the earlier post, choose Bearer Auth and use the same token you generated there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Choose which tools to expose
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Tools to Include&lt;/strong&gt; field controls how much of the external server's tool catalog the agent sees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All&lt;/strong&gt; — every tool the server exposes is available to the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selected&lt;/strong&gt; — pick specific tools by name, and only those are exposed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All Except&lt;/strong&gt; — expose everything except the tools you list in &lt;strong&gt;Tools to Exclude&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with &lt;strong&gt;Selected&lt;/strong&gt; and pick one tool while you're testing. An agent handed a large, unfiltered tool list is more likely to call the wrong one — narrowing the set is often the fastest fix if the agent seems to guess instead of reasoning about which tool applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test it from the agent's chat
&lt;/h2&gt;

&lt;p&gt;Save and activate the workflow, then open the AI Agent node's chat panel and ask for something that maps to one of the exposed tools, in plain English. If the connection and auth are correct, the agent lists the MCP server's tool(s) internally, calls the right one, and returns the result in its reply.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If the agent can't see any tools, double check the SSE endpoint first — a URL pointing at the base MCP path instead of the &lt;code&gt;/sse&lt;/code&gt; suffix is the most common cause, and it fails silently rather than throwing a clear connection error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A single AI Agent can hold multiple MCP Client Tool nodes side by side, each pointed at a different external server — combining your own n8n-hosted tools with a partner's or vendor's MCP server in one conversation. Between this node and the &lt;a href="https://solutionscraft.com/automation/enable-n8n-mcp-server-plain-english-workflow" rel="noopener noreferrer"&gt;MCP Server Trigger&lt;/a&gt; from the earlier post, the same n8n instance can sit on both ends of an MCP connection: serving tools to one agent while consuming another's. If you'd rather expose your whole instance at once instead of wiring up tools workflow by workflow, &lt;a href="https://solutionscraft.com/automation/connect-claude-desktop-n8n-instance-mcp-oauth" rel="noopener noreferrer"&gt;n8n's instance-level MCP&lt;/a&gt; covers that broader connection.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>n8n</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Audit and Patch Your Self-Hosted n8n Instance Against the Sandbox-Escape Vulnerability</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/audit-and-patch-your-self-hosted-n8n-instance-against-the-sandbox-escape-vulnerability-4fc</link>
      <guid>https://dev.to/thesolutionscraft/audit-and-patch-your-self-hosted-n8n-instance-against-the-sandbox-escape-vulnerability-4fc</guid>
      <description>&lt;p&gt;n8n's Code node and expression editor run inside a sandbox specifically so that a workflow can't reach out and touch the host it's running on. In July 2026, n8n &lt;a href="https://github.com/n8n-io/n8n/security/advisories/GHSA-gv7g-jm28-cr3m" rel="noopener noreferrer"&gt;patched a high-severity bypass&lt;/a&gt; of that sandbox: crafted arrow-function expressions could escape it entirely and execute arbitrary operating-system commands with the privileges of the n8n process. Rated High with a CVSS 4.0 score of 8.7, no CVE number had been assigned as of the advisory's publication.&lt;/p&gt;

&lt;p&gt;This walkthrough is for &lt;strong&gt;self-hosted instances&lt;/strong&gt; you manage yourself — Docker or npm/pnpm installs. If you're on n8n Cloud, patching is handled for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who's affected
&lt;/h2&gt;

&lt;p&gt;Exploiting this requires an authenticated account with permission to create or modify workflows — it's not an unauthenticated, drive-by attack. But in many self-hosted setups that's a wide net: any teammate with workflow-editor access (not just admins) could trigger it, and a successful exploit runs commands as the n8n process itself, which can expose &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; and, through it, every credential n8n has stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Affected versions:&lt;/strong&gt; anything below &lt;code&gt;2.31.5&lt;/code&gt;, plus the &lt;code&gt;2.32.0&lt;/code&gt; release specifically. &lt;strong&gt;Patched versions:&lt;/strong&gt; &lt;code&gt;2.31.5&lt;/code&gt; and &lt;code&gt;2.32.1&lt;/code&gt; onward.&lt;/p&gt;

&lt;p&gt;Notice the gap: &lt;code&gt;2.32.0&lt;/code&gt; shipped &lt;em&gt;after&lt;/em&gt; &lt;code&gt;2.31.5&lt;/code&gt; but was itself vulnerable — patch level alone doesn't tell you the answer, you need the actual version string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check your installed version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;npm/pnpm install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n8n --version

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container-name&amp;gt; n8n &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Run it against the affected ranges
&lt;/h2&gt;

&lt;p&gt;Rather than eyeball the version number against two separate ranges, this script does the comparison for you and exits non-zero if you're exposed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Checks an n8n version string against GHSA-gv7g-jm28-cr3m's affected ranges:&lt;/span&gt;
&lt;span class="c"&gt;# vulnerable: &amp;lt;2.31.5 OR &amp;gt;=2.32.0,&amp;lt;2.32.1&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?Usage:&lt;span class="p"&gt; check-n8n-version.sh &amp;lt;n8n-version&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

ver_lt&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="nv"&gt;lower&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lower&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

ver_ge&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.31.5"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VULNERABLE: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is below 2.31.5"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;elif &lt;/span&gt;ver_ge &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.32.0"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ver_lt &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"2.32.1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VULNERABLE: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is in the 2.32.0 pre-patch range"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: &lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt; is patched against GHSA-gv7g-jm28-cr3m"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;check-n8n-version.sh&lt;/code&gt;, then pipe your actual version straight in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x check-n8n-version.sh
./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# or, for Docker:&lt;/span&gt;
./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;container-name&amp;gt; n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Patch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker / Docker Compose&lt;/strong&gt; — pin the image tag to a patched version and redeploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n:2.32.1&lt;/span&gt; &lt;span class="c1"&gt;# or 2.31.5 if you need to stay on the 2.31.x train&lt;/span&gt;


&lt;span class="s"&gt;docker compose pull&lt;/span&gt;
&lt;span class="s"&gt;docker compose up -d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;npm/pnpm global install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; n8n@2.32.1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Confirm the patch took
&lt;/h2&gt;

&lt;p&gt;Re-run the same check — it should now report &lt;code&gt;OK&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./check-n8n-version.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;n8n &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If you can't upgrade right away
&lt;/h2&gt;

&lt;p&gt;The advisory's own workaround, worth noting because it's only partial: restrict workflow creation and editing to accounts you fully trust, since exploitation requires that permission. It doesn't close the hole — anyone who still has editor access can still trigger it — so treat it as a stopgap until you patch, not a substitute for patching.&lt;/p&gt;

&lt;p&gt;If keeping up with patches like this one indefinitely isn't something you want to own yourself, &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud&lt;/a&gt; handles this one — and every future one — for you automatically.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If you're running n8n behind the &lt;a href="https://solutionscraft.com/automation/enable-n8n-mcp-server-plain-english-workflow" rel="noopener noreferrer"&gt;MCP Server Trigger setup&lt;/a&gt;, the same principle applies doubly: an MCP client that can create or edit workflows through that surface has the same permissions this vulnerability requires, so keep both patched and access-scoped together.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>security</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Enable n8n's Built-In MCP Server and Build a Workflow From a Plain-English Prompt</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 14 Aug 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/enable-n8ns-built-in-mcp-server-and-build-a-workflow-from-a-plain-english-prompt-bh4</link>
      <guid>https://dev.to/thesolutionscraft/enable-n8ns-built-in-mcp-server-and-build-a-workflow-from-a-plain-english-prompt-bh4</guid>
      <description>&lt;p&gt;n8n ships a node that flips the usual direction of automation: instead of n8n calling out to an AI model, an AI assistant calls into n8n. The &lt;strong&gt;MCP Server Trigger&lt;/strong&gt; node turns a workflow into a tool that any &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; client — Claude Desktop, Claude Code, or anything else that speaks MCP — can discover and run. Ask the assistant in plain English, and it invokes your workflow directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need a running n8n instance (Cloud, self-hosted Community Edition, or Enterprise all support the MCP Server Trigger node — a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; is the fastest way to get one if you don't have an instance yet) and an MCP client to test with — this walkthrough uses Claude Desktop. No community nodes or extra installs are required on the n8n side; the node ships in n8n's built-in LangChain node package.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Add the MCP Server Trigger node
&lt;/h2&gt;

&lt;p&gt;Create a new workflow and add the &lt;strong&gt;MCP Server Trigger&lt;/strong&gt; node as its starting point. Unlike a normal trigger, this node doesn't fire on a schedule or a webhook — it stays listening, and its only job is to expose whatever tool nodes you connect to it.&lt;/p&gt;

&lt;p&gt;Open the node's panel and note the &lt;strong&gt;Path&lt;/strong&gt; field: by default it's a randomly generated segment of the MCP URL, which is fine for a first test. You can set a fixed path later if you want a stable URL to hand out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Expose a workflow as a tool
&lt;/h2&gt;

&lt;p&gt;The trigger does nothing on its own — it needs at least one tool node connected to it. Add a &lt;strong&gt;Custom n8n Workflow Tool&lt;/strong&gt; node and point it at the workflow you want the assistant to run (this can be the same workflow, or a separate one you've already built, like the folder-watcher from an earlier post).&lt;/p&gt;

&lt;p&gt;Give the tool a clear &lt;strong&gt;Name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt;. The description is what the AI actually reads to decide when to call it — write it the way you'd explain the tool to a new teammate: what it does, and when to use it. A vague description gets skipped or misused; a specific one ("Archives a file at the given path into the archive folder") gets called at the right moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Secure it with Bearer auth
&lt;/h2&gt;

&lt;p&gt;An MCP endpoint with no authentication is an open door into your automations — anyone with the URL can call your tools. In the MCP Server Trigger node, set &lt;strong&gt;Authentication&lt;/strong&gt; to &lt;strong&gt;Bearer Auth&lt;/strong&gt; , then create a new credential and generate a token. Keep the token in the credential, not pasted into the workflow body.&lt;/p&gt;

&lt;p&gt;Save and activate the workflow. With the workflow active, reopen the trigger node panel — it now shows the live &lt;strong&gt;MCP URL&lt;/strong&gt; and the &lt;strong&gt;Bearer Token&lt;/strong&gt; you'll need for the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect an MCP client
&lt;/h2&gt;

&lt;p&gt;Claude Desktop (like most current MCP clients) expects a local process it can talk to over stdio, not a raw HTTPS URL, so you bridge the two with &lt;a href="https://www.npmjs.com/package/mcp-remote" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-remote&lt;/code&gt;&lt;/a&gt;, a small proxy built for exactly this. Add an entry to your &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"n8n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://your-n8n-instance.example.com/mcp/&amp;lt;your-path&amp;gt;/sse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Authorization: Bearer ${N8N_MCP_TOKEN}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the URL with the MCP URL from Step 3, and set &lt;code&gt;N8N_MCP_TOKEN&lt;/code&gt; in your environment to the Bearer token. Save the file and restart Claude Desktop — it reads this config on launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Trigger it with a plain-English prompt
&lt;/h2&gt;

&lt;p&gt;Open a new chat in Claude Desktop and ask for the thing your tool does, in ordinary language — no slash commands, no JSON. If your tool description was specific, Claude recognizes it needs your n8n tool, calls it with the right input, and reports back what happened. Check the n8n execution log for the run to confirm it actually fired and see exactly what input the assistant sent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; If Claude doesn't call the tool when you expect it to, the description is almost always the fix — not the prompt. Make it more concrete about what the tool does and what kind of request it applies to.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;One workflow behind one tool is the starting point. The same MCP Server Trigger accepts multiple &lt;strong&gt;Custom n8n Workflow Tool&lt;/strong&gt; nodes, so a single MCP connection can expose your whole library of automations to an assistant at once — letting you describe a multi-step task in plain English and have it call several of your workflows in sequence. For the reverse direction, an n8n AI Agent calling tools on someone else's server, see the &lt;a href="https://solutionscraft.com/automation/connect-external-mcp-server-n8n-ai-agent" rel="noopener noreferrer"&gt;MCP Client Tool&lt;/a&gt; walkthrough. And if wiring up one workflow at a time starts to feel limiting, &lt;a href="https://solutionscraft.com/automation/connect-claude-desktop-n8n-instance-mcp-oauth" rel="noopener noreferrer"&gt;n8n's instance-level MCP&lt;/a&gt; exposes the whole instance through a single OAuth connection instead.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>n8n</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Auto-Resize and Compress Images with a Python Watch-Folder Script</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/auto-resize-and-compress-images-with-a-python-watch-folder-script-3887</link>
      <guid>https://dev.to/thesolutionscraft/auto-resize-and-compress-images-with-a-python-watch-folder-script-3887</guid>
      <description>&lt;p&gt;Resizing images by hand before every upload gets old fast. This script watches a folder and does it the moment a file lands — drop a photo in, it comes out resized and compressed, ready to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need Python 3.9+ and two packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pillow watchdog

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pillow handles the actual image processing; watchdog is what lets the script react to new files instead of polling the folder on a timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write the handler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# resize_watch.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;watchdog.observers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Observer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;watchdog.events&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileSystemEventHandler&lt;/span&gt;

&lt;span class="n"&gt;MAX_WIDTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;
&lt;span class="n"&gt;QUALITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FileSystemEventHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_WIDTH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_WIDTH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;
            &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;MAX_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;optimize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUALITY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Observer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImageHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./incoming&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python resize_watch.py

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop a large JPEG into &lt;code&gt;./incoming&lt;/code&gt; and watch it shrink in place within a second or two.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; &lt;code&gt;on_created&lt;/code&gt; fires as soon as the file starts being written, which can catch a still-copying file on a slow network drive. If you're watching a folder fed by uploads over a slow connection, add a short delay or check the file size is stable before processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;For a one-off batch instead of a always-running watcher, drop the &lt;code&gt;watchdog&lt;/code&gt; part entirely and just loop over &lt;code&gt;os.listdir()&lt;/code&gt; — that version fits neatly into an n8n &lt;strong&gt;Execute Command&lt;/strong&gt; node, so you can trigger it from the same kind of workflow covered in the &lt;a href="https://solutionscraft.com/automation/stripe-customers-to-google-sheet-n8n" rel="noopener noreferrer"&gt;Stripe-to-Sheets tutorial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sync New Stripe Customers to a Google Sheet with n8n</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/sync-new-stripe-customers-to-a-google-sheet-with-n8n-14p6</link>
      <guid>https://dev.to/thesolutionscraft/sync-new-stripe-customers-to-a-google-sheet-with-n8n-14p6</guid>
      <description>&lt;p&gt;If your "CRM" right now is remembering who paid you, this fixes that in about ten minutes with zero code. Every time someone becomes a Stripe customer, their details land in a Google Sheet automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need an n8n instance (local or cloud — a free &lt;a href="https://n8n.partnerlinks.io/4rue2byfzbu6" rel="noopener noreferrer"&gt;n8n Cloud trial&lt;/a&gt; is the fastest way to get one), a Stripe account, and a Google account with a Sheet ready to receive rows. If this is your first time in the n8n editor, &lt;a href="https://solutionscraft.com/automation/first-n8n-workflow" rel="noopener noreferrer"&gt;the folder-watcher walkthrough&lt;/a&gt; covers the basics this one assumes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; the n8n Cloud link above is an affiliate link — if you sign up through it, we may earn a commission at no extra cost to you. See our &lt;a href="https://solutionscraft.com/disclosure" rel="noopener noreferrer"&gt;affiliate disclosure&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Add the Stripe trigger
&lt;/h2&gt;

&lt;p&gt;Create a new workflow and add a &lt;strong&gt;Stripe Trigger&lt;/strong&gt; node. Connect your Stripe account (an API key from the Stripe dashboard), and set the event to &lt;code&gt;customer.created&lt;/code&gt;. This fires the workflow the moment someone new signs up or makes a purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Map the fields you want
&lt;/h2&gt;

&lt;p&gt;Add a &lt;strong&gt;Google Sheets&lt;/strong&gt; node after the trigger, set the operation to &lt;strong&gt;Append Row&lt;/strong&gt; , and point it at your sheet. Map the columns using the data Stripe already sent through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: {{$json["name"]}}
Email: {{$json["email"]}}
Created: {{$json["created"]}}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;n8n resolves these expressions against the trigger's output automatically — no transformation step needed for a simple case like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Test it
&lt;/h2&gt;

&lt;p&gt;Use Stripe's test mode to create a customer, then check the workflow's execution log in n8n. You'll see exactly what Stripe sent and what got written to the sheet — useful for catching a field name mismatch before it happens with a real customer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Turn on the workflow's &lt;strong&gt;Active&lt;/strong&gt; toggle before trusting it in production. A workflow that only runs when you manually execute it in the editor won't catch real Stripe events.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The same trigger works for a Slack notification — add a &lt;strong&gt;Slack&lt;/strong&gt; node in parallel with the Google Sheets node and you'll get pinged the moment someone new signs up, no extra Stripe configuration required. Worth pairing with a regular &lt;a href="https://solutionscraft.com/automation/audit-n8n-leaked-api-tokens" rel="noopener noreferrer"&gt;audit of your n8n instance for leaked API tokens&lt;/a&gt; too, since the Stripe key powering this trigger is exactly the kind of credential that matters if it ever ends up somewhere it shouldn't.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automate Windows Server Health Checks with PowerShell and Email Alerts</title>
      <dc:creator>SolutionsCraft</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/thesolutionscraft/automate-windows-server-health-checks-with-powershell-and-email-alerts-4gci</link>
      <guid>https://dev.to/thesolutionscraft/automate-windows-server-health-checks-with-powershell-and-email-alerts-4gci</guid>
      <description>&lt;p&gt;Checking server health by logging in and eyeballing Task Manager doesn't scale past one or two machines. This script checks disk space, memory, and a list of critical services, and only sends an email when something's actually wrong — not a daily "everything's fine" notification you'll learn to ignore within a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need PowerShell 5.1+ (built into Windows Server) and SMTP credentials for sending mail — an app password if you're using Microsoft 365 or Gmail, or your internal relay if you have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write the check
&lt;/h2&gt;

&lt;p&gt;Each check appends to an &lt;code&gt;$issues&lt;/code&gt; array only when something crosses a threshold. Nothing gets added when things are fine, which is what keeps the email quiet on a normal day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# health-check.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$diskThreshold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# percent free&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$memThreshold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# percent free&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$services&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"W3SVC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MSSQLSERVER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spooler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@()&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Get-PSDrive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PSProvider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$freePct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Free&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Free&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Used&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$freePct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$diskThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Drive &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$freePct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)% free"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_OperatingSystem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$memPct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FreePhysicalMemory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalVisibleMemorySize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$memPct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$memThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Memory: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$memPct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)% free"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Running"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Service &lt;/span&gt;&lt;span class="nv"&gt;$svc&lt;/span&gt;&lt;span class="s2"&gt; is not running"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Only email when there's something to say
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Send-MailMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-To&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@solutionscraft.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-From&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-alerts@yourdomain.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server health check: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; issue(s)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$issues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nt"&gt;-SmtpServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp.yourprovider.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UseSsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nt"&gt;-Credential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-StoredCredential&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp-alerts"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Don't hardcode the SMTP password in the script. &lt;code&gt;Get-StoredCredential&lt;/code&gt; (from the &lt;code&gt;CredentialManager&lt;/code&gt; module) pulls it from Windows Credential Manager instead, so the script stays safe to commit or share.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Run it on a schedule
&lt;/h2&gt;

&lt;p&gt;Register it as a scheduled task so it runs without you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Argument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-File C:\Scripts\health-check.ps1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskTrigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Daily&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-At&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;8am&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Register-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TaskName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ServerHealthCheck"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Trigger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$trigger&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If you're monitoring more than a couple of servers, this is also a good candidate for an n8n &lt;strong&gt;Execute Command&lt;/strong&gt; node instead of Task Scheduler — you get a run history and retry logic for free, the same tradeoff covered in the &lt;a href="https://solutionscraft.com/automation/first-n8n-workflow" rel="noopener noreferrer"&gt;n8n workflow tutorial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>automation</category>
      <category>sysadmin</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
