<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xml:base="https://bjorn.now/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Devlogs on bjorn.now</title><link>https://bjorn.now/devlog/</link><description>Recent content in Devlogs on bjorn.now</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 22 Mar 2026 23:31:31 +0100</lastBuildDate><atom:link href="https://bjorn.now/devlog/feed.xml" rel="self" type="application/rss+xml"/><item><title>It's beginning to crumble — The first steps of my breadcrumb tool</title><link>https://bjorn.now/devlog/2026-03-22-its-beginning-to-crumble/?utm_medium=feed</link><pubDate>Sun, 22 Mar 2026 23:31:31 +0100</pubDate><guid>https://bjorn.now/devlog/2026-03-22-its-beginning-to-crumble/</guid><description>I had a breakthrough while working on bjornstack that made me realize how I could make an MVP of breadcrumb, the tool I’ve wanted to build for ~18 months.
So anyway, bjornstack is my opinionated take on how to write code, aimed at constraining and guiding genies to get their work done. And while working on it, like I have been whenever I’m working with genies, I got thoughts that I wanted to explore later, or ideas for other things I’m working on. I realized that since I started using beads capturing in this project has gotten easier, while chatting I just mention we need “a new bead about x because y provide all the context we have here” and then there’d be a new bead linked to the work we were doing, and I can just pick it up later.
But as soon as it was something for another project, or even just “I need to remember to pack the charger when I leave the office today,” then I had to break out to capture the thought, and blow my stack and lose my flow. And if I don’t capture it, I lose the thought.</description><content:encoded><![CDATA[<p>I had a breakthrough while working on <a href="https://github.com/gaqzi/bjornstack">bjornstack</a> that made me realize how I could make an MVP of breadcrumb, the tool I&rsquo;ve wanted to build for ~18 months.</p>
<p>So anyway, bjornstack is my opinionated take on how to write code, aimed at constraining and guiding genies to get their work done. And while working on it, like I have been whenever I&rsquo;m working with genies, I got thoughts that I wanted to explore later, or ideas for other things I&rsquo;m working on. I realized that since I started using beads capturing <em>in this project</em> has gotten easier, while chatting I just mention we need &ldquo;a new bead about x because y provide all the context we have here&rdquo; and then there&rsquo;d be a new bead linked to the work we were doing, and I can just pick it up later.</p>
<p>But as soon as it was something for another project, or even just &ldquo;I need to remember to pack the charger when I leave the office today,&rdquo; then I had to break out to capture the thought, and blow my stack and lose my flow.
And if I don&rsquo;t capture it, I lose the thought.</p>
<p>Somewhere on Saturday I realized this is how <a href="https://github.com/steveyegge/gastown">Gas Town</a> seems to be using <a href="https://github.com/steveyegge/beads">beads</a>: You sling beads across projects, they get filed, and they can be queried across everything you&rsquo;re working on. There are ephemeral beads and long-lived ones. It&rsquo;s beads all the way down.</p>
<p>So maybe the breadcrumb app I&rsquo;ve been thinking about, crumb for short,
doesn&rsquo;t need to be a custom database with all the trimmings.
Maybe, as a first version, it&rsquo;s just a thin wrapper around beads.
A skill I can give to an agent that says: this is how you record something into my breadcrumbs.</p>
<h2 id="what-ive-been-missing">
  What I&rsquo;ve been missing
  <a href="#what-ive-been-missing" class="heading-anchor" aria-label="Direct link to What I&rsquo;ve been missing"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;ve been thinking about building breadcrumb for a while now, and I was stuck on how to get started. The idea is simple, capture stuff when you realize it, but it&rsquo;s hard to do without breaking flow. I was thinking of doing it when committing because it&rsquo;s a point of &ldquo;I&rsquo;m done&rdquo; but I&rsquo;m not really committing as much anymore on my own now.</p>
<p>Back before I started doing a lot of my work in chat windows with <em>genies</em> I would do the majority of my thinking in Roam Research. It was structured, it had bidirectional links, and I could find most things super easily. I had workflows to help me think. Glorious. ✨</p>
<p>But when I started building with agents, it would break me out of what I was doing to go leave breadcrumbs in Roam so I could track what I had been up to. So I stopped. And honestly, it was something I noticed when I worked on meaty problems when programming too, I would get caught up in what I did and not take notes. To my detriment later.</p>
<p>So, I wanted to get back to having my interstitial journal workflow I loved in Roam, to be able to look back on a day and get a feel for how it went based on the notes I left myself. And also have a better trail of things I learned as I worked.</p>
<p>And I also wanted to restart the strong weekly review routine I had, which was where I realized a lot of the stuff I did that turned out to be good ideas at work. I have also been reading Cal Newport and I wanted to start doing his daily shutdown routine, and the daily planning with a startup routine. So let&rsquo;s see if I can bake those into this too.</p>
<p>The thing I want crumb to be is kind of the flywheel that helps me grow. Daily capture feeds into daily reflection. Daily reflection feeds into weekly review. Weekly review is where I compound what I learned. The whole point is being able to retrace your steps. I did something, I learned something, and months later when I need it I can actually find it.</p>
<p>And over time <strong>the system starts producing things I actually care about</strong>: a brag document that writes itself from the work I did (even as my own boss, it feels good to see how much I got done), TILs worth sharing, a personal solution database I can query when I hit a similar problem months later. I don&rsquo;t have to buy into &ldquo;journaling is good for you&rdquo; to get value, I just need to care about any one of those outputs.</p>
<h2 id="what-i-built">
  What I built
  <a href="#what-i-built" class="heading-anchor" aria-label="Direct link to What I built"><span aria-hidden="true">#</span></a>
</h2>
<p>I knocked out a first version today.
It&rsquo;s a global Claude Code skill that I install on my machine.
In any session, in the middle of whatever I&rsquo;m working on,
I just write <code>(crumb: &lt;whatever thought I had&gt;)</code> and it gets sent over to my cottage project. It can be anywhere in the prompt because I told it to look for either <code>/crumb …</code> or <code>(crumb:…)</code> and it just worked.</p>
<p>One thing I learned getting this to work:
the skill needs to <code>cd</code> into the cottage project directory and run beads from there when creating.
Otherwise beads doesn&rsquo;t know which project context to file things into.</p>
<p>So I made a small wrapper around <code>bd</code> (the beads CLI) that I could <a href="/til/2026-01-10-claude-code-sandbox-exclusion-requires-both-permissions-allow-and-sandbox-excludedcommands/">allowlist globally</a> in my Claude Code settings:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># Wrapper that runs bd against the cottage project database.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Works from any working directory.</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/workspace/cottage&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">exec</span> bd <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>The key ideas:</p>
<ul>
<li>It finds a daily journal bead in the cottage project (or falls back to creating a plain bead)</li>
<li>It captures provenance: which project you&rsquo;re in, which bead you&rsquo;re working on, so you can backtrack to it later if you want more context</li>
<li>It files the thought as a comment on the daily bead, then moves on, and the nice part of the comment is that it gives me <em>when</em>, a timestamp, and it doesn&rsquo;t matter if I&rsquo;m working on several repos at the same time because it&rsquo;ll just be combined anyway in the daily bead.</li>
</ul>
<details>
<summary>The full crumb skill</summary>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">name: crumb
</span></span><span class="line"><span class="cl">description: &#34;Quick thought capture — adds a comment to the user&#39;s daily journal bead in their cottage project. Trigger when the user says /crumb, &#39;crumb:&#39;, or uses the inline form &#39;(crumb:…)&#39; as a parenthetical aside. File the crumb FIRST before handling anything else in the prompt. The user is in flow; receive, file, move on.&#34;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Crumb — Quick Thought Capture
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## How to run commands
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This skill includes a wrapper script `crumb-bd` that runs `bd` against the cottage database regardless of your current working directory. Use it for all cottage operations. Resolve the path relative to this skill&#39;s base directory:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd &lt;bd args...&gt;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Every invocation needs `dangerouslyDisableSandbox: true` (Dolt needs raw TCP to localhost:3308).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**If any command fails, you MUST show the error to the user and echo back the crumb text so nothing is lost.** Don&#39;t silently swallow failures.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Priority
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">File the crumb BEFORE responding to anything else in the user&#39;s message. The crumb is a side-channel capture — handle it first, confirm in one line, then continue with the main conversation.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## What this does
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Adds the user&#39;s thought as a comment on their daily journal bead (a bead in the cottage project with label `daily`). The thought gets processed later during review — your job is just to file it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Parse the input
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Format is `category: text` where the category is optional.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">/crumb idea: what if protocols were composable
</span></span><span class="line"><span class="cl">/crumb friction: dolt keeps dying on me
</span></span><span class="line"><span class="cl">/crumb just a raw thought with no category
</span></span><span class="line"><span class="cl">(crumb:reminder: pack the charger before leaving)
</span></span><span class="line"><span class="cl">(crumb:idea: what if we split beads by domain)
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Categories are freeform — `idea`, `friction`, `reminder`, `meeting`, `learning`, `question`, `todo`, whatever the user wrote. Don&#39;t rename or recategorize. No category is fine. `(crumb:idea: blah)` is valid — the category is `idea`.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Find the daily bead
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd query &#34;label=daily AND status=in_progress&#34; --json
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Found:** Use it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Not found — fallback chain:**
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Most recent closed daily that hasn&#39;t been reviewed yet:
</span></span><span class="line"><span class="cl">   ```bash
</span></span><span class="line"><span class="cl">   scripts/crumb-bd sql &#34;SELECT i.id, i.title FROM issues i JOIN labels l ON i.id = l.issue_id WHERE l.label = &#39;daily&#39; AND i.status = &#39;closed&#39; AND i.id NOT IN (SELECT l2.issue_id FROM labels l2 WHERE l2.label = &#39;reviewed:daily&#39;) ORDER BY i.created_at DESC LIMIT 1&#34;
</span></span><span class="line"><span class="cl">   ```
</span></span><span class="line"><span class="cl">   Comments on closed beads work fine.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. No daily bead at all — create a standalone bead:
</span></span><span class="line"><span class="cl">   ```bash
</span></span><span class="line"><span class="cl">   scripts/crumb-bd create &#34;&lt;the thought&gt;&#34; --type task --priority 3
</span></span><span class="line"><span class="cl">   ```
</span></span><span class="line"><span class="cl">   Tell the user: &#34;No daily bead open — filed as standalone bead.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Build the comment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Provenance
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Capture where the thought came from — project name, and what the user was working on.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Project name:**
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">basename &#34;$(git rev-parse --show-toplevel 2&gt;/dev/null || pwd)&#34;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Active bead:** Use this escalation — stop at the first that works:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. **Session context:** If the conversation has claimed or discussed a bead, use its ID and title directly.
</span></span><span class="line"><span class="cl">2. **Query local db:** `bd list --status=in_progress --json 2&gt;/dev/null` — use ID and title if exactly one result. Multiple results → pick the most recently discussed, or ask briefly. Zero → no active bead.
</span></span><span class="line"><span class="cl">3. **Folder fallback:** If no bead is found and no beads db exists, record the working directory path. This lets the user query session logs for that folder and time later.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Format the comment** with a provenance header on its own line, then the content:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from bjornstack, bs-xyz — Refactor auth middleware)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">When working in cottage, still include provenance if you&#39;re working on a specific bead — the point is capturing something different from current focus:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(working on in-mlk — Create the /crumb skill)
</span></span><span class="line"><span class="cl">idea: what if we made categories queryable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If no bead is found and no beads db exists, use the working directory:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from ~/workspace/some-project)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Multiple thoughts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If the user drops several thoughts in one message, file them as a single comment separated by newlines. One provenance header, multiple entries:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from bjornstack, bs-xyz — Refactor auth middleware)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">friction: dolt server died again
</span></span><span class="line"><span class="cl">reminder: pack the charger
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Add the comment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd comments add &lt;daily-id&gt; &#34;&lt;the comment text&gt;&#34;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**If this fails:** Show the error AND echo the full crumb text back to the user so they can capture it another way. Example:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Failed to file crumb (connection refused). Your crumb text:
</span></span><span class="line"><span class="cl">&gt; idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Confirm and move on
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">One line. The user is in flow.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Crumb filed → idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Multiple:
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Filed 3 crumbs on &lt;daily-id&gt;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Don&#39;t engage with the content. If the thought is so terse that future-them won&#39;t understand it, ask for one line of context in the same breath — &#34;Filed. Quick — what&#39;s the context? Won&#39;t make sense later without it.&#34; — then move on regardless.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## When to create a bead instead
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Only if the user explicitly asks: &#34;make this a bead&#34;, &#34;track this&#34;, &#34;this needs its own issue&#34;, or &#34;bead: ...&#34;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd create &#34;&lt;thought&gt;&#34; \
</span></span><span class="line"><span class="cl">  --description &#34;Captured from &lt;project&gt;, working on &lt;bead-id&gt; — &lt;bead-title&gt;&#34; \
</span></span><span class="line"><span class="cl">  --type task --priority 3
</span></span><span class="line"><span class="cl">```</span></span></code></pre></div>
</div>
</details>
<p>The provenance bit is what makes this useful later.
When I&rsquo;m doing my shutdown review, each crumb tells me where I was and what I was doing when the thought hit.
I don&rsquo;t have to reconstruct context, it&rsquo;s already there.</p>
<p>And it felt really nifty when I ended the day and the shutdown routine showed me: these are the beads you closed today, and here&rsquo;s the note you left yourself when you finished that session. A mini-review of my own work that I barely had to think about capturing. Because the majority of my work is already flowing through beads, it all just becomes queryable. It doesn&rsquo;t matter if I was working across several repos at the same time, the daily bead combines it all.</p>
<h2 id="why-beads-and-not-something-custom">
  Why beads and not something custom
  <a href="#why-beads-and-not-something-custom" class="heading-anchor" aria-label="Direct link to Why beads and not something custom"><span aria-hidden="true">#</span></a>
</h2>
<p>When I was thinking about building this tooling,
I assumed I&rsquo;d have to build something tool-agnostic (Roam/Obsidian/Apple Notes/etc),
and because this stuff is so sensitive, local-first with no centralized server.
By adopting beads and dolt, I get all of that for free.
Dolt pushes all the data into git, and there&rsquo;s an existing nice CLI interface, and more people are building on top of beads already.</p>
<p>I&rsquo;m admittedly kind of abusing beads here.
It&rsquo;s not really designed for this kind of journaling.
But most of what I&rsquo;m capturing are tasks I did,
and having a daily bead where I add notes as the day progresses,
that&rsquo;s kind of how research tasks work anyway.
You add comments, you talk to people, you figure it out,
and then it becomes a written conclusion.
So yeah, maybe bending it a little, but it&rsquo;s solving a whole lot more than it&rsquo;s bending.</p>
<p>A lot of the things I was originally planning to build already kind of exist,
just in a packaging that isn&rsquo;t necessarily optimal for what I want right now.
But it&rsquo;s a hell of a good start that I can adjust and adapt as I learn more about how I want this to work. And then get started on the part of &ldquo;how do I extract more knowledge from all the information I&rsquo;m creating?&rdquo;</p>
<h2 id="whats-next">
  What&rsquo;s next
  <a href="#whats-next" class="heading-anchor" aria-label="Direct link to What&rsquo;s next"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;m going to try this out for a bit and see how the daily and weekly routines feel.
If you&rsquo;re reading this and interested in trying it out before I share it publicly,
<a href="mailto:ba@bjorn.now?subject=Got%20some%20crumbs%20for%20me%3F">send me an email</a> and I&rsquo;ll give you a preview.</p>]]></content:encoded></item><item><title>Maintaining n8n inline Javascript as code …and more importantly: with unit tests</title><link>https://bjorn.now/devlog/2025-11-03-maintaining-n8n-inline-javascript-as-code/?utm_medium=feed</link><pubDate>Mon, 03 Nov 2025 15:04:43 +0100</pubDate><guid>https://bjorn.now/devlog/2025-11-03-maintaining-n8n-inline-javascript-as-code/</guid><description>In this devlog I’ll make a change in my n8n POSSE setup, that could’ve taken ~an hour if I kept doing it manually as I had, but instead I spent ~5 hours tidying and removing manual steps for the future.
The work itself was about making my “scraps,” that is this site’s tweets, be sent out raw to my syndication targets. I feel that sending a link to something you see in the preview, or it could be inline in the post, is a bit much, I should just let it show up in the platform itself.</description><content:encoded><![CDATA[<p>In this devlog I&rsquo;ll make a change in my n8n POSSE setup,
that could&rsquo;ve taken ~an hour if I kept doing it manually as I had,
but instead I spent ~5 hours tidying and removing manual steps for the future.</p>
<p>The work itself was about making my &ldquo;scraps,&rdquo; that is this site&rsquo;s tweets,
be sent out raw to my syndication targets.
I feel that sending a link to something you see in the preview,
or it could be inline in the post, is a bit much,
I should just let it show up in the platform itself.</p>
<p>I have a couple of things planned for this site,
and this was chosen as the first step
because it would force me to think about <em>how will I maintain</em> my n8n workflows.</p>
<p>So now, instead of writing some Javascript code, thinking really hard,
and then test it by tooting something out, I instead have some unit tests and
a way to programmatically update my workflow. So as I go down the rest of my
list of tasks I want to do working with n8n will be much smoother.</p>
<p>I had a couple of decisions to make,
so I listed them out using the <a href="https://www.cortexfutura.com/getting-started-algorithms-of-thought/#simple-choice-algorithm">Choice algorithm of thought</a>,
and as always it forced me to dig deeper into the problem,
even if I ended up doing what I guessed going in.
But, the benefit of this process is being intentional and explicit, it
gets me into the mode of &ldquo;I&rsquo;m trying to list out options,&rdquo; and to explain
what made me make the decision. Since this is now recorded in my notes
I can easily go back later to find why.</p>
<p>The kind of highlights of what I got done:</p>
<ul>
<li><a href="#updating-n8n-programmatically">Updating n8n programmatically</a>:
Extract the code from a Javascript source file (glorified <code>grep</code>),
and then wrap it so it&rsquo;ll work in n8n.</li>
<li><a href="#implementing-reusable-tests">Creating reusable tests in Javascript</a>:
which turned out to be a bad idea, because the code was the same but
diverged too much, so keeping the tests the same would&rsquo;ve needed too much
conditional logic.<br>
So I ended up copy/pasting instead of creating a maintenance nightmare.
This pattern is useful to know, though, I use it in other codebases where
the code is duplicated but some shared logic needs to stay the same.
For example, adapters for databases implementing a common interface.</li>
<li><a href="#update-telegram-logic">Telegram Instant Views</a>: Learning about and configuring.
Create an Instant View template for this site and learn a bit more about XPath.</li>
<li><a href="#understand-how-n8n-updates-work">n8n&rsquo;s update API</a>:
Dig into what logic there is calling the update workflow REST endpoint,
so I understand what I might break with how I decided to perform updates.
Created a guard-rail to try and catch if I&rsquo;m about to overwrite click-ops
changes I hadn&rsquo;t accounted for when changing locally.</li>
</ul>
<details>
    <summary>Table of contents</summary>

<nav id="TableOfContents">
  <ul>
    <li><a href="#background-and-scope-of-work">Background and scope of work</a></li>
    <li><a href="#coding-the-mastodon-logic">Coding the Mastodon logic</a>
      <ul>
        <li><a href="#designing-the-solution">Designing the solution</a></li>
        <li><a href="#backfilling-tests-for-the-existing-code">Backfilling tests for the existing code</a></li>
        <li><a href="#implementing-the-new-logic">Implementing the new logic</a></li>
      </ul>
    </li>
    <li><a href="#updating-n8n-programmatically">Updating n8n programmatically</a>
      <ul>
        <li><a href="#designing-the-extraction">Designing the extraction</a></li>
        <li><a href="#building-the-extractor">Building the extractor</a></li>
        <li><a href="#building-the-workflow-updater-script">Building the workflow updater script</a></li>
        <li><a href="#importing-the-updated-workflow">Importing the updated workflow</a></li>
        <li><a href="#validating-the-updated-workflow">Validating the updated workflow</a></li>
      </ul>
    </li>
    <li><a href="#repeat-for-bluesky">Repeat for Bluesky</a>
      <ul>
        <li><a href="#designing-the-change">Designing the change</a></li>
        <li><a href="#implementing-reusable-tests">Implementing reusable tests</a></li>
        <li><a href="#refactoring-after-working-tests">Refactoring after working tests</a></li>
        <li><a href="#testing-bsky-live">Testing bsky live</a></li>
      </ul>
    </li>
    <li><a href="#wednesday-eod">Wednesday EOD</a></li>
    <li><a href="#update-telegram-logic">Update Telegram logic</a>
      <ul>
        <li><a href="#configuring-instant-views">Configuring Instant Views</a></li>
        <li><a href="#using-instant-view-links-or-not">Using Instant View links or not?</a></li>
        <li><a href="#final-telegram-logic-and-decision">Final Telegram logic and decision</a></li>
      </ul>
    </li>
    <li><a href="#understand-how-n8n-updates-work">Understand how n8n updates work</a>
      <ul>
        <li><a href="#digging-into-the-source-code">Digging into the source code</a></li>
        <li><a href="#adding-only-update-if-version-is-what-i-expect-on-the-remote">Adding &ldquo;only update if version is what I expect on the remote&rdquo;</a></li>
      </ul>
    </li>
    <li><a href="#final-thoughts">Final thoughts</a></li>
  </ul>
</nav>
</details>

<h2 id="background-and-scope-of-work">
  Background and scope of work
  <a href="#background-and-scope-of-work" class="heading-anchor" aria-label="Direct link to Background and scope of work"><span aria-hidden="true">#</span></a>
</h2>
<p>I will (for now) not add images to scraps until I have native support for images in my posting (so they either only exist in a special content type, which gets linked, or they&rsquo;re attached when posting as a scrap).</p>
<p>I do this because I feel linking to these short-text things that really could just live on Masto/Bsky/etc. is a bit much, be a bit more native to them. Do use them to publish my other content but don&rsquo;t only send links when they&rsquo;re not necessary. It feels like being a bad citizen to them when it&rsquo;s only a sink that way.</p>
<p>The way I&rsquo;ve setup the sending in n8n right now, is that I have two steps in the status: 1) truncating the body and deciding if we need more, 2) and then adding the link, so I need to figure out a better maintenance pattern.</p>
<ul>
<li><strong>Choice:</strong> Manage n8n workflow with source management
<ul>
<li><strong>Constraints:</strong>
<ul>
<li>Be able to recreate my n8n setup if something goes belly-up</li>
<li>Keep as much as possible in source (but don&rsquo;t overdo it, I&rsquo;m a single person doing this and can manage myself)</li>
</ul>
</li>
<li><strong>Options:</strong>
<ul>
<li>Keep n8n fully clickops and keep code snippets in blog
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>Super simple and what I&rsquo;m doing, the status quo choice</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>If I have to recreate the n8n setup from scratch, painful</li>
</ul>
</li>
</ul>
</li>
<li>Create the workflow file in my blog and upload it for changes
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>All my n8n stuff is in my blog, so it&rsquo;s easy for me to deal with, and it&rsquo;s something I can easily share later so that&rsquo;s a win for sharing</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>More work to figure it out right now
<ul>
<li>But probably worth it, I&rsquo;ll download the workflow file and look at it to see what it says</li>
<li>looking at the JSON file it looks pretty straightforward, and using JQ I should be able to pretty easily inject what I need into it, so I can create a small file/script to help me out here that&rsquo;ll be worth it I think</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Decision:</strong> Create the workflow file in my blog and upload it for changes
<ul>
<li>It feels like a good step towards a maintainable setup and minimal effort to get it going</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Doing the choice here wasn&rsquo;t necessarily super useful, I kinda knew that I wanted to start storing the workflow in my repo, but the point was to step back and consider what I was doing. I was in my head starting to go down the path of just doing what I have been doing, it has been fine. But when I decided to look at the options &ldquo;the engineer in me&rdquo; reeled back enough that I felt that this time investment <em>will</em> be worth it.</p>
<p>Okay, the following jq will let me update a specific field in my workflow file, so I will have to make it do it &ldquo;in context&rdquo;, so that&rsquo;s a good start for what I&rsquo;m doing.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">jq &#39;.nodes[] | select(.id == &#34;e1bd320b-1222-4324-9168-22372d2e667c&#34;) | .parameters.status |= &#34;m000&#34;&#39; src/n8n/bjorn.now-feed-distribution.json</span></span></code></pre></div>
</div>
<p>So I think I want my maintenance to be: full status to be handled in one javascript block, so I can extract that and use jq to update the json file.</p>
<p>I haven&rsquo;t decided how I will ensure the javascript and json are up to date, maybe I&rsquo;ll just do a makefile because it&rsquo;s easy for now? Or maybe I do a template that doesn&rsquo;t store the &ldquo;production data&rdquo; so that I never have the actual prod version checked in and only create it on the fly as I&rsquo;m updating to encourage avoiding the disconnect.
I&rsquo;ll decide later.</p>
<h2 id="coding-the-mastodon-logic">
  Coding the Mastodon logic
  <a href="#coding-the-mastodon-logic" class="heading-anchor" aria-label="Direct link to Coding the Mastodon logic"><span aria-hidden="true">#</span></a>
</h2>
<p>With a short lunch break I&rsquo;m now back at it, so let&rsquo;s start by creating the logic I want so that I can let JQ pick it up.</p>
<h3 id="designing-the-solution">
  Designing the solution
  <a href="#designing-the-solution" class="heading-anchor" aria-label="Direct link to Designing the solution"><span aria-hidden="true">#</span></a>
</h3>
<p>Worry: I&rsquo;m not sure exactly <em>how</em> I will inject this into JQ right now, will I do a full JS file? Extract it smarter and use JQ as a library since I anyway has setup Javascript in the blog…? I don&rsquo;t know, I&rsquo;ll look into that AFTER I have finished updating the code I want and verified it manually, then I&rsquo;ll do this.</p>
<p>Okay, so what do I have available in the current setup?</p>
<p>I get injected <code>$('RSS Feed Trigger').item</code> into my inline function, which contains all the information from the RSS feed, so that&rsquo;s perfect, that means I don&rsquo;t need to send in anything more, I just need to decide what to output.</p>
<p>To make my testing life a bit easier I will now go and take my test data and make that available in a test, so I can TDD this thing.</p>
<p>Looking at <a href="https://github.com/gaqzi/bjorn.now/blob/df4abc0a299906a8c357a6e4619ce01ab5d4c530/src/js/n8n.copies.js#L7-L31">my code that I&rsquo;ve used in mastodon</a> (I just remove the <code>let mastodon = </code> when using) I think it&rsquo;ll work well as-is, so let&rsquo;s start by backfilling some missing tests, so I have a clean place to work from, the only problem is that I&rsquo;m calling it as I should in mastodon because I was only using it manually, so that&rsquo;ll have to go.
For now, I&rsquo;ll remove the self-calling and figure out how to deal with this when I inject with jq, since it feels like as long as I can get the function definition I can very easily wrap it in a self-call with the param I need.</p>
<h3 id="backfilling-tests-for-the-existing-code">
  Backfilling tests for the existing code
  <a href="#backfilling-tests-for-the-existing-code" class="heading-anchor" aria-label="Direct link to Backfilling tests for the existing code"><span aria-hidden="true">#</span></a>
</h3>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">mastodon</span> <span class="o">=</span> <span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">msg</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">contentSnippet</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">msgLength</span> <span class="o">=</span> <span class="mi">500</span><span class="o">-</span><span class="mi">23</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="c1">// the default mastodon post size is 500, -23 chars for the link, and -1 char for newline
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">isTruncated</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">[</span><span class="s1">&#39;content:encodedSnippet&#39;</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">contentSnippet</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">truncateIndicator</span> <span class="o">=</span> <span class="s1">&#39;\n… more&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">wordTruncate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">str</span><span class="p">,</span> <span class="nx">n</span><span class="p">,</span> <span class="nx">indicator</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">indicator</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="nx">indicator</span> <span class="o">=</span> <span class="s1">&#39;…&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="nx">str</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">n</span><span class="p">)</span> <span class="k">return</span> <span class="nx">str</span><span class="p">.</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nx">n</span> <span class="o">-=</span> <span class="nx">indicator</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="nx">str</span><span class="p">[</span><span class="nx">n</span><span class="p">]</span> <span class="o">===</span> <span class="s1">&#39; &#39;</span><span class="p">)</span> <span class="k">return</span> <span class="nx">str</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nx">str</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">i</span> <span class="o">=</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">lastIndexOf</span><span class="p">(</span><span class="s1">&#39; &#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// when there&#39;s no spaces at all, just cut in the middle of the text.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">===</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">i</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">msg</span> <span class="o">=</span> <span class="nx">wordTruncate</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">msgLength</span><span class="p">,</span> <span class="nx">truncateIndicator</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">isTruncated</span> <span class="o">?</span> <span class="nx">msg</span> <span class="o">+</span> <span class="nx">truncateIndicator</span> <span class="o">:</span> <span class="nx">msg</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">})(</span><span class="nx">$</span><span class="p">(</span><span class="s1">&#39;RSS Feed Trigger&#39;</span><span class="p">).</span><span class="nx">item</span><span class="p">)</span></span></span></code></pre></div>
</div>
<p>Cool, I have a first test showing the standard behavior of not including any links, so let&rsquo;s continue backfilling everything and then I&rsquo;ll add in the rest.</p>
<p>That was only two tests, nothing much to write home about but this covers the behavior I have and it&rsquo;s enough to characterize it for me, so these are the changes I want to do now:</p>
<p>Output the link as well, on a newline of it&rsquo;s own.</p>
<h3 id="implementing-the-new-logic">
  Implementing the new logic
  <a href="#implementing-the-new-logic" class="heading-anchor" aria-label="Direct link to Implementing the new logic"><span aria-hidden="true">#</span></a>
</h3>
<p>So I&rsquo;ll do this first, and make sure it&rsquo;s done correctly for both tests, I will start by making both tests fail, because that feels like the best way.</p>
<p>If the URL contains <code>/scrap/</code> then only output link if truncated.</p>
<p>Done, the code is there, I have some tests, it&rsquo;s nothing too spectacular, but it does what I want.
I realize that I should install standard and run the formatter regularly because I&rsquo;m being inconsistent and I don&rsquo;t like it. <code>gofmt</code> and <code>black</code> has ruined me 😁.</p>
<h2 id="updating-n8n-programmatically">
  Updating n8n programmatically
  <a href="#updating-n8n-programmatically" class="heading-anchor" aria-label="Direct link to Updating n8n programmatically"><span aria-hidden="true">#</span></a>
</h2>
<p>Now to figure out how to merge this into my workflow file.</p>
<ul>
<li><strong>Choice:</strong> How to merge my javascript into my workflow file
<ul>
<li><strong>Constraints:</strong>
<ul>
<li>No manual steps</li>
<li>Need to return the function to run, and then transform it into a &ldquo;self-run&rdquo; (<code>((item) =&gt; { item.json.link })($('RSS Feed Trigger').item)</code>) so it&rsquo;ll work</li>
</ul>
</li>
<li><strong>Options:</strong>
<ul>
<li>Is there some easy AST stuff I can use with node to extract it?
<ul>
<li>Let&rsquo;s ask claude
<ul>
<li>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">is there a function in node to parse the AST of a javascript file and to get the content of a variable as a string?</span></span></code></pre></div>
</div>
</li>
</ul>
</li>
<li><strong>Advantages:</strong>
<ul>
<li>Exists, will be robust
<ul>
<li>I got back a code example but I&rsquo;m hesitant to pull in these dependencies
<ul>
<li>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">parser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/parser&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">traverse</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/traverse&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">generate</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/generator&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;fs&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="s1">&#39;functions/processData.js&#39;</span><span class="p">,</span> <span class="s1">&#39;utf8&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ast</span> <span class="o">=</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="p">{</span> <span class="nx">sourceType</span><span class="o">:</span> <span class="s1">&#39;module&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">functionCode</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">FunctionDeclaration</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="s1">&#39;processData&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">functionCode</span> <span class="o">=</span> <span class="nx">generate</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">).</span><span class="nx">code</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">VariableDeclarator</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="s1">&#39;processData&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">functionCode</span> <span class="o">=</span> <span class="nx">generate</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">init</span><span class="p">).</span><span class="nx">code</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">functionCode</span><span class="p">);</span></span></span></code></pre></div>
</div>
</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>Huge set of dependencies for a small piece of work</li>
</ul>
</li>
</ul>
</li>
<li>Mark it up with text and do a text replacement so <code>let mastodon =</code> disappears
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>Simple to implement</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>Possibly brittle. Will rely on me not changing the wrapping of the function
<ul>
<li>But, I will notice pretty quickly if I get that wrong</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Something else? Search and see how others solve this
<ul>
<li>Because I tend to get too carried away at just solving the problem and not look around enough, because honestly, I think just adding some text markers is enough for what I need and I&rsquo;m inclined to just do that, because it&rsquo;s good enough for my needs.</li>
<li>But, let&rsquo;s start by doing a quick search and also ask Claude to help me search, since it&rsquo;s usually good at coming up with unique search patterns</li>
<li>Prompt to Claude:
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">I am working with n8n and want to maintain my workflow as code.
</span></span><span class="line"><span class="cl">My workflow uses custom javascript in some of the nodes so I have them stored on my filesystem and I want to extract the function in question and inject and run the function in my node.
</span></span><span class="line"><span class="cl">Are there existing solutions for doing this?
</span></span><span class="line"><span class="cl">My spontaneous reaction for how to solve it is:
</span></span><span class="line"><span class="cl">1. Add markers in text in my file
</span></span><span class="line"><span class="cl">2. Extract the content between those markers
</span></span><span class="line"><span class="cl">3. Strip the prefix/suffix around the function required for testing the function
</span></span><span class="line"><span class="cl">4. Wrap the remaining stuff in what I need to then use it in the workflow and inject it into a workflow template using jq</span></span></code></pre></div>
</div>
</li>
<li>Claude is questioning my approach as brittle, which is fair, it suggests I package it as a node module instead or some templating that injects the source code of a function. If I can just inject the source code of a function then that&rsquo;d be awesome, then I&rsquo;ll just make a little node script that outputs the correct thing for me
<ul>
<li>Right, so Claude dreamt up a suggested way of implementing my marker idea.
And a custom npm module would mean maintaining the code on the server and injecting it into n8n. It totally makes sense if I&rsquo;m doing more advanced stuff, but for my current situation, I think having a single file trumps that.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Decision:</strong> Mark it up with text and do a text replacement so <code>let mastodon =</code> disappears
<ul>
<li>My use-case is so small, this feels like the pragmatic choice until I have more advanced needs</li>
<li>I wonder if I should just do all this work in javascript or if I should do what my gut says: a small bash script.</li>
<li>Quick overview in my head: upside of using javascript is that I can just load the json into a file really easy, and then output to stdout in a pretty-printed form.
The logic to process between the markers isn&rsquo;t that hard and I already have the testing setup</li>
<li>The other option is to do it in Python since the rest of my blog is in python. But since n8n is more js maybe it makes sense to keep this stuff in the one language</li>
<li>And if I keep it with JS then I don&rsquo;t have to deal with jq, I can just do the whole thing in one pass by in JS. I think that&rsquo;ll be easier.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="designing-the-extraction">
  Designing the extraction
  <a href="#designing-the-extraction" class="heading-anchor" aria-label="Direct link to Designing the extraction"><span aria-hidden="true">#</span></a>
</h3>
<p>Okay, so what I&rsquo;ll do is create a little helper script that.</p>
<ol>
<li>I can point at a js file and it&rsquo;ll extract out the marker I have specified,.</li>
<li>Using the content of the marker, change it slightly so it&rsquo;ll work in place (make it self-calling)</li>
<li>Load the n8n workflow file, replace the key in the right place.</li>
<li>Pretty-print the json to STDOUT.</li>
</ol>
<p>I&rsquo;ll make the marker format <code>//&lt;EXTRACT:name&gt;</code>, the go convention of programmatic stuff doesn&rsquo;t start with a space and all :p.</p>
<h3 id="building-the-extractor">
  Building the extractor
  <a href="#building-the-extractor" class="heading-anchor" aria-label="Direct link to Building the extractor"><span aria-hidden="true">#</span></a>
</h3>
<p>I wrote up my first tests, ensured the first &ldquo;do nothing&rdquo; test passes, and now with my second test in place I&rsquo;m gonna ask Claude to implement it, because I don&rsquo;t know these node APIs. And claude did that quickly and the implementation seems sane. Claude decided to wrap everything so no errors would bubble up, so I removed that <code>catch</code> because any errors should bubble up because I don&rsquo;t want to miss out.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;extract.fromFile&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">test</span><span class="p">(</span><span class="sb">`if the markers aren&#39;t found, then nothing is returned`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">extract</span><span class="p">.</span><span class="nx">fromFile</span><span class="p">(</span><span class="s1">&#39;testdata/no-markers.txt&#39;</span><span class="p">,</span> <span class="s1">&#39;test&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="s1">&#39;expected an empty string when no match found&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="nx">test</span><span class="p">(</span><span class="sb">`if the markers are found, the content is returned`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">extract</span><span class="p">.</span><span class="nx">fromFile</span><span class="p">(</span><span class="s1">&#39;testdata/plain-text.txt&#39;</span><span class="p">,</span> <span class="s1">&#39;test&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s1">&#39;Hello, World!\n&#39;</span><span class="p">,</span> <span class="s1">&#39;expected the content between the markers&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>I also discovered when done that my tests weren&rsquo;t passing in intellij because it was running the tests from my repo root, so I changed my <code>Node.js test runner</code> config to default the working directory to <code>src/js/</code> for this repo, because I am deciding now that I&rsquo;ll only ever have javascript stuff there, and I&rsquo;ll only run the tests from that directory. It feels like a decent solution.
Maybe I have to revisit that if I ever introduce a package.json and decide whether it belongs here or in the root, but it probably belongs in <code>src/js/</code>, but that&rsquo;s the future 🤷.</p>
<p>Now, Claude did one thing I didn&rsquo;t intend yet (because there isn&rsquo;t any test), which is that it will ignore the whole line the markers are on (which I indicated in the docstring) but I don&rsquo;t have a test for it yet.
I want this behavior so that I don&rsquo;t have to do any special handling to ignore whatever comment marker there is.
So, let&rsquo;s add a test for this even if the code is already implemented. I guess I <em>could</em> remove that comment and reimplement, but that feels like waste, so I won&rsquo;t. Time to be pragmatic instead of dogmatic about the testing.</p>
<p>Hah! Adding the test was good, because I skimmed the code too quickly and it hadn&rsquo;t done what I expected, it was checking the full line. So I actually <em>do</em> have to change the code.</p>
<p>Cool, so I have my extract function.</p>
<h3 id="building-the-workflow-updater-script">
  Building the workflow updater script
  <a href="#building-the-workflow-updater-script" class="heading-anchor" aria-label="Direct link to Building the workflow updater script"><span aria-hidden="true">#</span></a>
