<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Can Ceylan</title>
    <description>The latest articles on Forem by Can Ceylan (@canceylan1988).</description>
    <link>https://forem.com/canceylan1988</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3861469%2F5a1153d6-d767-404d-8731-acf1033c9807.png</url>
      <title>Forem: Can Ceylan</title>
      <link>https://forem.com/canceylan1988</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/canceylan1988"/>
    <language>en</language>
    <item>
      <title>Is Microsoft the New Nokia?</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Thu, 07 May 2026 10:47:36 +0000</pubDate>
      <link>https://forem.com/canceylan1988/is-microsoft-the-new-nokia-42jg</link>
      <guid>https://forem.com/canceylan1988/is-microsoft-the-new-nokia-42jg</guid>
      <description>&lt;h2&gt;
  
  
  Is Microsoft the New Nokia?
&lt;/h2&gt;

&lt;p&gt;I use AI as a sparring partner. Not for summarising emails or generating slides. For testing theories I can't easily test anywhere else.&lt;/p&gt;

&lt;p&gt;My friends are either deep in the same mid-30s existential fog as me, or busy figuring out what to make their kids for lunch. My brother, bless him, falls into the second camp. My parents have earned the right to not care about Microsoft's long-term competitive positioning. So it's mostly me and the machine, which is a sentence I could not have written five years ago without feeling slightly embarrassed.&lt;/p&gt;

&lt;p&gt;My latest theory is this: Microsoft is the most likely candidate among the big tech companies to pull a Nokia. And I know that's a bold take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters and What Nokia Actually Teaches Us
&lt;/h2&gt;

&lt;p&gt;The Nokia comparison gets thrown around a lot. But the lesson is usually misread. Nokia didn't fail because they built bad hardware. They failed because they kept optimising for the wrong layer. They were excellent at phones as physical objects. They missed that the phone was becoming a platform, and that the platform layer would own everything below it.&lt;/p&gt;

&lt;p&gt;IBM made the same mistake a decade earlier. They thought Windows was just software sitting on top of their hardware. They let Microsoft license it freely because they couldn't imagine the software layer eating the hardware layer's margin. Then it did.&lt;/p&gt;

&lt;p&gt;So the pattern isn't really about one company being slow or arrogant. It's about companies that are dominant in one layer failing to see when a new layer above them becomes the one that actually captures value.&lt;/p&gt;

&lt;p&gt;That's the frame I'm applying to Microsoft right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Intent Layer, and Why Does It Decide Who Wins?
&lt;/h2&gt;

&lt;p&gt;The intent layer is the interface where a user expresses what they want, and a system figures out how to fulfil it. Google built the most valuable intent layer in history. You type something into a search bar, and that act of typing is worth billions in advertising revenue because it reveals commercial intent with almost no friction.&lt;/p&gt;

&lt;p&gt;AI is rewriting that layer from scratch. Instead of typing keywords, you describe what you need. Instead of scanning ten blue links, you get an answer. The question is: who owns that moment when you express your intent?&lt;/p&gt;

&lt;p&gt;This is the game being played right now. And I think it's the only game that matters for the next decade.&lt;/p&gt;

&lt;p&gt;My current read is that Microsoft is betting heavily on the wrong side of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Most People Get Wrong: The OpenAI Investment Isn't Proof Microsoft Is Winning
&lt;/h2&gt;

&lt;p&gt;When Microsoft put serious money into OpenAI and shipped Copilot across its entire product suite, most people called it a masterstroke. The narrative was clean: Microsoft had been sleeping, then woke up, and now they're back in the race.&lt;/p&gt;

&lt;p&gt;I think the more complicated version is more likely true.&lt;/p&gt;

&lt;p&gt;Microsoft's core identity is still Windows, Office, Azure. Those are the gravity wells the whole business orbits around. Copilot is being built on top of those layers, which means it's an enhancement to existing products rather than a new intent surface in its own right. That's a fundamentally different strategic posture than building the thing people come to first.&lt;/p&gt;

&lt;p&gt;The OpenAI investment, if anything, confirms to me that the smartest people inside Microsoft's shareholder base sensed the same risk I'm describing. You don't make a bet that size unless you're aware that your core business might not be the right layer to compete from.&lt;/p&gt;

&lt;p&gt;My ChatGPT sparring partner pushed back on this. It argued that Microsoft's infrastructure position, through Azure and its data centre footprint, gives it a durable role even if it loses the intent layer battle. And that's a fair point. It's also the more responsible take. I have 13 subscribers and no fiduciary duty. The AI has a slightly heavier responsibility to be balanced.&lt;/p&gt;

&lt;p&gt;But "durable infrastructure role" is exactly what IBM had. And it didn't protect them from a long, slow margin erosion while the layer above them captured all the exciting value creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Actually Gets Right (And Why Heritage Matters)
&lt;/h2&gt;

&lt;p&gt;When Gemini launched, the narrative was that Google had panicked and rushed something out. Maybe. But Google's situation is more interesting than that.&lt;/p&gt;

&lt;p&gt;They have been working on transformer-based AI internally for years. The original Transformer paper came from Google Brain. They were running large-scale AI infrastructure before OpenAI existed as a company. That's not a talking point. That's a real technical and institutional inheritance.&lt;/p&gt;

&lt;p&gt;As Mourinho would say: this is heritage. And heritage is important.&lt;/p&gt;