</h3>
<p>Now I can create a script file and use it. I think I&rsquo;ll keep the script in <code>src/js/</code> folder and it&rsquo;ll take the path to the reference workflow file and read it in and parse, and then I&rsquo;ll work it from there.</p>
<p>I won&rsquo;t TDD this file because it should be at the level that I&rsquo;ll notice problems when using it. We&rsquo;ll see how much I&rsquo;ll regret <em>that</em> decision later.</p>
<p>In hope that Claude can do this for me, I started by documenting at the top of my script how I want it to work:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm"> This script&#39;s job is to:
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Extract the mastodon and bsky functions from n8n.copies.js.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Read the JSON file in the first argument and then replace the nodes for Mastodon and bsky respectively with their functions.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Output the updated JSON object on stdout, pretty-printed
</span></span></span><span class="line"><span class="cl"><span class="cm">Example: ./update_n8n_workflow.js ../bjorn-now-feed-distribution.json &gt; workflow.json
</span></span></span><span class="line"><span class="cl"><span class="cm">Will update the node id `e1bd320b-1222-4324-9168-22372d2e667c` so the parameter.status is `={{ ((item) =&gt; {... })($(&#39;RSS Feed Trigger&#39;).item) }}
</span></span></span><span class="line"><span class="cl"><span class="cm">*/</span></span></span></code></pre></div>
</div>
<p>Then I asked Claude to implement it:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">implement @src/js/update_n8n_workflow.js as described in it&#39;s first comment, use @src/js/extract.js to extract the information. Only implement the mastodon portion right now </span></span></code></pre></div>
</div>
<p>My example contained too much information, so claude suggested that it needed to <code>wrap the extracted function as: ={{ ((item) =&gt; {extracted code})($('RSS Feed Trigger').item) }}</code> but it should be <code>(...)($('RSS Feed Trigger').item)</code>, so I told Claude to plan again <code> I was unclear: the wrapping is &quot;(extracted code)($('RSS Feed Trigger').item)&quot;</code>.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Implementation Plan for update_n8n_workflow.js
</span></span><span class="line"><span class="cl"> 1. Import dependencies: Import fromFile from extract.js and Node.js fs module
</span></span><span class="line"><span class="cl"> 2. Read command line arguments: Get the workflow JSON file path from process.argv[2]
</span></span><span class="line"><span class="cl"> 3. Extract mastodon function: Use fromFile(&#39;./n8n.copies.js&#39;, &#39;mastodon&#39;) to extract the function code
</span></span><span class="line"><span class="cl"> 4. Read workflow JSON: Parse the workflow file
</span></span><span class="line"><span class="cl"> 5. Find and update node: Locate node with id e1bd320b-1222-4324-9168-22372d2e667c and update its parameters.status field to wrap the extracted function as: ={{ ((item) =&gt; {extracted code})($(&#39;RSS Feed Trigger&#39;).item) }}
</span></span><span class="line"><span class="cl"> 6. Output result: Print the updated JSON to stdout with pretty formatting
</span></span><span class="line"><span class="cl"> The script will be executable with: node update_n8n_workflow.js ../bjorn-now-feed-distribution.json &gt; workflow.json</span></span></code></pre></div>
</div>
<p>The plan now looks correct, it says &ldquo;arrow function&rdquo; instead of &ldquo;extracted code&rdquo;, which I guess is because it added a step to strip <code>let mastodon =</code> from the line, so I&rsquo;ll assume it does the correct thing, either way, this is close enough that manually fixing it will likely be trivial. allons-y!</p>
<h3 id="importing-the-updated-workflow">
  Importing the updated workflow
  <a href="#importing-the-updated-workflow" class="heading-anchor" aria-label="Direct link to Importing the updated workflow"><span aria-hidden="true">#</span></a>
</h3>
<p>A quick skim of the code and it looks correct, the output also seems correct to me, so let&rsquo;s save it to a file, run jq to verify it&rsquo;s valid json (I assume it is since node didn&rsquo;t bork out), and then upload and see what goes. My saved file locally is what&rsquo;s live now, so if it goes wrong, I can back out easily.</p>
<p>Right, interesting problem: n8n supports importing from a file and then it adds the file content <em>alongside</em> what you already have. So now I have a copy of all my existing nodes and with a 1 at the end. The content of my new Mastodon status node DOES look correct, though. So that&rsquo;s a win
Not quite what I had in mind, so I feel like I&rsquo;m doing something wrong.
Let&rsquo;s start by asking Claude about this workflow and see if they can push me towards some online docs.
or, n8n has these boxes &ldquo;I wish this node would…&rdquo; I can just ask there and see what goes.</p>
<p>Seems like this is working as intended (d&rsquo;uh) and the way to handle this is doing it by API. And… fair enough, can. so I asked Claude to make me bash script that I could use.
Then it started complaining that I didn&rsquo;t have a name in my file, which I do, so there seems to be a mismatch between the downloaded workflow and what the API expects.
D&rsquo;oh, I had missed a line in cURL <code>-d @-</code> to read the file, so that&rsquo;s on me.</p>
<p>Then I had issues because apparently my downloaded workflow doesn&rsquo;t just work with the API, so Claude and I did some back and forth and finally ended up with this that limits the fields I want to update:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="nv">$1</span> <span class="p">|</span> jq <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">           name,
</span></span></span><span class="line"><span class="cl"><span class="s1">           nodes,
</span></span></span><span class="line"><span class="cl"><span class="s1">           connections,
</span></span></span><span class="line"><span class="cl"><span class="s1">           settings
</span></span></span><span class="line"><span class="cl"><span class="s1">         }&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  curl -X PUT <span class="s2">&#34;</span><span class="si">${</span><span class="nv">N8N_HOST</span><span class="si">}</span><span class="s2">/api/v1/workflows/</span><span class="si">${</span><span class="nv">workflow_id</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;X-N8N-API-KEY: </span><span class="si">${</span><span class="nv">SANITARIUM_N8N_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -d @-</span></span></code></pre></div>
</div>
<p>Weirdly, I had to update the <code>settings</code> so it only contained the keys <code>executionOrder</code> and <code>errorWorkflow</code> as the other two values there weren&rsquo;t okay. Whatever, they didn&rsquo;t seem important so 🤷.</p>
<h3 id="validating-the-updated-workflow">
  Validating the updated workflow
  <a href="#validating-the-updated-workflow" class="heading-anchor" aria-label="Direct link to Validating the updated workflow"><span aria-hidden="true">#</span></a>
</h3>
<p>Now I have uploaded my changed thing, and it&rsquo;s time to verify it works as I want. I&rsquo;m not entirely sure what I should post to verify it :p.</p>
<p>I was looking at the result from running the update, and unless I always save the result of the update I <em>might</em> rerun from a prevous point when updating. Or, no, because it&rsquo;s a top-level field that I&rsquo;m ignoring when updating. So it should be fine. But, I think for now, I&rsquo;ll anyway pipe the update stuff <em>into</em> the saved file so it&rsquo;s there and see what changes between runs.</p>
<p>Committed, and tested by posting a scrap about &ldquo;wow… I really spent 5h doing this? That&rsquo;s longer than I expected.&rdquo; But, I guess the thing I have to keep in mind that this is the difference between &ldquo;engineering&rdquo; vs. &ldquo;just throwing stuff together.&rdquo; If I hadn&rsquo;t invested in the infrastructure stuff for managing this, I would&rsquo;ve been done after 30-40m when I updated the code.
The point was the investment in robustness I think I&rsquo;ve gained.</p>
<h2 id="repeat-for-bluesky">
  Repeat for Bluesky
  <a href="#repeat-for-bluesky" class="heading-anchor" aria-label="Direct link to Repeat for Bluesky"><span aria-hidden="true">#</span></a>
</h2>
<p>Handle the no-link with Bsky too.</p>
<h3 id="designing-the-change">
  Designing the change
  <a href="#designing-the-change" class="heading-anchor" aria-label="Direct link to Designing the change"><span aria-hidden="true">#</span></a>
</h3>
<p>Now, with all that done, time to look into Bsky, there I don&rsquo;t append the links but instead attach a &ldquo;website card.&rdquo;
There, I guess I&rsquo;d have to change it so:</p>
<ol>
<li>The body gets the same logic/setup.</li>
<li>The logic is repeated in the &ldquo;Website Card&rdquo; field, assuming that having an empty value there will do nothing (which I assume it will, Javascript being very used to weird cases of keys not existing, but I should test (and executed with an empty body there, it just worked), so just repeat the same logic in that place)</li>
</ol>
<p>this is a bit more annoying, will have to do some copy paste, but I can at least handle the consistency by creating a reusable test.</p>
<p>But I&rsquo;ll work on this either tomorrow or later today, it&rsquo;s after 16, and I feel like taking a break. And heading home feels like a good way to clear the head and then continue.</p>
<h3 id="implementing-reusable-tests">
  Implementing reusable tests
  <a href="#implementing-reusable-tests" class="heading-anchor" aria-label="Direct link to Implementing reusable tests"><span aria-hidden="true">#</span></a>
</h3>
<p><strong>20:03</strong> And it&rsquo;s after dinner, let&rsquo;s see if we can get this in so we can push it through.</p>
<p>What I think I&rsquo;ll do is just do a quick duplication of what I have, don&rsquo;t be clever, just do the dumbest thing possible.
With one caveat: I know the tests are mostly the same, so let&rsquo;s make the tests reusable and configurable and see if that pattern works in node (I bet it does).
Meaning, I want my existing tests for Mastodon to be shared with Bsky but change the limit to 300 char instead of 500.</p>
<p>The way I&rsquo;ll test that is that I&rsquo;ll make a function that returns a closure over my config, then I&rsquo;ll run it by passing in my code to execute and then see if the assertions match. I&rsquo;m not sure how it&rsquo;ll go with the test runner, but let&rsquo;s see. I.e. something like:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">reusableTest</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">maxLength</span><span class="p">,</span> <span class="nx">urlLength</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kd">function</span><span class="p">(</span><span class="nx">toTest</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">test</span><span class="p">(</span><span class="sb">`it does stuff`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">toTest</span><span class="p">(</span><span class="nx">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s2">&#34;whatever I want&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;mastodon&#39;</span><span class="p">,</span> <span class="nx">reusableTest</span><span class="p">(</span><span class="s1">&#39;mastodon&#39;</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">23</span><span class="p">))</span></span></span></code></pre></div>
</div>
<p>And another part is, I don&rsquo;t know how Bsky does links. There&rsquo;s something <a href="https://github.com/bluesky-social/atproto/discussions/3517">about facets</a> and how you can use them to reduce links but a big 🤷 in understanding that. I don&rsquo;t know if I&rsquo;ll care to look this up or not.</p>
<p>I could potentially do some smarts and just attach the URL as a card and remove if it&rsquo;s a single link in the post, which I could make as standard for myself.</p>
<p>Anyway, let&rsquo;s get the tests working now and see what goes first.</p>
<p>Okay, that kinda just worked as expected, a little surprised at how smooth it was. <a href="https://github.com/gaqzi/bjorn.now/commit/acd53a5056c11ea11c2d3accac69cf2c7a008cc9">The commit.</a></p>
<h4 id="is-this-really-reusable">
  Is this really reusable?
  <a href="#is-this-really-reusable" class="heading-anchor" aria-label="Direct link to Is this really reusable?"><span aria-hidden="true">#</span></a>
</h4>
<p>I am getting a bit worried that the setup might be… too fiddly, but I&rsquo;ll feel it out a bit more as I add in bsky. This feels like it&rsquo;s hitting the point where code <em>looks the same</em> but it isn&rsquo;t really reusable, because it looks similar but differs in intent so as we diverge it&rsquo;ll just add more <code>if</code>-statements. But, we&rsquo;ll continue going and feel it properly.</p>
<p>the first case: there isn&rsquo;t any clear &ldquo;a link is always 3 chars&rdquo; so it&rsquo;ll be counted as whatever the raw text is, so I&rsquo;ll use <code>-1</code> to indicate don&rsquo;t do anything (an <code>if</code>, urgh).
Spontaneous thought: I could pass in a function to handle the link calculation instead and just assume it sends me the link, so that would sort that problem out nicer and it keeps the test consistent. My favorite the strategy pattern showing up and making my day cleaner, again.</p>
<p>I&rsquo;ll just do that, it&rsquo;s much better than a magic number, and I can create that magic number as a variable from the function call. I know I have to implement this now, so it&rsquo;s the right time to &ldquo;Tidy First&rdquo; á Kent Beck. 🙂.</p>
<p>And there we hit the problem it: it&rsquo;s all very similar for good reasons, BUT the logic <strong>has to be different</strong> because the default way where I add the link at the end for Mastodon doesn&rsquo;t apply for bsky, there I can just attach it as a card.
So all the URL counting stuff don&rsquo;t matter.
So, I&rsquo;m better off copying the tests and changing them slightly, and calling out that they&rsquo;re similar but importantly different, any optimization here is folly.</p>
<p>I only spent ~15m on this, so I don&rsquo;t feel too bad about it. In some other cases it would&rsquo;ve worked out, and it does feel good to keep coding instead of just thinking a whole lot before doing anything.</p>
<p>So, <code>git reset --hard</code> on those files and a revert on the commit. Now let&rsquo;s start by copying and changing.</p>
<p>And, to get what I need later to decide whether to do the card I&rsquo;ll just look for <code>msg.endsWith('… more')</code> and if so, attach the link if it&rsquo;s a <code>/scrap/</code> link. So that&rsquo;s pretty straightforward, it does mean I have to include the info, but that&rsquo;s okay.</p>
<h3 id="refactoring-after-working-tests">
  Refactoring after working tests
  <a href="#refactoring-after-working-tests" class="heading-anchor" aria-label="Direct link to Refactoring after working tests"><span aria-hidden="true">#</span></a>
</h3>
<p>And done,  but looking at how I&rsquo;m deciding whether something is truncated, I don&rsquo;t get what I&rsquo;m doing. Why don&rsquo;t I just look at the string and check if it changed? That seems way more straightforward than comparing two fields from the RSS….</p>
<p>So I&rsquo;m just gonna do that and see what happens, but first, commit my changes, then change the format of how I work.</p>
<p>That was quick, maybe 5m to make the change. I looked at the workflow file and the difference between <code>contentSnippet</code> and <code>encoded:contentSnippet</code> seems to have to do with newlines and other formatting, so that&rsquo;s not gonna be very useful. Seems like a major brainfart that accidentally ended up working 🤷
(likely that those that didn&rsquo;t need truncating were on a single line)</p>
<p>Okay, next up, now to make sure that <code>update_n8n_workflow.js</code> handles bsky as well and updates it.</p>
<p>Went through and updated it, I mostly kept the very procedural code and just added on, maybe I&rsquo;ll clean it up a bit more if I loop back and add Telegram (for Telegram I&rsquo;m thinking I&rsquo;ll just do the preview in app button intead? but maybe it makes sense to put the scraps in there too, it&rsquo;s no different from the others and there&rsquo;s less links :))</p>
<h3 id="testing-bsky-live">
  Testing bsky live
  <a href="#testing-bsky-live" class="heading-anchor" aria-label="Direct link to Testing bsky live"><span aria-hidden="true">#</span></a>
</h3>
<p>And I made a post that would fit 300 chars about <a href="https://bjorn.now/scrap/2025-10-29t213248/">helping my brother install some RAM</a>, where the point behind is that having done it before gives confidence that instructions just don&rsquo;t give. Whether installing RAM or relying on your tests 😜. And it gave me a chance to see that the logic works.</p>
<p>What does the n8n update endpoint do and how does it work? Becuase it feels like not understanding how it&rsquo;s designed might come and bite me later, so let&rsquo;s not just rely on the vibe coded stuff that <em>seems</em> to be working.</p>
<h2 id="wednesday-eod">
  Wednesday EOD
  <a href="#wednesday-eod" class="heading-anchor" aria-label="Direct link to Wednesday EOD"><span aria-hidden="true">#</span></a>
</h2>
<p><strong>21:39</strong> And done, I&rsquo;ve finished bsky. Tomorrow I&rsquo;ll be hanging with the niece all day so I&rsquo;ll format and post this devlog when I&rsquo;m in the office again 🙂
Okay, scrap that (heh), I&rsquo;ll finish Telegram before posting. Then for formatting to post,  I&rsquo;ll do it by creating a script alongside Claude to figure out the formatting. I&rsquo;ll have to brainstorm how then.</p>
<h2 id="update-telegram-logic">
  Update Telegram logic
  <a href="#update-telegram-logic" class="heading-anchor" aria-label="Direct link to Update Telegram logic"><span aria-hidden="true">#</span></a>
</h2>
<p>Add the scrap logic to Telegram too (if it&rsquo;s a scrap, only post the text, otherwise the link)</p>
<h3 id="configuring-instant-views">
  Configuring Instant Views
  <a href="#configuring-instant-views" class="heading-anchor" aria-label="Direct link to Configuring Instant Views"><span aria-hidden="true">#</span></a>
</h3>
<p>Time to get Telegram to be a bit nicer. I overall don&rsquo;t want to force people to click through to my site if they&rsquo;re happy reading where they are. Telegram has these <a href="https://instantview.telegram.org/">Instant View</a> buttons and I can translate my links to use those instead, so I figure I&rsquo;ll do that in my channel so you can just click and see the full thing without loading my site. I guess this will mess with my statistics a bit (because I use <a href="https://plausible.io/">Plausible</a> which is doing it all in javascript), but 🤷, for now I&rsquo;m just happy if people wish to to engage with anything I post.</p>
<p>I did create a preview a couple of months back so I started by taking a link to my site and then copying in what I did last time:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">~version: &#34;2.1&#34;
</span></span><span class="line"><span class="cl">site_name: //*[@class=&#34;masthead&#34;]/a
</span></span><span class="line"><span class="cl">article: //article
</span></span><span class="line"><span class="cl">title: $article//*[@class=&#34;title&#34;]
</span></span><span class="line"><span class="cl">subtitle: $article//*[@class=&#34;subtitle&#34;]
</span></span><span class="line"><span class="cl">author: $article//*[@class=&#34;author&#34;]
</span></span><span class="line"><span class="cl">image_url: //meta[@property=&#34;og:image&#34;]
</span></span><span class="line"><span class="cl">published_date: $article//*[@class=&#34;published-at&#34;]
</span></span><span class="line"><span class="cl">body: $article//*[@class=&#34;content&#34;]</span></span></code></pre></div>
</div>
<p>That looks pretty good, but I&rsquo;m not seeing the published time in the live preview, the xpath expression looks right to me. And I similarly don&rsquo;t see the image_url, and looking in the template website I don&rsquo;t see that the <code>&lt;head&gt;</code> section is part of the example document (From the future: Nope, that&rsquo;s wrong, I was looking at the containing doc and not the iframe, it&rsquo;s all there. Telegram will always do a <code>GET</code> for your page to get the current version and then format the page using these rules). So maybe that&rsquo;s why? I&rsquo;ll skim through <a href="https://instantview.telegram.org/docs">the documentation</a> and see if I can find why.</p>
<p>I see that the <code>published_date</code> says &ldquo;unix time&rdquo; <a href="https://instantview.telegram.org/docs#instant-view-format">in the docs</a>, so maybe that&rsquo;s the problem? I gave it the element and it&rsquo;s in readable, but I do have the RFC3339 format string in the <code>&lt;time&gt;</code> tags <code>datetime</code> attribute. Let&rsquo;s keep digging.</p>
<p>Right, so the docs shows that there&rsquo;s a function <a href="https://instantview.telegram.org/docs#datetime">@datetime</a> that can take an xpath expression and return a unix timestamp, so I likely will have to give it the attribute and then specify the format so it can be converted.</p>
<p>So with that, the pattern should be something like this, which doesn&rsquo;t work, and I think I&rsquo;m trying to access the property incorrectly in xpath.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">@datetime(-2, &#34;en-SV&#34;, &#34;yyyy-MM-ddTHH:mm:ssZ&#34;): $article//*[@class=&#34;published-at&#34;][&#39;datetime&#39;]
</span></span><span class="line"><span class="cl">published_date: $@</span></span></code></pre></div>
</div>
<p>I asked Claude how to access it in xpath:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">I want to access the property `datetime` on the element matched by this xpath expression (for Telegram Instant View), how?
</span></span><span class="line"><span class="cl">`*[@class=&#34;published-at&#34;]</span></span></code></pre></div>
</div>
<p>Which gave me the answer that I need to access it by saying <code>/@datetime</code>, which <strong>to me</strong> reads that I &ldquo;dig into&rdquo; the element and <code>@</code> seems to have to do with attributes/properties of a tag. When I replaced <code>['datetime']</code> with <code>/@datetime</code> and saved my match then Telegram showed my publish date!</p>
<p><strong>TIL:</strong> Use <code>/@&lt;name&gt;</code> to access the value of an attribute when you&rsquo;ve selected an element with XPath.
With the expression <code>//*[@class=&quot;published-at&quot;]</code> you will get the HTML tag that has <code>class=&quot;published-at&quot;</code>, and if you then want the <code>datetime</code> property you add <code>/@datetime</code> to get it.</p>
<p>And now that I have the <code>published_at</code> working I&rsquo;ll see if I can figure out how to access my preview image which is available in <code>&lt;meta property=&quot;og:image&quot; content=&quot;&lt;url&gt;&quot;&gt;</code>, and I bet the problem is that I&rsquo;m not going into <code>/@content</code> the way I need to. Changed and that worked!</p>
<p>There&rsquo;s this Instant View property called <code>kicker</code> which I don&rsquo;t know what it means, so I asked Claude and it said:</p>
<blockquote>
<p><code>kicker</code> is the small text that appears <strong>above</strong> the main headline in article layouts - typically used for:</p>
<ul>
<li>Category labels (&ldquo;Politics&rdquo;, &ldquo;Technology&rdquo;)</li>
<li>Section names (&ldquo;Opinion&rdquo;, &ldquo;Analysis&rdquo;)</li>
<li>Eyebrow text (&ldquo;Breaking News&rdquo;, &ldquo;Exclusive&rdquo;)</li>
</ul>
</blockquote>
<p>I guess this is some publishing term and it sounds like my &ldquo;content types&rdquo; would be fitting here. I.e. post, crumb, link, scrap, etc. which I actually don&rsquo;t show at the moment on my single pages. Maybe I should add that in there? Or should I just skip it? It&rsquo;s part of the menu. Although, I have added it into the <code>article</code>&rsquo;s <code>class</code> attribute what category it is, because I have been planning to do custom styling of scraps (pre-emptive code, I know bad, but turns out this bit of unnecessary future planning might be helpful now.)
So, question is, can I strip away the <code>post</code> and <code>single</code> values from that string only leaving the unique piece?</p>
<p>In the list of <a href="https://instantview.telegram.org/docs#supported-functions">functions</a> there&rsquo;s a <code>@match</code> function that takes a regex, so that <em>could</em> work. Let&rsquo;s see if there&rsquo;s anything else before I go into making a regex (which admittedly can be pretty simple given how it&rsquo;s setup right now)</p>
<p>It seems that&rsquo;s the only function I have. So right now, I&rsquo;ll do it super simple and since my <code>class</code> attribute looks like this: <code>class=&quot;post devlog single&quot;</code> I&rsquo;ll just do <code>post ([^ ]+)</code> and then extract the second, the <code>single</code> also isn&rsquo;t needed, but since I&rsquo;m doing this on the <code>article</code> element I don&rsquo;t feel the need to do more, I&rsquo;ll just have the rule that the first class is always the post and this will work.
The <code>@match</code> function&rsquo;s second argument is which capturing group you want to use. While debugging it was a bit non-obvious to me that the debugger&rsquo;s output included <code>class=&quot;devlog&quot;</code> but it seems that it shows the full context, but when you add it to the kicker as <code>$@</code> (which is the return value of the last function call) then it&rsquo;ll just do the string and not where it was from. 🤷.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">@match(&#34;post ([^ ]+)&#34;, 1): $article/@class 
</span></span><span class="line"><span class="cl">kicker: $@</span></span></code></pre></div>
</div>
<p>Next up, <code>author_url</code> which is a property on the <code>&lt;a&gt;</code> tag which contains my name, so let&rsquo;s dig into it: <code>author_url: $article//*[@class=&quot;author&quot;]/@href</code>.</p>
<h3 id="using-instant-view-links-or-not">
  Using Instant View links or not?
  <a href="#using-instant-view-links-or-not" class="heading-anchor" aria-label="Direct link to Using Instant View links or not?"><span aria-hidden="true">#</span></a>
</h3>
<p>And with that, I have the Instant View working as I want in the preview. Now to figure out how I use it when posting so others can see it. <a href="https://instantview.telegram.org/#publishing-templates">The documentation</a> for it says:</p>
<blockquote>
<p>Note that if you send the resulting <code>t.me/iv?url=...&amp;rhash=...</code> link to other Telegram users, they will see an Instant View page built using your template (the <strong>rhash</strong> parameter in the link determines which template is used to create the page).</p>
</blockquote>
<p>So all I have to do is wrap my URL to this and it&rsquo;ll work. The downside of that is that then if you click that URL it&rsquo;ll lead to Telegram instead of my site, and it&rsquo;s not obvious to me how to click through to my site. I added the <code>document_url</code> but it seems it doesn&rsquo;t show the real URL.
I don&rsquo;t like this. MAYBE what I could do is add a second link that allows the instant preview, as a way of making it simpler for Telegram users. But iunno, it also feels clunky.
I could try to submit my template and see if it gets approved and then it just works.
I asked Claude and it says the same thing, it&rsquo;s not possible, and if you want that then you need to submit it. So that&rsquo;s the only real option, I don&rsquo;t want to completely nerf the readability for Instant View while I also want to make it simpler for others to read it where they are. The same reason I have the full content in the RSS feed. :)
So I added 10 links that should work, looked that they seemed correct, and added them as checked. Then I added the homepage and blog post listing pages and saw that they <strong>don&rsquo;t</strong> have any Instant View, which is what is supposed to happen.</p>
<h3 id="final-telegram-logic-and-decision">
  Final Telegram logic and decision
  <a href="#final-telegram-logic-and-decision" class="heading-anchor" aria-label="Direct link to Final Telegram logic and decision"><span aria-hidden="true">#</span></a>