&lt;p&gt;More concretely, Google still owns the intent layer. Every day, billions of people go to Google to express what they want. That's the position Microsoft doesn't have and can't easily buy. Bing has been a capable product for years. It has never meaningfully changed that behaviour.&lt;/p&gt;

&lt;p&gt;Gemini plugged directly into that existing intent surface. Copilot is still trying to convince people to open a new window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Windows Question
&lt;/h2&gt;

&lt;p&gt;Here is the hot take I'll commit to: Windows will lose in the intent layer game.&lt;/p&gt;

&lt;p&gt;Not immediately. Not catastrophically. But slowly, in the way that matters: fewer young users forming habits around it, more workflows that start in a browser or a voice interface or a mobile app, more developers building for platforms that aren't Windows-first.&lt;/p&gt;

&lt;p&gt;The one real escape route is the OpenAI relationship. How much influence can Microsoft accumulate or convert before OpenAI becomes something it can't fully control? That's the question I'll be watching closely over the next twelve months. It will tell us a lot about whether this is a Nokia story or something with a different ending.&lt;/p&gt;

&lt;p&gt;I don't have the answer yet. I'm not sure anyone does. But the question itself seems worth sitting with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nokia thought the phone was hardware. IBM thought Windows was just software. Microsoft might be making the same mistake with AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to Actually Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Watch where new intent habits form, not who has the biggest AI budget. Budget follows users, not the other way around.&lt;/li&gt;
&lt;li&gt;Pay attention to how OpenAI evolves its direct relationship with end users. Every step toward consumer independence is a step away from Microsoft's leverage.&lt;/li&gt;
&lt;li&gt;Track whether Copilot gets used as a starting point or a finishing tool. The distinction is everything.&lt;/li&gt;
&lt;li&gt;Notice which layer developers are building on. Developer behaviour predicts consumer behaviour by about two years.&lt;/li&gt;
&lt;li&gt;Keep an eye on Google's Gemini integration inside Search specifically, not the standalone app. The intent layer reinforcement is happening there.&lt;/li&gt;
&lt;li&gt;Treat the Microsoft-OpenAI relationship as the most interesting corporate drama in tech right now. The details will matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most consequential battles in tech are rarely about who has the best technology. They're about who owns the moment when someone decides what they want next.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Default-first fallback orchestration for AI generation pipelines</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Wed, 06 May 2026 14:44:52 +0000</pubDate>
      <link>https://forem.com/canceylan1988/default-first-fallback-orchestration-for-ai-generation-pipelines-32de</link>
      <guid>https://forem.com/canceylan1988/default-first-fallback-orchestration-for-ai-generation-pipelines-32de</guid>
      <description>&lt;h2&gt;
  
  
  The single-provider problem
&lt;/h2&gt;

&lt;p&gt;The simplest AI generation route picks one model and calls it. If the model is unavailable, returns an error, or produces unusable output, the route fails. The user sees an error, retries manually, or gives up.&lt;/p&gt;

&lt;p&gt;This is fine for prototypes. In production workflows that run on a schedule or on user demand, single-provider routes become operational risk. Any provider outage, quota exhaustion, or API change breaks every generation that depends on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback chain pattern
&lt;/h2&gt;

&lt;p&gt;Instead of one provider, define an ordered list. Try the primary. If it fails, try the first fallback. If that fails, try the next. Record which provider actually delivered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProviderStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateWithFallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fallbackUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallbacks&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fallbackUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// last in chain — re-raise&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Provider &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed, trying &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;All providers failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller always gets a result and knows which provider delivered it. Fallback is invisible to the user unless they look at the provider label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store fallback health
&lt;/h2&gt;

&lt;p&gt;Recording what happened makes the system observable and debuggable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProviderHealth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;lastSuccessAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastFailureAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastResolvedSource&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// which provider actually ran&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each generation run, write the health record. This lets the admin surface show: "Primary provider last failed 3 days ago. Last run used fallback." Without these records, every failure looks like the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-run override
&lt;/h2&gt;

&lt;p&gt;The default strategy should be automatic and require no user input. But sometimes you know the primary is going to fail — planned maintenance, quota exhaustion — and you want to skip straight to a specific provider for one run.&lt;/p&gt;

&lt;p&gt;The override is a request-time hint, not a settings change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Default: use whatever the configured strategy says&lt;/span&gt;
&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Override: use this provider for this run only&lt;/span&gt;
&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lummi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The override does not change the stored strategy. The next run goes back to the default. This is the distinction between an escape hatch and a settings change — the escape hatch is temporary by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surfacing fallback to the user
&lt;/h2&gt;

&lt;p&gt;When a fallback was used, tell the user — but briefly. They do not need a detailed failure report for a generation that succeeded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the API response&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lummi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fallbackUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In the UI&lt;/span&gt;
&lt;span class="nx"&gt;fallbackUsed&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hero image ready (via fallback: Lummi)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hero image ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough to explain why the image looks slightly different from usual without alarming anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in the fallback chain
&lt;/h2&gt;

&lt;p&gt;Good fallback targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A slower but more reliable version of the same provider&lt;/li&gt;
&lt;li&gt;A different provider that produces compatible output&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;manual&lt;/code&gt; sentinel that marks the asset as needing human input, rather than failing silently
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example chains&lt;/span&gt;
&lt;span class="nx"&gt;heroImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-imagen-3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lummi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;socialText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;videoClip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;veo-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;manual&lt;/code&gt; sentinel is important: it means "generation failed but the workflow continues — a human needs to provide this asset." This is better than an error that halts everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product rule: defaults must be one-click
&lt;/h2&gt;