</h3>
<p>Which leaves my Telegram posting logic as:</p>
<ol>
<li>
<p>If the URL of the item contains <code>/scrap/</code> then post the <code>content</code> in full (I&rsquo;ll deal with images later, but for now scraps are text-only with no formatting)</p>
</li>
<li>
<p>Otherwise, post only the link so it can be clicked through and if the Instant View is approved then that&rsquo;s available.</p>
</li>
</ol>
<p>And that took about 1.5h to do. Next up, to research how the update endpoint works for n8n, and then to do the formatting of this post and post.</p>
<h2 id="understand-how-n8n-updates-work">
  Understand how n8n updates work
  <a href="#understand-how-n8n-updates-work" class="heading-anchor" aria-label="Direct link to Understand how n8n updates work"><span aria-hidden="true">#</span></a>
</h2>
<p>I left a note for myself earlier that I wanted to check how n8n handles the updates, so I can feel more comfortable in not overriding stuff.</p>
<h3 id="digging-into-the-source-code">
  Digging into the source code
  <a href="#digging-into-the-source-code" class="heading-anchor" aria-label="Direct link to Digging into the source code"><span aria-hidden="true">#</span></a>
</h3>
<p>I figure it&rsquo;ll be a bit annoying to find this, so I&rsquo;ll just ask Claude to find it for me:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">where is the n8n source code for the `PUT` logic for workflow updates?</span></span></code></pre></div>
</div>
<p>And it did some searches and told me path in the repo is <a href="https://github.com/n8n-io/n8n/blob/n8n%401.116.0/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts#L268">packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts</a> so I went to GitHub and opened.</p>
<p>It does special handling for <code>settings</code> and will <a href="https://github.com/n8n-io/n8n/blob/f65b6a82a95f0fd298422faa5f6cf3f69bbd3249/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts#L107-L112">only change settings</a> I have set and otherwise let existing ones be.</p>
<p>Okay, working my way to the <a href="https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/db/src/repositories/workflow.repository.ts#L52">WorkflowRepository</a> it doesn&rsquo;t implement <code>update</code> itself, so it&rsquo;s probably in the base class, so let&rsquo;s see what&rsquo;s there.
The base class comes from <code>@n8n/typeorm</code> and I don&rsquo;t know where that is. I looked in the <code>@n8n</code> folder on GitHub but didn&rsquo;t see it. I tried pressing <code>t</code> and typing it but it doesn&rsquo;t seem to be part of the path. The search showed that there&rsquo;s a <code>CLAUDE.md</code> so maybe it contains something useful? (I&rsquo;ll dig a bit first before asking Claude to find it for me).
And there I find this: <code>**Backend:** Node.js + TypeScript + Express + TypeORM</code> which seems to indicate that TypeORM might be a general package?
I searched for <code>TypeORM</code> and saw a linter step to ensure we only use it in <code>@n8n/db</code> folder, so is there a definition of <code>@n8n/typeorm</code> in the <code>db/</code> folder?
The <code>package.json</code> lists the dependency as <code>&quot;@n8n/typeorm&quot;: &quot;catalog:&quot;,</code>, so maybe there&rsquo;s something in this catalog folder (or maybe package?)
I had a gut feel since I saw <code>pnpm</code> mentioned and something around monorepos, maybe this is a special <code>pnpm</code> thing so I did a search for &ldquo;pnpm catalog&rdquo; and found explains it&rsquo;s a &ldquo;<a href="https://pnpm.io/catalogs">workspace feature for defining dependency version ranges as reusable constants</a>.&rdquo; So if I get that right, it&rsquo;s how you ensure the same version of TypeORM is used across this entire monorepo. Sounds neat.
So, that means I&rsquo;ll just assume we&rsquo;re doing the bog standard TypeORM stuff so I&rsquo;ll look at that. But I will assume it&rsquo;ll simply do: replace the named keys if they&rsquo;re part of what comes in, because a generic package usually doesn&rsquo;t do more than that.</p>
<p>TypeORM&rsquo;s documentation for update says: <code>update</code> - Updates entities by entity id, ids or given conditions. Sets fields from supplied partial entity.
And I read that to mean: we&rsquo;ll replace all keys we see, and the others will not be touched. So I won&rsquo;t accidentally unset any fields I don&rsquo;t change.
SO, maybe then I should try and limit what I send even further since I only really want to update the <code>nodes</code> portion, so could I get away with only sending that? And the answer is no, because then it gives an error that I need to provide <code>connections</code>.</p>
<h3 id="adding-only-update-if-version-is-what-i-expect-on-the-remote">
  Adding &ldquo;only update if version is what I expect on the remote&rdquo;
  <a href="#adding-only-update-if-version-is-what-i-expect-on-the-remote" class="heading-anchor" aria-label="Direct link to Adding &ldquo;only update if version is what I expect on the remote&rdquo;"><span aria-hidden="true">#</span></a>
</h3>
<p>But, it&rsquo;s fine, I&rsquo;m anyway saving the version in the repo after each update, so I would only loose things if the version live is different from what I have. Which for now, I don&rsquo;t worry about. I COULD do a check to see that the current version is the same as what I have downloaded or warn. I guess that <em>would</em> be pretty tiny effort, and therefore worth it since all you&rsquo;d have to do to sort it out is run <code>get-workflow</code> and then recreate the update and check it.
So… letting me know there&rsquo;s a mismatch WOULD be useful.</p>
<p>The way to do that then is: <code>jq -r '.versionId' bjorn.now-feed-distribution.json</code> gives me the version id, so then I would have to pull down the current version of the workflow and check the version number is the same:
<code>./get-workflow | jq -r '.versionId'</code>
and then compare it, so… screw it, let&rsquo;s put that in the update script.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Only update the workflow if the `.versionId` ISN&#39;T the same as the server,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># because we&#39;re downloading the workflow and then applying our updates to it.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This intends to avoid overwriting anything we didn&#39;t expect.</span>
</span></span><span class="line"><span class="cl"><span class="nv">localVersion</span><span class="o">=</span><span class="k">$(</span>jq -r <span class="s1">&#39;.versionId&#39;</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">serverVersion</span><span class="o">=</span><span class="k">$(</span><span class="nv">workflow_id</span><span class="o">=</span><span class="nv">$workflow_id</span> ./get-workflow 2&gt;/dev/null <span class="p">|</span> jq -r <span class="s1">&#39;.versionId&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$localVersion</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$serverVersion</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;The local workflow file (</span><span class="nv">$1</span><span class="s2">) is stale to the server version&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  local version : </span><span class="si">${</span><span class="nv">localVersion</span><span class="si">}</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  server version: </span><span class="si">${</span><span class="nv">serverVersion</span><span class="si">}</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Hint: Pull down the latest version as and recreate:&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  ./get-workflow &gt; bjorn.now-feed-distribution.json&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  ./create-update&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div>
</div>
<p>Added a hint to myself in case I forget.</p>
<h2 id="final-thoughts">
  Final thoughts
  <a href="#final-thoughts" class="heading-anchor" aria-label="Direct link to Final thoughts"><span aria-hidden="true">#</span></a>
</h2>
<p>This entire endeavour got a bit more robust than what I&rsquo;ve had before with my n8n setup, but that&rsquo;s because I&rsquo;m planning to add more functionality to my POSSE setup. So I wanted to make sure I don&rsquo;t create more of a mess for myself as I change things, and create more things to check.</p>
<p>This isn&rsquo;t 100% the production level I would do but it&rsquo;s good enough for where I am. The &ldquo;create the update&rdquo; script is pretty rough, but I also expect that I will <em>very rarely</em> change it, and it&rsquo;s built around a couple of assumptions that I don&rsquo;t see changing. So keeping it hacky, or more as disposable, feels okay to me. If I had more people working on this I might not be, but for now, there&rsquo;s enough guard rails.</p>
<p>I&rsquo;m happy I decided to add the optimistic check that we&rsquo;re updating against the version on the server, it&rsquo;s the kind of thing that was <em>really</em> straightforward and simple to add and it&rsquo;ll rarely fail, but it&rsquo;ll warn in case I end up doing click-ops and forget to sync back to the repo.</p>
]]></content:encoded></item><item><title>Fixing Mastodon URL truncation in n8n</title><link>https://bjorn.now/devlog/2025-10-28-fixing-mastodon-url-truncation-in-n8n-module/?utm_medium=feed</link><pubDate>Tue, 28 Oct 2025 13:35:07 +0100</pubDate><guid>https://bjorn.now/devlog/2025-10-28-fixing-mastodon-url-truncation-in-n8n-module/</guid><description>I have been working on automating my POSSE setup, and ended up trying n8n and it has been a great experience, there are so many plugins that it’s mostly just wiring things together.
But, I discovered that despite being within the character limit, one of my URLs were truncated when posting to Mastodon. This is because Mastodon does something non-obvious: it treats all URLs as 23 characters, no matter if they’re shorter or longer.</description><content:encoded><![CDATA[<p>I have been working on automating my <a href="https://indieweb.org/POSSE">POSSE setup</a>,
and ended up trying <a href="https://n8n.io">n8n</a> and it has been a great experience,
there are so many plugins that it&rsquo;s mostly just wiring things together.</p>
<p>But, I discovered that despite being within the character limit,
one of my URLs were truncated when posting to Mastodon.
This is because Mastodon does something non-obvious:
it treats all URLs as 23 characters, no matter if they&rsquo;re shorter or longer.</p>
<p>This is because they copied this behavior from Twitter,
which ran all links through their t.co link shortener.
The reason is that Mastodon doesn&rsquo;t want you to use a link shortener,
since it hides the real URL (i.e. makes rick rolling easier 😜).</p>
<p>So the module truncated the text in the status.
Since it wasn&rsquo;t URL-aware, all I got was a broken link.</p>
<p>I decided to fix it myself, because dammit, I&rsquo;m a programmer.
It&rsquo;s an unfamiliar codebase I know nothing about,
so I rolled up my sleeves and asked Claude,
&ldquo;<em>Pretty please, can you fix the problem and add tests?</em>&rdquo;
(ah the modern world we live in)</p>
<p>The key constraint: I needed to understand every line of code before shipping it,
and it had to pass my quality standards because my <a href="https://bjorn.now/blog/2025/07/24/your-name-is-still-on-it/">name would be on the PR.</a>
Happily, that worked out <a href="https://github.com/redoracle/n8n-nodes-the-mastodon/pull/4">becuse my PR was accepted</a>. 🙂</p>
<p>Here&rsquo;s how I worked through it over three days. All <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commits/url-aware-post-length-calculation/">commits are here</a> if you want to step through them.
I really intend to share these devlogs earlier in the future, but I got lazy 😅</p>
<h2 id="the-rough-notes">
  The rough notes
  <a href="#the-rough-notes" class="heading-anchor" aria-label="Direct link to The rough notes"><span aria-hidden="true">#</span></a>
</h2>
<p>Lightly edited for the web, and I&rsquo;ve kept the day breaks in.</p>
<h3 id="2025-10-08">
  2025-10-08
  <a href="#2025-10-08" class="heading-anchor" aria-label="Direct link to 2025-10-08"><span aria-hidden="true">#</span></a>
</h3>
<p>I discovered that my mastodon poster truncated the URL (which seems to indicate that the n8n plugin does a naïve counting of characters, whereas it should only count 23 chars/URL, because Mastodon don&rsquo;t penalize longer URLs so we don&rsquo;t need link shorteners.</p>
<p>So I looked at the source for the package in question and found the 500 limit (I had looked at this before) and knew it was hardcoded, and then I decided to download the repo and ask Claude to find the bug and suggest a fix:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">this is a plugin for n8n, and @nodes/Mastodon/status/StatusMethods.ts seems to have a bug where if I post a status with a link then it will truncate the status assuming the link takes up the full
</span></span><span class="line"><span class="cl">length it has, instead of only 23 characters as is defined by Mastodon. Identify where this bug comes from, explain it, and come up with a plan for how to fix it. Also suggest tests to add that
</span></span><span class="line"><span class="cl">fits with existing tests in this repostiory.</span></span></code></pre></div>
</div>
<p>It then came up with a plan that I rejected because I wondered if there any existing packages that implement this logic, since the Javascript community is quite keen on tiny packages</p>
<details>
<summary>The rejected plan to implement</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Fix Plan
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Create URL-aware character counting utilit
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add new method ValidationUtils.calculateMastodonLength() that:
</span></span><span class="line"><span class="cl">  - Detects URLs in the text using regex
</span></span><span class="line"><span class="cl">  - Counts each URL as 23 characters (Mastodon&#39;s characters_reserved_per_url)
</span></span><span class="line"><span class="cl">  - Counts remaining text normally
</span></span><span class="line"><span class="cl">  - Returns the effective character count
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Update validateStatusParam function
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Replace simple sanitizeStringParam with URL-aware validation
</span></span><span class="line"><span class="cl">- Use the new calculateMastodonLength() to check if status exceeds 500 chars
</span></span><span class="line"><span class="cl">- Only trim/reject if the effective length (with URL shortening) exceeds limit
</span></span><span class="line"><span class="cl">- Keep the full URL text intact (don&#39;t truncate it)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Add comprehensive tests
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Following the existing test pattern in __tests__/status.test.ts, add:
</span></span><span class="line"><span class="cl">- Test for status with single URL that would exceed 500 chars without URL counting
</span></span><span class="line"><span class="cl">- Test for status with multiple URLs
</span></span><span class="line"><span class="cl">- Test for status with URL at the beginning, middle, and end
</span></span><span class="line"><span class="cl">- Test that status correctly rejects when effective length &gt; 500
</span></span><span class="line"><span class="cl">- Test that long URLs are preserved in the request body
</span></span><span class="line"><span class="cl">- Test edge cases: URL-like text that isn&#39;t a URL, localhost URLs, etc.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Optional enhancement
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Consider fetching instance configuration (characters_reserved_per_url) dynamically
</span></span><span class="line"><span class="cl">- For now, hardcode 23 as it&#39;s the Mastodon standard</span></span></code></pre></div>
</div>
</details>
<p>So I asked Claude to look for a package:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">search and see if there&#39;s an existing npm package that does this calculation of post length per mastodon so we don&#39;t have to write it ourselves</span></span></code></pre></div>
</div>
<p>It searched and didn&rsquo;t find anything (it was a bit slow, so I decided to look as well, and didn&rsquo;t find anything obvious), when I looked back after giving up it asked to download the original Ruby source for the <a href="https://github.com/mastodon/mastodon/blob/main/app/validators/status_length_validator.rb">status length validation</a> and then came up with new search patterns</p>
<p>It then found twitter-text and suggested to import it and use</p>
<details>
<summary>Plan to use twitter-text</summary>
<div class="full-witdh no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Fix Plan for Mastodon URL Character Counting Bug
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Add twitter-text dependency
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Run npm install twitter-text (version 3.1.0, stable and well-maintained)
</span></span><span class="line"><span class="cl">- This library provides the same URL detection/counting logic that Mastodon uses
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Update ValidationUtils in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add new method ValidationUtils.countMastodonLength(text: string): number
</span></span><span class="line"><span class="cl">  - Use twitter-text&#39;s parseTweet() or extract URLs manually
</span></span><span class="line"><span class="cl">  - Count each URL as 23 characters (Mastodon&#39;s standard)
</span></span><span class="line"><span class="cl">  - Return effective character count
</span></span><span class="line"><span class="cl">- Keep existing sanitizeStringParam for other use cases
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Update validateStatusParam in StatusMethods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Replace line 16: Instead of truncating, validate the effective length
</span></span><span class="line"><span class="cl">- If effective length &gt; 500, throw error with helpful message
</span></span><span class="line"><span class="cl">- Don&#39;t truncate the actual status text (preserve full URLs)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Add comprehensive tests to tests/status.test.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Following existing test patterns, add:
</span></span><span class="line"><span class="cl">- Test: Status with long URL (&gt;100 chars) that fits within 500 char limit
</span></span><span class="line"><span class="cl">- Test: Status with multiple URLs counted correctly
</span></span><span class="line"><span class="cl">- Test: Status that exceeds 500 chars even with URL shortening
</span></span><span class="line"><span class="cl">- Test: URL preservation (ensure full URL is sent to API, not truncated)
</span></span><span class="line"><span class="cl">- Test: Edge cases (URL at start/middle/end, multiple URLs)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This approach uses a proven library rather than reinventing URL detection regex.</span></span></code></pre></div>
</div>
</details>
<p>I wasn&rsquo;t sure about all this because looking at the repo it looks like a big dependency, and maybe it has too much extra stuff that doesn&rsquo;t really come into play for Mastodon?</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">will `parseTweet()` introduce any other logic that might not work with mastodon? Might we be better off copying the code around links (and tests) and adding it to this repo (do the licenses match?)
</span></span><span class="line"><span class="cl">We *cannot* break the existing behavior and throw an error instead of truncating, but we can change the truncating behavior to the effective length instead of actual length. </span></span></code></pre></div>
</div>
<p>It then went off and did a bunch of searches around east-asian specific character encodings (Japanese and Chinese alphabets as far as I can tell)</p>
<p>I then got a new plan that looks better, but I still wonder about the <code>when possible</code> for preserving URLs (I expect it is that if there&rsquo;s not 23 chars left, then it <em>has</em> to cut, so I will have it clarified in a more detailed plan)</p>
<details>
<summary>The new plan that includes <code>when possible</code> for preserving URLs</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Updated Fix Plan (Mastodon-Compatible)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Copy URL regex from twitter-text (with attribution)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add file header comment crediting twitter-text (Apache 2.0 license)
</span></span><span class="line"><span class="cl">- Copy only the URL detection regex pattern (battle-tested, handles edge cases)
</span></span><span class="line"><span class="cl">- No dependencies needed - just the regex
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Create ValidationUtils.calculateMastodonLength() in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Extract all URLs using the copied regex
</span></span><span class="line"><span class="cl">- Replace each URL with 23-char placeholder (&#34;x&#34;.repeat(23))
</span></span><span class="line"><span class="cl">- Count remaining characters using standard .length
</span></span><span class="line"><span class="cl">- Return the effective character count
</span></span><span class="line"><span class="cl">- No CJK weighting - Mastodon treats all chars equally
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Update validateStatusParam() in StatusMethods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Calculate effective length using new method
</span></span><span class="line"><span class="cl">- If effective length &gt; 500, truncate at boundary that fits within 500 effective chars
</span></span><span class="line"><span class="cl">- Preserve full URLs when possible (don&#39;t cut URLs)
</span></span><span class="line"><span class="cl">- Keep existing truncation behavior (no breaking changes)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Add tests to tests/status.test.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Status with long URL (&gt;100 chars) that fits in 500 effective chars
</span></span><span class="line"><span class="cl">- Status with multiple URLs
</span></span><span class="line"><span class="cl">- Status requiring truncation with URL preservation
</span></span><span class="line"><span class="cl">- CJK characters counted same as Latin (no weighting)
</span></span><span class="line"><span class="cl">- Edge cases: URL at boundaries, multiple URLs</span></span></code></pre></div>
</div>
</details>
<p>I asked what <code>when possible</code> means and guessed some cases (I forgot to save the prompt)</p>
<details>
<summary>The details for when it's not possible to save the URL</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">⏺ Cases Where We Cannot Preserve URLs:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Theoretical impossible case (highly unlikely in practice):
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: &#34;Check this out: https://example.com/article&#34;
</span></span><span class="line"><span class="cl">Actual length: 44 chars
</span></span><span class="line"><span class="cl">Effective length (URL=23): 39 chars ✅ Fits!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">But if someone tried:
</span></span><span class="line"><span class="cl">Text: 480 chars of text + &#34; https://example.com/very/long/url&#34;
</span></span><span class="line"><span class="cl">Actual: 520 chars
</span></span><span class="line"><span class="cl">Effective: 480 + 23 = 503 chars ❌ Over limit!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To fit within 500 effective chars, we&#39;d need to:
</span></span><span class="line"><span class="cl">- Remove 3 chars from somewhere
</span></span><span class="line"><span class="cl">- We can&#39;t split the URL (would break it)
</span></span><span class="line"><span class="cl">- We&#39;d have to remove text BEFORE the URL
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Multiple URLs scenario:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: &#34;See https://example1.com and https://example2.com and more text...&#34;
</span></span><span class="line"><span class="cl">If effective length &gt; 500, we might need to:
</span></span><span class="line"><span class="cl">- Keep URL1 (23 chars)
</span></span><span class="line"><span class="cl">- Keep URL2 (23 chars)
</span></span><span class="line"><span class="cl">- Truncate the &#34;and more text...&#34; part
</span></span><span class="line"><span class="cl">- OR drop URL2 entirely if there&#39;s not enough room
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. The actual problematic case:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: 450 chars + &#34;Visit https://example.com/some/long/url for more info and keep reading...&#34;
</span></span><span class="line"><span class="cl">Effective: 450 + 23 + 35 = 508 chars ❌
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To fix:
</span></span><span class="line"><span class="cl">- Keep first 450 chars ✅
</span></span><span class="line"><span class="cl">- Keep URL (counts as 23) ✅
</span></span><span class="line"><span class="cl">- Truncate &#34;for more info and keep reading&#34; to fit in remaining 27 effective chars
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The Real Answer:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We can ALWAYS preserve URLs! Here&#39;s why:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- URLs only count as 23 chars in the effective length
</span></span><span class="line"><span class="cl">- If someone has room for 23 effective chars, they have room for the full URL (even if it&#39;s 200 chars long)
</span></span><span class="line"><span class="cl">- We just need to truncate the non-URL text to make room
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The algorithm:
</span></span><span class="line"><span class="cl">1. Find all URLs and their positions
</span></span><span class="line"><span class="cl">2. Calculate effective length
</span></span><span class="line"><span class="cl">3. If over 500:
</span></span><span class="line"><span class="cl">  - Truncate text BETWEEN or AFTER URLs
</span></span><span class="line"><span class="cl">  - Never truncate the URL itself
</span></span><span class="line"><span class="cl">  - Work backwards to find a safe truncation point
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Edge case that might force URL removal:
</span></span><span class="line"><span class="cl">Text with 22 URLs: Each counts as 23 = 506 effective chars
</span></span><span class="line"><span class="cl">Even with ZERO other text, we&#39;re over the limit!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">In this case, we&#39;d have to drop some URLs entirely (remove the last URL(s) until we fit).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Should I proceed with a plan that preserves URLs when possible, but drops trailing URLs if necessary when there are too many?</span></span></code></pre></div>
</div>
</details>
<p>So, now to handle this I will ask it to write this high-level plan into <code>high-level.plan.md</code> and then to create a detailed plan that could be implemented by a junior engineer, so we then can ask sub agents to implement them step-by-step. I saw the plan and it suggested to drop URLs that went over the limit, so I asked it to instead keep them.</p>
<p>I haven&rsquo;t added any general/background context to this repo about my preferences, so I wonder if this will be enough to give me something useful in this repo:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">create a deatiled implementation plan that a junior engineer could implement.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ensure each step is written in a TDD fashion and that the tests are passing, changed to show the expected new behavior (red),
</span></span><span class="line"><span class="cl">and then the change is implemented to make it pass (green).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Only implement as much as is needed to get each test passing and no more. 
</span></span><span class="line"><span class="cl">Cover the copied code from Twitter with tests too.</span></span></code></pre></div>
</div>
<p>It came up with a detailed plan, it looked good enough (skimmed it), so I asked claude to:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">execute the test suite to verify it runs, if it runs successfully then document how to run the tests in the plan and then save the plan as plan.md</span></span></code></pre></div>
</div>
<p>Then it couldn&rsquo;t run any commands, likely because I never ran <code>npm install</code> in the folder so I did that and verified that <code>npm test</code> worked and then asked Claude to try again.</p>
<p>Next up, execute the plan: <code>Now plan to execute @plan.md with subagents and marking off which steps has been done in the plan as they finish.</code>.</p>
<details>
<summary>The plan</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Strategy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Use general-purpose agents in parallel where possible to implement the plan steps efficiently. Track progress by updating plan.md with checkmarks as each step completes.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 1: URL Extraction (Steps 1.1-1.6)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 1: Implement URL extraction utility with all tests
</span></span><span class="line"><span class="cl"> - Create __tests__/url-extraction.test.ts
</span></span><span class="line"><span class="cl"> - Add extractUrls() to ValidationUtils in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 1.1-1.6
</span></span><span class="line"><span class="cl"> - Mark steps 1.1-1.6 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 2: Length Calculation (Steps 2.1-2.5)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 2: Implement Mastodon length calculation with all tests
</span></span><span class="line"><span class="cl"> - Create __tests__/mastodon-length.test.ts
</span></span><span class="line"><span class="cl"> - Add calculateMastodonLength() to ValidationUtils
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 2.1-2.5
</span></span><span class="line"><span class="cl"> - Mark steps 2.1-2.5 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 3: URL-Aware Truncation (Steps 3.1-3.9)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 3: Implement truncation with URL preservation
</span></span><span class="line"><span class="cl"> - Create __tests__/url-truncation.test.ts
</span></span><span class="line"><span class="cl"> - Add truncateWithUrlPreservation() to ValidationUtils
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 3.1-3.9
</span></span><span class="line"><span class="cl"> - Mark steps 3.1-3.9 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 4: Status Validation Update (Steps 4.1-4.4)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 4: Update status validation to use new utilities
</span></span><span class="line"><span class="cl"> - Add tests to __tests__/status.test.ts
</span></span><span class="line"><span class="cl"> - Update validateStatusParam in StatusMethods.ts
</span></span><span class="line"><span class="cl"> - Verify all new tests pass
</span></span><span class="line"><span class="cl"> - Mark steps 4.1-4.4 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 5: Final Verification (Step 5)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Run npm test to verify all tests pass
</span></span><span class="line"><span class="cl">- Update plan.md with final completion status
</span></span><span class="line"><span class="cl">- Report summary of what was implemented</span></span></code></pre></div>
</div>
</details>
<p>I then set Claude to execute and left some questions for myself to answer after we were done:</p>
<ul>
<li>Are there more places we should use this new validation/truncation logic?</li>
<li>Is our new code implemented similarly to the rest of the code in this repository?</li>
<li>What questions should I ask you to verify that we don&rsquo;t break something?</li>
</ul>
<p><strong>00:34</strong> It&rsquo;s late, so time to let Claude spin away while I prepare for sleep.</p>
<p><strong>00:50</strong> Okay, while prepping for sleep it finished and I had a look on the new tests
the tests have some problems that we&rsquo;ll need to iterate on:</p>
<ul>
<li>Doesn&rsquo;t assert the exact values we&rsquo;re looking for</li>
<li>Doesn&rsquo;t make the 500 or 23 constants and are instead magic numbers, let&rsquo;s name them</li>
<li>The truncation logic is <strong>too smart</strong> and it should just cut the URL and leave an ugly &ldquo;scar&rdquo; if that&rsquo;s what&rsquo;s required, that&rsquo;s fine. We can iterate on that</li>
</ul>
<p>I think the way to work with this is to create a fork and do my changes in a repo with small focused commits, and then show the work as I commit, so I indicate how I grew the solution</p>
<p>Now, sleep for reals</p>
<h3 id="2025-10-10">
  2025-10-10
  <a href="#2025-10-10" class="heading-anchor" aria-label="Direct link to 2025-10-10"><span aria-hidden="true">#</span></a>
</h3>
<p>Okay, back at it again after a day&rsquo;s break, got some tunes on and now to review.</p>
<p>Okay, to help me document how I&rsquo;m working on this I will <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/8765c425b0cfffaf31d2b56ec00b4fa2dc855c72">commit the change I got from Claude</a>, and then I&rsquo;ll do my changes from there and commit as I do them.
Doing this so that it&rsquo;ll be easier to blog about it, and I don&rsquo;t know if that workflow will be what I want/need, so we&rsquo;ll see.</p>
<h4 id="reviewing">
  Reviewing
  <a href="#reviewing" class="heading-anchor" aria-label="Direct link to Reviewing"><span aria-hidden="true">#</span></a>
</h4>
<p>Starting the review by looking at the <code>status.test.ts</code> since I&rsquo;m supposed to have good tests that tell me what&rsquo;s going on, so that&rsquo;ll be the lense through which I review this.</p>
<p>First off, I got header comments explaining the change that I don&rsquo;t need, so nixing &rsquo;em, and I think all these tests belong in a <code>describe</code> that shows that the tests belong together. <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/028b146d20234486eef241b63e9268e06d744a49">Commit</a></p>
<p>Then, let&rsquo;s get going with the first test &ldquo;should preserve long URL when using URL-aware character counting&rdquo; doesn&rsquo;t do one that will hit the problem, and the test doesn&rsquo;t check the count is what we expect it to be (which is also a problem). okay, changed the test to <strong>only contain</strong> a URL and then verify that the expected status is exactly 23 characters long, which passes. <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/f8500029bb7a2acae6763b1093501fb6ff681b67">Commit</a></p>
<p>Next is a test with multiple long URLs, which is useful, make sure we don&rsquo;t do this for only one URL in the status. The URLs are not pushing it as far as it should, so I made both URLs more than the total length of a status <code>(80 -&gt; 500)</code></p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">url1</span> <span class="o">=</span> <span class="s1">&#39;https://example.com/&#39;</span> <span class="o">+</span> <span class="s1">&#39;a&#39;</span><span class="p">.</span><span class="nx">repeat</span><span class="p">(</span><span class="mi">80</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">url2</span> <span class="o">=</span> <span class="s1">&#39;https://www.example.org/&#39;</span> <span class="o">+</span> <span class="s1">&#39;b&#39;</span><span class="p">.</span><span class="nx">repeat</span><span class="p">(</span><span class="mi">80</span><span class="p">);</span></span></span></code></pre></div>
</div>
<p>The <code>text</code> is a bit annoying so simplifying it for my own sake to make the length calculation simpler, it&rsquo;s a list of links one per line <code>(2*2+1 = 5 + 23 + 23 = 51)</code>:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="sb">`- </span><span class="si">${</span><span class="nx">url1</span><span class="si">}</span><span class="sb">\n- </span><span class="si">${</span><span class="nx">url2</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span></span></span></code></pre></div>
</div>
<p><a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/9a253678e5f4c4e8621a63c262374433c5f37ad4">Commit.</a></p>
<p>And about now I&rsquo;m annoyed at the way the mock implementation has so much setup, it doesn&rsquo;t follow my coding style (too much repetition), but this isn&rsquo;t my project, and it seems to be how all other tests are written in this file so we keep it. <strong>Consistency and avoiding surprises is more important than my personal style.</strong></p>
<p>Now comes the really nasty bit: Claude decided to <strong>truncate text instead of URLs</strong> which <em>isn&rsquo;t what I want</em>, if the URL is placed so that it&rsquo;s unavoidable that it will be cut, then cut it. Don&rsquo;t remove it, and don&rsquo;t remove text before it. It&rsquo;s better to indicate that something went wrong (by showing a cut URL) than silently dropping it
(I&rsquo;d rather go boom than be silent, been saying this for a very looong time, mostly living that philosophy :p)</p>
<p>So I think this particular test will have to change. And about now I&rsquo;m regretting that I didn&rsquo;t commit already because I feel I should&rsquo;ve done it per test instead. Because now I&rsquo;m about to make the test red, so I want to make sure I can change it cleanly. So, I&rsquo;ll undo my changes (thanks IntelliJ for having infinite undo in the same session), commit, and then redo per function.
That took ~5min to fix, and while not really necessary, I feel a lot better from going back and redoing and creating clean commits to show the work (so the commits linked was made before this).</p>
<p>Okay, now, this test I&rsquo;ll also change up a bit:</p>
<ul>
<li>Loong text, <code>a</code>x480, a space, and a url which means the effective length will be 505 so the URL will be cut</li>
<li>I changed the assertion on the <code>sentStatus</code> to be the naively cut status instead (since the URL can&rsquo;t ever fit now, so it will just have to mangle the URL)</li>
<li>And as expected, the test is now failing, so that&rsquo;s good. Now, let&rsquo;s ask Claude to fix the failing test I&rsquo;ve created</li>
<li>I highlighted the test in IntelliJ and opened up Claude and said (mostly because I haven&rsquo;t looked at the code, this might actually be faster to do manually, so I&rsquo;ll look at it as the fresh Claude session is figuring out what&rsquo;s going on):
<ul>
<li>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">The highlighted test is failing correctly, I want to adjust the business logic to match. Confirm the test is failing by running `npm test` and then plan how to fix it </span></span></code></pre></div>
</div>
</li>
</ul>
</li>
</ul>
<p>Found the place where this is, it&rsquo;s in <code>nodes/Mastodon/Mastodon_Methods.ts</code> and the new <code>truncateWithUrlPreservation</code>.</p>
<p>And I see that Claude is spinning and reading what it&rsquo;s doing I realize I made a logical error: if Mastodon <strong>always</strong> counts URLs as 23 chars, <strong>I can&rsquo;t truncate a URL and make it work.</strong> The logic Claude made was reasonable since I told it to <strong>never</strong> raise an exception. So that means I need to make this case an explicit negative case, that it will fail the validation, and I could exemplify that <strong>better</strong> by making it a really short but valid URL (ex <a href="https://example.com">https://example.com</a> which is 19 chars)</p>
<p>Okay, asked Claude to get to it, but it&rsquo;s struggling with the logic and I&rsquo;m realizing that I should just add another test alongside this where we have a long URL at the start and a really long text at the end, that we can just truncate as needed, so it can see the logic better.</p>
<p>BUT, for right now, I&rsquo;m gonna start a stew so I&rsquo;ll go do that and then let it… stew for a coupe of hours so I can continue then</p>
<p><strong>20:15</strong> back and doing stuff again</p>
<p>I added another test that swaps the order so it&rsquo;s <code>&lt;url&gt; &lt;text&gt;</code> and now it&rsquo;s failing too, so let&rsquo;s clear the session for Claude and start over</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">these two tests are failing because the business logic is incorrect, suggest how we can fix the business logic</span></span></code></pre></div>
</div>
<p>Claude came back with a plan to let the server fail instead of us truncating long tests, which won&rsquo;t make the tests pass, so I prompted:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">before our changes this library truncated any text &gt;500 chars, so we need to retain that logic. BUT if we end with a URL let it through and let the server</span></span></code></pre></div>
</div>
<p>Claude then suggested a flow that seems to work:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">New logic:
</span></span><span class="line"><span class="cl">1. First check actual length (not effective length)
</span></span><span class="line"><span class="cl">2. If text.length &lt;= 500 → return text as-is (no truncation)
</span></span><span class="line"><span class="cl">3. If text.length &gt; 500:
</span></span><span class="line"><span class="cl">  - Check if characters after position 500 are part of a URL
</span></span><span class="line"><span class="cl">  - If yes (would break a URL) → return full text unchanged
</span></span><span class="line"><span class="cl">  - If no (normal text overflow) → truncate at position 500
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This preserves backward compatibility:
</span></span><span class="line"><span class="cl">- Original behavior: truncate at 500 actual characters
</span></span><span class="line"><span class="cl">- New behavior: same, BUT skip truncation if it would break a URL</span></span></code></pre></div>
</div>
<p>And Claude implemented the fix and did it TDD style and highlighted that we have more code than necessary to pass, so I need to write another test case that exercises all the extra code we have in <code>truncateWithUrlPreservation</code></p>
<h2 id="2025-10-11">
  2025-10-11
  <a href="#2025-10-11" class="heading-anchor" aria-label="Direct link to 2025-10-11"><span aria-hidden="true">#</span></a>
</h2>
<p><strong>14:43</strong> The next day, I ended up watching TV yesterday, and today I&rsquo;m feeling sluggish</p>
<p>But I was reflecting about this yesterday and I&rsquo;m a bit unsure about how to write a test that refuses the current &ldquo;fix&rdquo; of &quot; if the actual is less than 500 let through&quot; because it&rsquo;s true since the edge-cases it won&rsquo;t do it for is basically where the URL is wrong, BUT, the URL could be wrong at the start and the text after is okay to be truncated. I.e. cases where say <code>https://example.com</code> (19) x 21 =  = 399 actual but the effective length is 483 and if I add 18 characters of text it&rsquo;ll fail and could be truncated.</p>
<p>Maybe that&rsquo;s the case to do, because it&rsquo;ll force it to handle it correctly.</p>
<p>I ended up changing the &ldquo;this can be truncated&rdquo; test so the URL is shorter and the text is exactly so it <em>should</em> pass, and then pointed out that we need to hint that we need the special logic if there are URLs in the text.</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">run the tests and suggest how to fix the them while not changing any tests, the underlying business logic is incorrect and too complex. Hint: consider only doing the special handling logic if there is a URL in the status text</span></span></code></pre></div>
</div>
<p>Claude came back with a plan:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">New simplified logic:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Extract URLs first to determine which path to take
</span></span><span class="line"><span class="cl">2. If NO URLs in text:
</span></span><span class="line"><span class="cl">  - Simple check: text.length &lt;= 500 → return as-is
</span></span><span class="line"><span class="cl">  - Otherwise → truncate at 500 actual characters
</span></span><span class="line"><span class="cl">3. If HAS URLs in text:
</span></span><span class="line"><span class="cl">  - Calculate effective length
</span></span><span class="line"><span class="cl">  - If effective ≤ 500 → return as-is
</span></span><span class="line"><span class="cl">  - If effective &gt; 500:
</span></span><span class="line"><span class="cl">      - Calculate how many actual characters to remove: excess = effective - 500
</span></span><span class="line"><span class="cl">    - Target length: targetLength = text.length - excess
</span></span><span class="line"><span class="cl">    - Check if truncating at targetLength would break a URL (cut it in half)
</span></span><span class="line"><span class="cl">    - If YES (would break URL) → return full text unchanged (let Mastodon fail)
</span></span><span class="line"><span class="cl">    - If NO (safe to truncate) → return text.slice(0, targetLength)</span></span></code></pre></div>
</div>
<p>And that update gave me the logic I wanted, so now let&rsquo;s see what the change looks like and it&rsquo;s good.</p>
<p>As I now look at it and am preparing the commit message I realize, this doesn&rsquo;t fit the original design of this library/n8n node: it was blindly truncating, so I could also just truncate <em>just before</em> this URL so we can post. It doesn&rsquo;t <strong>fit</strong> with how I discovered this problem, which was a truncated URL (which was still longer than 23 characters but wrong).</p>
<p>So, with that, I will instead say: nix the URL because it goes with the grain of the library and people like me have to ensure they truncate correctly. It&rsquo;s the least surprising thing we can do. But, I have done all this work and despite realizing that I went down the wrong path, I should commit what I got so I can see the changes. I learned something and that&rsquo;s still a win.</p>
<p>Okay, done , and I asked Claude to look at the comments I&rsquo;ve tried to add to clarify the logic and it suggested some improvements to them which I&rsquo;ve folded into the commit.</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">review the changes we&#39;ve done, are the explanations in text clear enough that those reading the code would be able to understand?</span></span></code></pre></div>
</div>
<p><a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/5c311e2212bef9bbd5beb949d9e656c0ccba374a">Committed.</a></p>
<p>I&rsquo;m not sure if using Claude here has saved me time, but I have been doing it in low energy and while watching TV, so upside is that it got done compared to that I likely wouldn&rsquo;t have put in the full energy otherwise.</p>
<p>Although, partly that&rsquo;s because when fixing the bad stuff I didn&rsquo;t want to go in and learn how all the code worked, becasue I&rsquo;m relying on the tests to tell me what&rsquo;s working and not, so that&rsquo;s pure laziness on my part. And the fact it dug through the full codebase and made a first pass when I was turning in for the night is <em>sooooo</em> worth it. I&rsquo;m sure I would&rsquo;ve spent more time figuring out the codebase on my own otherwise.</p>
<p>But, I am also saying this <em>before</em> I have manually tested any of this. I have only run the tests so far and not plugged it into n8n and seen if it works. So my optimism might still be premature.</p>
<p>Okay, updated one test file and the corresponding business logic, let&rsquo;s start with the next test file (which I changed a little for the previous task, so I know there&rsquo;s work to do).</p>
<h3 id="2025-10-12">
  2025-10-12
  <a href="#2025-10-12" class="heading-anchor" aria-label="Direct link to 2025-10-12"><span aria-hidden="true">#</span></a>
</h3>
<p>I did some final cleanup yesterday and didn&rsquo;t document it all, it&rsquo;s <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commits/url-aware-post-length-calculation/">only available in the commits</a>, but I basically went over the changes as I would any PR at work and then raised it. The maintainer looked at it during the day and then <a href="https://github.com/redoracle/n8n-nodes-the-mastodon/pull/4#issuecomment-3394429927">left a nice note</a> thanking me for being thorough and documenting my code well 🥰</p>
<blockquote>
<p><strong>Hey Björn!</strong></p>
<p>Thank you so much for your outstanding contribution with PR #4! 🚀</p>
<p>Your pull request to fix the URL character counting behavior was absolutely <strong>fantastic</strong> - it addressed a real problem that many users probably didn&rsquo;t even realize they were experiencing. The fact that Mastodon counts all URLs as exactly 23 characters regardless of their actual length is such an important detail, and your implementation handles it perfectly.</p>
<h3 id="what-i-especially-appreciated-about-your-work">
  What I especially appreciated about your work:
  <a href="#what-i-especially-appreciated-about-your-work" class="heading-anchor" aria-label="Direct link to What I especially appreciated about your work:"><span aria-hidden="true">#</span></a>
</h3>
<p>✨ <strong>Thorough Research</strong> - You referenced the actual Mastodon source code to validate the behavior<br>
🧪 <strong>Comprehensive Testing</strong> - 19 new tests covering all the edge cases!<br>
📚 <strong>Excellent Documentation</strong> - Clear comments explaining the &ldquo;why&rdquo; behind the logic<br>
🔧 <strong>Smart Implementation</strong> - The decision to remove entire URLs rather than break them is brilliant<br>
🔒 <strong>Clean Integration</strong> - Minimal changes to existing code while adding powerful functionality</p>
<h3 id="the-impact">
  The Impact:
  <a href="#the-impact" class="heading-anchor" aria-label="Direct link to The Impact:"><span aria-hidden="true">#</span></a>
</h3>
<p>Your fix means users won&rsquo;t have their URLs unexpectedly broken when posting longer content. That&rsquo;s a real quality-of-life improvement that will prevent confusion and frustration.</p>
<h3 id="code-quality">
  Code Quality:
  <a href="#code-quality" class="heading-anchor" aria-label="Direct link to Code Quality:"><span aria-hidden="true">#</span></a>
</h3>
<p>The implementation is clean, well-tested, and follows all the best practices. It&rsquo;s the kind of contribution that makes maintaining open source projects a joy!</p>
<p><strong>Would you be interested in contributing more to this project?</strong> Your expertise with Mastodon&rsquo;s APIs and attention to detail would be incredibly valuable.</p>
<p>Thanks again for taking the time to identify this issue, implement a robust solution, and provide such thorough testing. This is exactly the kind of contribution that makes the open source community amazing! 🙌</p>
<p>Best regards</p>
</blockquote>
<p>As they said, these interactions make open source fun because it&rsquo;s people working and building stuff together 🙂</p>
]]></content:encoded></item></channel></rss>