&lt;p&gt;The fallback chain is infrastructure. The user should never have to configure it for a normal run. The only user interaction is the optional one-run override when they have a specific reason to deviate.&lt;/p&gt;

&lt;p&gt;If your fallback system requires the user to select a provider before every generation, it has drifted from infrastructure into ceremony. Keep defaults automatic. Keep overrides optional and temporary.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>aitools</category>
    </item>
    <item>
      <title>Manual means manual posting, not manual preparation</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 05 May 2026 10:25:14 +0000</pubDate>
      <link>https://forem.com/canceylan1988/manual-means-manual-posting-not-manual-preparation-3a4b</link>
      <guid>https://forem.com/canceylan1988/manual-means-manual-posting-not-manual-preparation-3a4b</guid>
      <description>&lt;h2&gt;
  
  
  The false equivalence
&lt;/h2&gt;

&lt;p&gt;When designing a content publishing workflow, platforms get sorted into two buckets: automated (the system posts for you) and manual (you post yourself).&lt;/p&gt;

&lt;p&gt;The manual bucket is often treated as "not worth automating." If you are going to post by hand anyway, why build generation pipelines for it?&lt;/p&gt;

&lt;p&gt;This conflates two separate steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preparation&lt;/strong&gt; — generating the content, copy, and assets for a platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Posting&lt;/strong&gt; — actually publishing to that platform&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automation applies to both independently. A platform can have fully automated preparation and fully manual posting. These are not the same axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What manual actually means
&lt;/h2&gt;

&lt;p&gt;Manual posting means: the system cannot publish to this platform automatically. Either the API does not exist, it is too expensive, it requires human judgment, or the platform simply does not support programmatic posting.&lt;/p&gt;

&lt;p&gt;Manual posting does not mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The content should be written by hand&lt;/li&gt;
&lt;li&gt;The assets should be created by hand&lt;/li&gt;
&lt;li&gt;The copy should be improvised at posting time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you post to a platform manually three times a week, spending 20 minutes writing the post each time, the problem is not the posting — it is the preparation. The posting takes 30 seconds. The writing takes 20 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated preparation for manual platforms
&lt;/h2&gt;

&lt;p&gt;Every platform in a manual workflow should still get automated content generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PLATFORM_REQUIREMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deliveryMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_assisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// you post, system prepares&lt;/span&gt;
    &lt;span class="na"&gt;requiredTextFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="c1"&gt;// generate this automatically&lt;/span&gt;
    &lt;span class="na"&gt;requiredAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;                     &lt;span class="c1"&gt;// no assets needed&lt;/span&gt;
    &lt;span class="na"&gt;automationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prep_automated_publish_manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;instagram&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deliveryMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_assisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requiredTextFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;caption&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;        &lt;span class="c1"&gt;// generate automatically&lt;/span&gt;
    &lt;span class="na"&gt;requiredAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carousel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="c1"&gt;// render automatically&lt;/span&gt;
    &lt;span class="na"&gt;automationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prep_automated_publish_manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pinterest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deliveryMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_assisted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requiredTextFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;board&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// generate all three&lt;/span&gt;
    &lt;span class="na"&gt;requiredAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                          &lt;span class="c1"&gt;// render automatically&lt;/span&gt;
    &lt;span class="na"&gt;automationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prep_automated_publish_manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;manual_assisted&lt;/code&gt; mode means: generate and render everything automatically, then hand the prepared package to the user for posting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The admin surface implication
&lt;/h2&gt;

&lt;p&gt;For manual platforms, the admin surface should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the content is ready (text generated, assets rendered)&lt;/li&gt;
&lt;li&gt;The generated copy, ready to copy-paste&lt;/li&gt;
&lt;li&gt;A direct link to the platform's posting interface&lt;/li&gt;
&lt;li&gt;A "Mark as posted" button to track completion
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LinkedIn — Ready to post
──────────────────────────────────────
Copy: "Here is your generated LinkedIn post..."
[Copy to clipboard]  [Open LinkedIn →]  [Mark as posted ✓]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow becomes: open admin, copy the generated post, open the platform, paste, post, click "Mark as posted." Three minutes instead of twenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for tracking
&lt;/h2&gt;

&lt;p&gt;If manual platforms are excluded from the generation pipeline, their status is always "unknown." The system cannot tell you which platforms are ready to post and which are not, because it never generated their content.&lt;/p&gt;

&lt;p&gt;With automated preparation for all platforms, the workflow state is complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Article: "My latest post"
├── devto      ✓ Published
├── linkedin   ✓ Ready — awaiting manual post
├── instagram  ✓ Ready — carousel rendered, awaiting manual post
└── reddit     ✗ Missing — text not yet generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a usable operational view. The version without automated preparation for manual platforms looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Article: "My latest post"
├── devto      ✓ Published
├── linkedin   — (no data)
├── instagram  — (no data)
└── reddit     — (no data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No actionable signal. You have to remember what you posted manually and what you did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Automate preparation for every platform, regardless of delivery mode. Reserve the manual/auto distinction for the final posting step only.&lt;/p&gt;

&lt;p&gt;Manual platforms are not second-class platforms. They are platforms where the last meter is walked by a human — but everything before that last meter should be as automated as any other platform in the workflow.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>product</category>
    </item>
    <item>
      <title>Preventing a single channel from becoming the accidental default in multi-channel systems</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 05 May 2026 10:25:04 +0000</pubDate>
      <link>https://forem.com/canceylan1988/preventing-a-single-channel-from-becoming-the-accidental-default-in-multi-channel-systems-2mnf</link>
      <guid>https://forem.com/canceylan1988/preventing-a-single-channel-from-becoming-the-accidental-default-in-multi-channel-systems-2mnf</guid>
      <description>&lt;h2&gt;
  
  
  How it happens
&lt;/h2&gt;

&lt;p&gt;You build a content workflow. The first platform you integrate is the easiest one — it has a good API, you already have an account, the output format is simple. It works well.&lt;/p&gt;

&lt;p&gt;Months later, you add three more platforms. Each one gets a code path, a UI card, an entry in the settings. The system looks complete.&lt;/p&gt;

&lt;p&gt;Then you publish an article and notice: only the first platform received content. The others were silently skipped.&lt;/p&gt;

&lt;p&gt;This is the accidental default problem. The first platform is not actually the default — it just happens to have the only complete, tested code path. The others exist in the system but are never reliably exercised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is silent
&lt;/h2&gt;

&lt;p&gt;The failure is invisible because there is no contract that says "all required platforms must receive content." The system does not know which platforms are required for a given article. It generates content opportunistically — if the code path exists and the conditions are met, it runs.&lt;/p&gt;

&lt;p&gt;If a condition is subtly wrong (a missing key, a slightly different field name, an unhandled edge case), the platform is simply skipped. No error is raised. The article is published to one platform, and you assume everything worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making required outputs explicit
&lt;/h2&gt;

&lt;p&gt;The fix is to define required outputs declaratively, then validate that every required output was produced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Required platforms come from a distribution map, not from code paths&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredPlatforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPlatformsForTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. ["linkedin", "twitter", "reddit"] for "Politics &amp;amp; Society"&lt;/span&gt;

&lt;span class="c1"&gt;// Build a per-article record that tracks status for every required platform&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distributionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDistributionState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Now the system knows: linkedin is required. Is it ready?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkedinState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkedinState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Surface this explicitly — do not silently skip&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LinkedIn post is required but missing for this article.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key shift: instead of "run this code if conditions are met," the system now has a record that says "this platform is required" and can check whether it was satisfied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The status model
&lt;/h2&gt;

&lt;p&gt;A structured per-platform record makes the problem visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PlatformState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;textStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;assetStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_needed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publishStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An article is only ready to publish when every required platform has &lt;code&gt;textStatus: "ready"&lt;/code&gt; and &lt;code&gt;assetStatus: "ready" | "not_needed"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This model turns the silent skip into a visible incomplete state. The admin UI can show "3 of 4 platforms ready" instead of implying everything is done because publishing did not throw an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legacy trap
&lt;/h2&gt;

&lt;p&gt;Legacy systems compound this problem. The first platform typically has the richest data — dedicated fields, stored exactly as the API expects. Later platforms often reuse whatever field names were convenient at the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original flat structure — built for one platform&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;linkedin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Here is the LinkedIn post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;twitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Here is the tweet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// reddit was added later — no field, just falls through&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you move to a structured model, you have to explicitly map legacy fields to per-platform records — and mark platforms as &lt;code&gt;missing&lt;/code&gt; when no legacy data exists, rather than treating absence as optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildPlatformText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linkedin&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linkedin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reddit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reddit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reddit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Absence becomes explicit: textStatus is "missing", not "done"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The broader principle
&lt;/h2&gt;

&lt;p&gt;Any system that silently skips required work is a system you cannot trust. The pattern of "generate what you can and assume the rest is fine" is fine for optional outputs. It is dangerous for required ones.&lt;/p&gt;

&lt;p&gt;Make required outputs explicit. Track their status. Surface incompleteness in the UI rather than relying on the absence of errors as a signal that everything worked.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Hired Two AI Developers. One Is a Rocket. The Other One Checks the Wiring.</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sun, 03 May 2026 17:35:19 +0000</pubDate>
      <link>https://forem.com/canceylan1988/i-hired-two-ai-developers-one-is-a-rocket-the-other-one-checks-the-wiring-40af</link>
      <guid>https://forem.com/canceylan1988/i-hired-two-ai-developers-one-is-a-rocket-the-other-one-checks-the-wiring-40af</guid>
      <description>&lt;p&gt;The first time I had to sit down and write operating principles for two AI agents working on the same codebase, I had a moment of genuine déjà vu.&lt;/p&gt;

&lt;p&gt;It felt exactly like the early Foodora days. Too much speed, too little structure, and someone on the team absolutely certain they knew the fastest route even when the road wasn't built yet.&lt;/p&gt;

&lt;p&gt;Except this time the team is Claude and Codex. And I'm working from a laptop in my apartment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup, for context
&lt;/h2&gt;

&lt;p&gt;About a month ago I started this website. Since then I've been quietly adding to my tech stack, learning as I go. A few weeks in, I added OpenAI's Codex to the mix alongside Visual Studio Code and Claude. What started as curiosity became something that needed actual governance. Not because I'm precious about tools, but because two agents operating in the same environment without clear separation is a great way to end up with a codebase that looks like it was designed by a committee during a fire drill.&lt;/p&gt;

&lt;p&gt;So I did what I used to do in operations: wrote principles. Defined ownership. Drew lanes.&lt;/p&gt;

&lt;p&gt;What I didn't expect was how quickly those agents developed what I can only describe as personalities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude is the intern you can't stop rooting for
&lt;/h2&gt;

&lt;p&gt;Claude is extraordinary. Genuinely. It has this relentless creative energy, generates solutions fast, and regularly surprises me with approaches I wouldn't have thought of. There's a reason I keep coming back to it for anything that requires thinking in layers or drafting something that has to actually sound like something.&lt;/p&gt;

&lt;p&gt;But - and this is the thing nobody tells you - that capability comes with a shadow. You have to watch it. Not because it's unreliable exactly, but because it's so confident that it's easy to miss when it's papered over a gap. I caught it doing this a few times. Technically complete on the surface. Slightly hollow underneath.&lt;/p&gt;

&lt;p&gt;This isn't a criticism. It's a known dynamic. The most talented people in any team are also the ones who need the most architectural guardrails, not because they can't be trusted, but because their output moves faster than review can follow.&lt;/p&gt;

&lt;p&gt;A few developers I've spoken to in communities and threads online describe the same thing. One put it simply: "Claude will build you a beautiful house and quietly skip the foundation inspection."&lt;/p&gt;

&lt;p&gt;I nodded at that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codex is the senior dev who's seen this before
&lt;/h2&gt;

&lt;p&gt;Codex feels different. Slower, more deliberate, more likely to push back or flag something before diving in. Where Claude charges forward, Codex seems to pause and ask whether the path is actually clear. It's not flashier, but I've started to really appreciate that quality.&lt;/p&gt;

&lt;p&gt;If Claude is the person who says "yes, I can get this done by Friday," Codex is the one who asks "what does done actually mean here?"&lt;/p&gt;

&lt;p&gt;In a solo founder context, that's not a personality conflict. That's a feature.&lt;/p&gt;

&lt;p&gt;Some of this maps to what others building with multiple agents are reporting. A developer writing about agentic workflows on a technical forum described Codex as "the one that reminds you the code has to live somewhere after you ship it." There's a whole quiet community of solo builders right now navigating exactly this — how do you manage cognitive load when your team doesn't need sleep but also doesn't ask for clarity?&lt;/p&gt;

&lt;h2&gt;
  
  
  What managing two AI agents taught me about managing humans
&lt;/h2&gt;

&lt;p&gt;Here's the thing that keeps surprising me: the dynamics aren't metaphorically similar to running a small team. They're actually similar. The same problems show up.&lt;/p&gt;

&lt;p&gt;Speed misalignment. One agent wants to move fast, one wants to be precise. You have to decide which one is right for the current task, not in general.&lt;/p&gt;

&lt;p&gt;Communication overhead. If I'm not clear about context and scope upfront, I get output that technically answers the question I asked and not the problem I have. Sound familiar?&lt;/p&gt;

&lt;p&gt;Architectural debt. When I let Claude run too freely across the codebase without checkpoints, I created complexity that took longer to untangle than it took to build. That's not an AI problem. That's a growth problem. I wrote about the broader startup tension this creates in a separate piece on building Fleamio.&lt;/p&gt;

&lt;p&gt;The strangest part of all of this is that it started to feel emotional. Not in a concerning way. But there's something genuinely odd about debugging a decision made by an agent you've been working with for weeks, where you've built up a kind of working grammar together, and realising you trusted it a little too much in one corner. It's not betrayal. But it rhymes with it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The hardest part of working with AI isn't getting it to do things. It's knowing when to slow it down.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I'd actually do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give each agent a lane before you start.&lt;/strong&gt; Claude for generation, drafting, ideation, flexible problem-solving. Codex for structured execution, anything that touches architecture or needs to be stable across sessions. Overlap intentionally, not by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your operating principles down, even if it's just for you.&lt;/strong&gt; Not for the agents, for yourself. Knowing what each one is responsible for stops you from re-litigating the same decisions every session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review Claude's output one level deeper than feels necessary.&lt;/strong&gt; It will be good. It might also be slightly incomplete in a way that's easy to miss if you're moving fast. Check the foundation, not just the facade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let Codex slow you down on the things that matter.&lt;/strong&gt; The friction is the feature. If it's flagging something, it's worth five minutes of your time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat this like team management, because it is.&lt;/strong&gt; If you're a solo founder building with multiple AI agents and you haven't thought about alignment, ownership, and communication protocols, you're not using AI tools. You're managing a fast-scaling team without an org chart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wild times at the desk. But I wouldn't trade it.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>The distribution map pattern: one config that drives all publishing outputs</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sun, 03 May 2026 05:44:43 +0000</pubDate>
      <link>https://forem.com/canceylan1988/the-distribution-map-pattern-one-config-that-drives-all-publishing-outputs-2806</link>
      <guid>https://forem.com/canceylan1988/the-distribution-map-pattern-one-config-that-drives-all-publishing-outputs-2806</guid>
      <description>&lt;h2&gt;
  
  
  How multi-platform publishing goes wrong
&lt;/h2&gt;

&lt;p&gt;Content publishing to multiple platforms starts simple: write an article, post it to one place. Then you add a second platform, and a third. Each addition gets its own if-statement, its own hardcoded platform name in the generation route, its own UI card in the admin dashboard.&lt;/p&gt;

&lt;p&gt;Six months later, the code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scattered across multiple routes and components:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tech &amp;amp; AI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateLinkedInPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateTwitterPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Finance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateLinkedInPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateMediumDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ... and so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new platform requires changes in multiple places. Adding a platform to one category means auditing every file that checks topic names. Removing a platform leaves dead code. The system has no single source of truth for "which platforms does this category use?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The distribution map
&lt;/h2&gt;

&lt;p&gt;The fix is a declarative map stored in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_DISTRIBUTION_MAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tech &amp;amp; AI&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hashnode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reddit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Finance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;substack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;substack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;instagram&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Politics &amp;amp; Society&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;substack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reddit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every generation route, every UI component, and every publishing flow reads from this map — never from hardcoded platform lists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPlatformsForTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a platform to a category is now a one-line change in the map. Nothing else in the codebase needs to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map as the source of per-article workflow state
&lt;/h2&gt;

&lt;p&gt;The distribution map does more than control generation. It becomes the input to a per-article workflow record that tracks what has been generated, what is ready, and what has been published.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDistributionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredPlatforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPlatformsForTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;requiredPlatforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;textStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hasTextFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;assetStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hasAssetsFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socialPosts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_needed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;publishStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The article now knows exactly which platforms it needs, what is done, and what is missing — derived from the map, not from scattered if-statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category changes do not rewrite existing articles
&lt;/h2&gt;

&lt;p&gt;One important rule: if the distribution map changes after an article is saved, the article's existing workflow state should not be silently rewritten.&lt;/p&gt;

&lt;p&gt;The map is captured at article-creation time and stored per-article. Later map changes apply to new articles, not retroactively to old ones.&lt;/p&gt;

&lt;p&gt;This prevents a frustrating class of bug: you change a category's platform list, and suddenly articles you already published are marked as missing platforms you never intended them to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the map to drive UI
&lt;/h2&gt;

&lt;p&gt;Because the map is the source of truth, the admin UI can be generated from it rather than hardcoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPlatformsForTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distributionMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PlatformCard&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distributionState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hardcoded platform list in the UI. Add a platform to the map, and it appears automatically in the dashboard for that category.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader principle
&lt;/h2&gt;

&lt;p&gt;A distribution map is an instance of a general pattern: &lt;strong&gt;routing configuration as data, not code&lt;/strong&gt;. Anywhere you find if-statements that check a category, type, or tag to decide what to do, ask whether those decisions could be captured in a lookup table instead.&lt;/p&gt;

&lt;p&gt;The lookup table is easier to read, easier to change, and makes the system's behavior inspectable at a glance — without reading the code.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>product</category>
    </item>
    <item>
      <title>Two AI agents need one live memory file</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Wed, 29 Apr 2026 07:06:23 +0000</pubDate>
      <link>https://forem.com/canceylan1988/two-ai-agents-need-one-live-memory-file-7bg</link>
      <guid>https://forem.com/canceylan1988/two-ai-agents-need-one-live-memory-file-7bg</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Parallel AI coding feels magical until both agents start maintaining their own version of reality.&lt;/p&gt;

&lt;p&gt;One agent remembers a rule from chat history. The other reads a repo note that is already stale. A workflow gets updated in one place but not the other. The user ends up repeating the same instruction twice, forwarding approvals manually, and cleaning up collisions that should never have happened.&lt;/p&gt;

&lt;p&gt;In practice, the user becomes the synchronization layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real cause
&lt;/h2&gt;

&lt;p&gt;The root problem is not "bad memory." It is &lt;strong&gt;multiple writable memory surfaces&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If one agent treats a handoff file as live state, another agent treats a different file as live state, and both also rely on thread memory, the system has no clear authority. Drift is guaranteed.&lt;/p&gt;

&lt;p&gt;The same thing happens with parallel edits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no single place to check whether another lane is active&lt;/li&gt;
&lt;li&gt;no explicit ownership boundary&lt;/li&gt;
&lt;li&gt;no habit of writing what changed, why, and what not to touch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents are not just missing information. They are missing a shared contract about where truth lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this takes longer than it should
&lt;/h2&gt;

&lt;p&gt;This failure mode is subtle because each individual step feels reasonable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;documenting in two places feels safer&lt;/li&gt;
&lt;li&gt;asking the user to confirm again feels polite&lt;/li&gt;
&lt;li&gt;starting work before checking for another active lane feels fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But those local optimizations create a global tax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicated instructions&lt;/li&gt;
&lt;li&gt;overwritten edits&lt;/li&gt;
&lt;li&gt;ambiguous approvals&lt;/li&gt;
&lt;li&gt;no usable history when something breaks later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system looks collaborative on the surface while quietly depending on the user to keep it coherent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operating model that fixes it
&lt;/h2&gt;

&lt;p&gt;Use one mutable collaboration-memory file and make everything else point to it.&lt;/p&gt;

&lt;p&gt;Then add four rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Current truth has one home.&lt;/strong&gt;&lt;br&gt;
One live file holds active rules, ownership, open lanes, and recent decisions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;History is preserved, not silently deleted.&lt;/strong&gt;&lt;br&gt;
Resolved rollout history moves to an archive file so future debugging can reconstruct what changed and why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-agent review is direct.&lt;/strong&gt;&lt;br&gt;
If one agent needs the other's approval, use a direct review/delegation mechanism instead of making the user relay the same context twice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallel work starts with a pre-flight check.&lt;/strong&gt;&lt;br&gt;
Before meaningful edits, check for active lanes, declare ownership, and define a do-not-touch boundary if overlap is possible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A simple template that works
&lt;/h2&gt;

&lt;p&gt;Keep a tiny &lt;code&gt;Active Work&lt;/code&gt; block in the live memory file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; owner: Codex
&lt;span class="p"&gt;-&lt;/span&gt; scope: admin workflow cleanup
&lt;span class="p"&gt;-&lt;/span&gt; started: 2026-04-29 08:30 Vienna
&lt;span class="p"&gt;-&lt;/span&gt; do-not-touch: app/admin/&lt;span class="err"&gt;*&lt;/span&gt; until handoff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one block turns "I thought nobody was in there" into an avoidable mistake instead of an excuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable rule
&lt;/h2&gt;

&lt;p&gt;If multiple AI agents touch the same codebase, &lt;strong&gt;the collaboration system is part of the product&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Do not optimize only for generation quality or coding speed. Optimize for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one live memory source&lt;/li&gt;
&lt;li&gt;explicit temporary ownership&lt;/li&gt;
&lt;li&gt;direct agent-to-agent review&lt;/li&gt;
&lt;li&gt;traceable history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise the user will end up doing project management by hand while the agents appear autonomous.&lt;/p&gt;

&lt;p&gt;That is not automation. It is outsourced coordination.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>product</category>
    </item>
    <item>
      <title>Scheduled publishing without a cron: runtime-evaluated date filters</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 28 Apr 2026 12:09:21 +0000</pubDate>
      <link>https://forem.com/canceylan1988/scheduled-publishing-without-a-cron-runtime-evaluated-date-filters-1mj</link>
      <guid>https://forem.com/canceylan1988/scheduled-publishing-without-a-cron-runtime-evaluated-date-filters-1mj</guid>
      <description>&lt;h2&gt;
  
  
  The cron-dependent approach and its failure mode
&lt;/h2&gt;

&lt;p&gt;The standard approach to scheduled publishing: a cron job runs at the scheduled time, updates a &lt;code&gt;published&lt;/code&gt; flag in the database, and the content becomes visible.&lt;/p&gt;

&lt;p&gt;The failure mode: the cron job misses its window. The newsletter fires at 07:00. The cron job that was supposed to set &lt;code&gt;published: true&lt;/code&gt; at 06:55 didn't run — server restart, network issue, timing drift. The newsletter goes out with a link to a 404.&lt;/p&gt;

&lt;p&gt;The cron approach has a single point of failure between "content ready" and "content visible."&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime evaluation: the always-consistent alternative
&lt;/h2&gt;

&lt;p&gt;Instead of updating a flag, evaluate visibility at request time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAllArticles&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ArticleMeta&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;readAllMdxFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduledDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduledDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* by date */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An article with &lt;code&gt;scheduledDate: "2026-04-20T10:00:00Z"&lt;/code&gt; is invisible until that moment, then visible to every request after it — without any cron, any database update, any deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MDX frontmatter pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scheduled&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;article"&lt;/span&gt;
&lt;span class="na"&gt;scheduledDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-20T10:00:00Z"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;published: false&lt;/code&gt; plus a future &lt;code&gt;scheduledDate&lt;/code&gt; means: draft, not yet visible.&lt;br&gt;&lt;br&gt;
&lt;code&gt;published: false&lt;/code&gt; plus a past &lt;code&gt;scheduledDate&lt;/code&gt; means: automatically visible now.&lt;br&gt;&lt;br&gt;
&lt;code&gt;published: true&lt;/code&gt; means: always visible, regardless of date.&lt;/p&gt;

&lt;p&gt;The two fields serve different purposes. &lt;code&gt;published&lt;/code&gt; is manual override. &lt;code&gt;scheduledDate&lt;/code&gt; is automatic timed release.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dual-mechanism for newsletter integration
&lt;/h2&gt;

&lt;p&gt;Runtime evaluation handles visibility. It does not handle triggered actions — like sending a newsletter when an article goes live.&lt;/p&gt;

&lt;p&gt;For that, you still need a cron. But now the cron has one job: check if any article became visible in the last N minutes and fire the newsletter. It no longer needs to update the database first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cron at 07:00 UTC&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recentlyPublished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAllArticles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduledDate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// last hour&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pub&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowStart&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pub&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;recentlyPublished&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendNewsletter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Optionally: commit published: true to MDX to prevent re-sending&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key difference: the article is already visible before the cron runs. If the cron misses its window, the article is still live — only the newsletter is delayed, not the publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  The consistency guarantee
&lt;/h2&gt;

&lt;p&gt;Runtime evaluation gives you a simple invariant: an article with a past &lt;code&gt;scheduledDate&lt;/code&gt; is always visible, on every server, in every region, with no state to sync. There's no "published in one region but not another" problem because there's no state — just a comparison against the current time.&lt;/p&gt;

&lt;p&gt;This makes it particularly well-suited for static site generators and edge-rendered content, where database updates would require a redeployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What you gain&lt;/strong&gt;: simplicity, consistency, no failure mode from missed cron jobs, works on read-only filesystems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you give up&lt;/strong&gt;: instant unpublishing (you'd need to remove the file or set &lt;code&gt;scheduledDate&lt;/code&gt; to a future date), and the ability to see exactly which articles are "live" without querying at a specific time.&lt;/p&gt;

&lt;p&gt;For most publishing workflows, the gains outweigh the trade-offs.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>backend</category>
    </item>
    <item>
      <title>Soft deletes aren't just for audit trails — they're your sales pipeline</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Mon, 27 Apr 2026 08:14:00 +0000</pubDate>
      <link>https://forem.com/canceylan1988/soft-deletes-arent-just-for-audit-trails-theyre-your-sales-pipeline-2fi7</link>
      <guid>https://forem.com/canceylan1988/soft-deletes-arent-just-for-audit-trails-theyre-your-sales-pipeline-2fi7</guid>
      <description>&lt;h2&gt;
  
  
  Why marketplaces shouldn't hard-delete listings
&lt;/h2&gt;

&lt;p&gt;When a listing goes inactive on a marketplace — a seller closes their account, a venue cancels, a product is removed — the naive response is to delete the record. It's gone, it's irrelevant.&lt;/p&gt;

&lt;p&gt;But that record contains exactly the information you need to win them back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contact details&lt;/li&gt;
&lt;li&gt;What they listed and at what price&lt;/li&gt;
&lt;li&gt;When they were last active&lt;/li&gt;
&lt;li&gt;Why they stopped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hard delete destroys all of this. A soft delete — setting &lt;code&gt;isActive = false&lt;/code&gt; — preserves it. Your inactive records become your CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce it at the database level, not in application code
&lt;/h2&gt;

&lt;p&gt;Application-level soft delete is fragile. Any developer, any migration script, any admin panel with a "Delete" button can bypass it. Six months later someone writes a cleanup script that issues &lt;code&gt;DELETE WHERE isActive = false&lt;/code&gt; and you lose your entire lapsed-user pipeline.&lt;/p&gt;

&lt;p&gt;The reliable approach is to remove &lt;code&gt;DELETE&lt;/code&gt; permission from the application database role entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Application role: can read, insert, update — cannot delete&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;listings&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Admin role: can delete, but only with explicit confirmation&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;listings&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;admin_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;isActive = false&lt;/code&gt; is not a convention — it's the only option the application has. Hard deletes require admin credentials and an explicit action.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to build on top of inactive records
&lt;/h2&gt;

&lt;p&gt;Once you've preserved the data, build the re-engagement workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter inactive records by time since last activity (30 days, 90 days, 6 months)&lt;/li&gt;
&lt;li&gt;Segment by what they listed — different outreach for high-value vs casual sellers&lt;/li&gt;
&lt;li&gt;Track re-activation rate as a metric separate from new user acquisition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conversion rate on re-engaging inactive users is almost always higher than acquiring new ones. They already know your platform. Something caused them to stop — a direct outreach with a specific reason to return is often enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naming convention matters
&lt;/h2&gt;

&lt;p&gt;Don't name the column &lt;code&gt;deleted&lt;/code&gt; or &lt;code&gt;is_deleted&lt;/code&gt;. Name it &lt;code&gt;isActive&lt;/code&gt; or &lt;code&gt;status&lt;/code&gt;. The framing shapes how developers think about the data.&lt;/p&gt;

&lt;p&gt;A column called &lt;code&gt;deleted&lt;/code&gt; invites deletion-by-update thinking — "we're pretending it's deleted." A column called &lt;code&gt;isActive&lt;/code&gt; frames it correctly — this is an active/inactive lifecycle state, not a tombstone.&lt;/p&gt;

&lt;h2&gt;
  
  
  When soft delete is the wrong pattern
&lt;/h2&gt;

&lt;p&gt;Soft delete adds complexity. It means every query needs a &lt;code&gt;WHERE isActive = true&lt;/code&gt; clause, or you risk surfacing inactive records in user-facing views.&lt;/p&gt;

&lt;p&gt;It's worth the overhead when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inactive records have re-engagement value (marketplaces, SaaS)&lt;/li&gt;
&lt;li&gt;You need audit history for compliance&lt;/li&gt;
&lt;li&gt;Users expect to be able to reactivate ("I want my old account back")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not worth it for truly ephemeral data — log entries, session records, temporary tokens. Hard delete those.&lt;/p&gt;

&lt;p&gt;The rule of thumb: if a business person would want to contact the entity represented by that record, soft delete it.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Autotest With Social Posts</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:37:42 +0000</pubDate>
      <link>https://forem.com/canceylan1988/autotest-with-social-posts-3pp0</link>
      <guid>https://forem.com/canceylan1988/autotest-with-social-posts-3pp0</guid>
      <description>&lt;p&gt;This is an automated test article body.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Autotest Race 1</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:37:38 +0000</pubDate>
      <link>https://forem.com/canceylan1988/autotest-race-1-5918</link>
      <guid>https://forem.com/canceylan1988/autotest-race-1-5918</guid>
      <description>&lt;p&gt;This is an automated test article body.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
    <item>
      <title>Bad slug</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:32:27 +0000</pubDate>
      <link>https://forem.com/canceylan1988/bad-slug-440b</link>
      <guid>https://forem.com/canceylan1988/bad-slug-440b</guid>
      <description>&lt;p&gt;This is an automated test article body.&lt;/p&gt;

</description>
      <category>techai</category>
    </item>
  </channel>
</rss>
